/* ==========================================================================
   SwipeVocab — visual system
   Mobile-first. No dependencies, no build step.

   ONE IDEA: a dark desk, one lit card, two doors.

   The app is an unlit room and the CARD is the only thing with a lamp on it.
   Every judgement in this game is the physical act of sending that card left or
   right, so the two directions are built into the furniture — a rose rail down
   the left edge of the card, a mint rail down the right — and they light as the
   card moves toward them. That is what teaches the swipe; no sentence has to.

   Three rules hold the whole sheet together:

     Mint and rose are reserved words. They say "right" and "wrong" and they are
     spent nowhere else. Gold belongs to streaks and trophies alone.

     Nothing competes with the card. The room dims when a run starts, panels are
     quiet, and the loudest type in the interface is on screen for 600ms.

     One value per decision. Four radii, one gutter, three easing curves, one
     type ramp. A fifth of anything is a fifth personality.
   ========================================================================== */

:root {
  /* Colour values here are the fallbacks. css/themes.css sets them per mode and
     is where a palette is changed; the rest of this block is theme-independent. */

  /* ---- ground ----------------------------------------------------------
     Not #000. A pure-black OLED panel smears on the fast horizontal move a
     thrown card makes, and the near-black below costs nothing to avoid it. */
  --bg-void: #07080f;
  --bg-deep: #0b0d1a;
  --bg-lift: #141833;

  /* ---- ink on the card (the light surface) ---- */
  --ink: #16182c;
  --ink-soft: #565b7a;
  --ink-faint: #8f93b0;

  /* ---- text on the dark ---- */
  --text-1: #f5f6fc;
  --text-2: #adb2cf;   /* ~8:1 on --bg-deep */
  --text-3: #767b9b;   /* decorative and label use only */

  /* ---- meaning ----------------------------------------------------------
     Reserved. These two never appear on anything that is not an answer. */
  --yes: #2ee6a8;
  --yes-deep: #12b783;
  --no: #ff5a78;
  --no-deep: #e0243f;

  --accent: #6f6cf7;
  --accent-lit: #9b98ff;
  --cyan: #35d7f2;
  --gold: #fbbf24;
  --gold-lit: #ffe08a;

  /* ---- surfaces ---------------------------------------------------------
     `--felt` is the new one and it is why panels stopped disappearing. The
     old surface was 4.5% white on near-black, which on a phone in daylight is
     no surface at all — the mode buttons read as floating text. Felt is opaque
     and very slightly warm, so a panel is an object on the desk that catches a
     little of the room's light. */
  --felt: #12142a;
  --felt-lift: #191c38;
  --line: rgba(255, 255, 255, .09);
  --line-strong: rgba(255, 255, 255, .17);
  --panel: rgba(255, 255, 255, .045);
  --panel-lift: rgba(255, 255, 255, .075);

  /* ---- type -------------------------------------------------------------
     Fraunces is the dictionary: it carries the word being learned, the score
     and the praise. Plus Jakarta Sans is the machine around it. Assistant
     covers Hebrew. All three degrade to a real stack when the network is not
     there — which, for the single-file build, is most of the time. */
  --font-ui: 'Plus Jakarta Sans', 'Assistant', -apple-system, BlinkMacSystemFont,
             'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-display: 'Fraunces', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
  --font-he: 'Assistant', 'Plus Jakarta Sans', 'Segoe UI', Arial, sans-serif;

  /* One ramp, replacing roughly forty one-off pixel values. Every size in the
     interface is one of these seven, which is what makes two screens built
     months apart look like the same product. */
  --t-hero: clamp(30px, 9vw, 42px);   /* the word on the card */
  --t-display: 26px;                  /* screen titles */
  --t-lead: 17px;                     /* the meaning, definitions */
  --t-body: 14px;                     /* normal copy */
  --t-meta: 12px;                     /* secondary copy */
  --t-label: 10px;                    /* uppercase labels */
  --t-micro: 9px;                     /* stat captions */

  /* ---- shape ----
     Four radii. Card, panel, control, pill. Nothing else. */
  --r-card: 28px;
  --r-panel: 20px;
  --r-control: 14px;
  --r-pill: 999px;

  /* ---- space ----
     4pt base. One gutter, used by every screen, so nothing is ever "nearly"
     aligned with the thing above it. */
  --gutter: 18px;
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px;

  --shadow-card:
    0 2px 4px rgba(0, 0, 0, .3),
    0 20px 48px -14px rgba(0, 0, 0, .7);
  --shadow-lift: 0 12px 30px -14px rgba(0, 0, 0, .8);

  /* ---- the card box -----------------------------------------------------
     Fixed, not content-sized, and that is deliberate: three cards are stacked
     in this box at once, and a box that resized to each card's content would
     make the whole stack jump every time one was thrown.

     What it is NOT is the old 58vh, which left a definition-sized hole under a
     two-word Hebrew answer. The height is tuned per content type instead —
     app.js writes data-content on <html> — so a translation card is short and a
     definition card is tall, and neither has a void in it. */
  /* Narrowed from 88vw to leave the two gate rails a lane of their own either
     side. A rail pinned to the screen edge is a screen decoration; a rail
     beside the card is a door the card goes through. */
  --card-w: min(80vw, 330px);
  --card-h: min(56vh, 452px);

  /* ---- motion ----
     One overshoot curve, one settle curve, one throw curve. A fourth would be
     a fourth personality. */
  --ease-snap: cubic-bezier(.18, .89, .32, 1.28);
  --ease-out: cubic-bezier(.22, .7, .28, 1);
  --ease-throw: cubic-bezier(.16, .68, .32, 1);

  /* Written by Deck.js while a card is being dragged: -1 hard left, +1 hard
     right, 0 at rest. The gate rails read it directly, so the doors light from
     the same number that decides the answer. */
  --swipe: 0;
}

/* A translation is two or three words; a definition is a paragraph. Sizing the
   box to the shorter of the two is what removed the empty half of the card. */
[data-content="translation"] { --card-h: min(50vh, 404px); }

* { box-sizing: border-box; margin: 0; padding: 0; }

/* The `hidden` attribute is how every screen and panel here is toggled, and
   any `display` rule outranks the browser default for it. One card-swallowing
   invisible overlay was enough — this makes the attribute win everywhere. */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  overscroll-behavior: none;
}

body {
  font-family: var(--font-ui);
  color: var(--text-1);
  /* Explicit colour under the gradients: when this page is embedded, a
     background-image alone would let the host's own ground show through. */
  /* --chrome is the status-bar tint (js/theme.js). It sits under opaque
     gradients, so only the phone's bar ever shows it. */
  background-color: var(--chrome, var(--bg-void));
  background-image:
    radial-gradient(140% 90% at 50% -18%, var(--bg-glow) 0%, transparent 58%),
    linear-gradient(178deg, var(--bg-deep) 0%, var(--bg-void) 100%);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

/* ---------- Ambient light ----------
   Three slow blobs and a grain sheet. The blobs give the dark somewhere to go;
   the grain is what stops a wide, low-contrast gradient from banding into
   stripes on an 8-bit panel. Both are inert and both stop on request. */

.ambient {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  transition: opacity .6s var(--ease-out);
}

/* The room lowers its lights when a card is up. The blobs are the only thing
   in the interface that moves on its own, and a moving thing beside a card
   being read is a moving thing being read instead. */
.is-playing .ambient { opacity: .38; }

.ambient__blob {
  position: absolute;
  width: 62vmax;
  height: 62vmax;
  border-radius: 50%;
  filter: blur(70px);
  opacity: .5;
  will-change: transform;
}

.ambient__blob--a {
  top: -34vmax; left: -18vmax;
  background: radial-gradient(circle, var(--glow-a), transparent 66%);
  animation: drift-a 26s var(--ease-out) infinite alternate;
}

.ambient__blob--b {
  top: 22vmax; right: -30vmax;
  background: radial-gradient(circle, var(--glow-b), transparent 66%);
  animation: drift-b 31s var(--ease-out) infinite alternate;
}

.ambient__blob--c {
  bottom: -38vmax; left: 6vmax;
  background: radial-gradient(circle, var(--glow-c), transparent 66%);
  animation: drift-c 37s var(--ease-out) infinite alternate;
}

@keyframes drift-a { to { transform: translate3d(14vmax, 8vmax, 0) scale(1.16); } }
@keyframes drift-b { to { transform: translate3d(-12vmax, -9vmax, 0) scale(1.1); } }
@keyframes drift-c { to { transform: translate3d(9vmax, -12vmax, 0) scale(1.2); } }

/* Fractal noise as a data URI — 700 bytes, and the only thing that keeps the
   gradients above from banding. */
.ambient__grain {
  position: absolute;
  inset: -50%;
  opacity: .16;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}

.app {
  position: relative;
  z-index: 1;
  height: 100dvh;
  max-width: 480px;
  margin-inline: auto;
}

.screen {
  position: relative;
  height: 100%;
  display: grid;
  padding: max(14px, env(safe-area-inset-top)) var(--gutter) max(18px, env(safe-area-inset-bottom));
  animation: screen-in .34s var(--ease-out) both;
}

/* Screens arrive rather than appear. 8px and a fade — enough to say "this
   replaced that", short enough that a player restarting for the ninth time
   never waits for it. */
@keyframes screen-in {
  from { opacity: 0; transform: translateY(8px); }
}

:focus-visible {
  outline: 2px solid var(--accent-lit);
  outline-offset: 3px;
  border-radius: 8px;
}

/* One recipe for every raised surface. The inset highlight is the part that
   reads as an object rather than a grey box: light catching a top edge. */
.panel {
  border: 1px solid var(--line);
  border-radius: var(--r-panel);
  background: var(--felt);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .07),
    var(--shadow-lift);
}

/* ---------- Brand ---------- */

.brand { display: flex; align-items: center; gap: var(--s2); }

.brand__mark {
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  border-radius: 9px;
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 800;
  letter-spacing: -.02em;
  color: #fff;
  background: linear-gradient(150deg, var(--accent-lit), var(--accent));
  box-shadow: 0 6px 18px -8px var(--accent);
}

.brand__name {
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -.02em;
}

/* ============================== MENU ==============================
   Purpose: choose a mode and be playing one tap later.

   The four modes are the screen. Everything above them — brand, player,
   language, difficulty — is setup a player touches once a session, so it is
   compressed into two short rows rather than the six stacked, centred blocks
   that used to push Review below the fold on a 812px phone. */

/* A column rather than a grid, and for one reason: two of this screen's
   children (the player row and the notice) are hidden most of the time, so a
   fixed `grid-template-rows` hands the growing row to whatever happens to land
   in that position. In a flex column the modes block asks for the leftover
   space itself and gets it however many siblings are on screen. */
.screen--menu {
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  gap: var(--s3);
}

.menuBar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
}

.tagline {
  font-size: var(--t-meta);
  line-height: 1.5;
  color: var(--text-2);
  text-wrap: balance;
}

/* The rule of the game, stated in the two colours the game answers in. Anyone
   who reads only this line knows how to play. */
.tagline__yes { color: var(--yes); font-weight: 700; }
.tagline__no { color: var(--no); font-weight: 700; }

/* ---------- The setup strip ----------
   Language and difficulty were two centred blocks with their own headings and
   180px of vertical space between them and the game. They are settings: they
   belong on one line each, label left, control right, inside a single panel. */

.setup {
  display: grid;
  gap: var(--s2);
  padding: var(--s3) var(--s3) var(--s2);
  border: 1px solid var(--line);
  border-radius: var(--r-panel);
  background: var(--felt);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06);
}

.setup__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
}

/* The control takes the leftover width and is allowed to lose some of it: on a
   360px screen "English definitions" beside its label is wider than the row,
   and without these the segmented pill hung off the right edge. */
.setup__row .segmented { flex: 1 1 auto; min-width: 0; }
.setup__row .segmented__opt { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

.setup__label {
  flex: none;
  font-size: var(--t-label);
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* How big the loaded word bank is. It answers a question the player asks once,
   so it sits at the bottom of the strip in the quietest type on the screen. */
.bankMeta {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-3);
  min-height: 1em;
}

/* Small phones: the labels give up their tracking before the controls give up
   their words. */
@media (max-width: 380px) {
  .setup__label { font-size: var(--t-micro); letter-spacing: .08em; }
  .segmented__opt { padding: 7px 9px; font-size: 11px; }
}

/* ---------- Mode tiles ---------- */

/* The four tiles share whatever height the screen has left. On a short phone
   they compress to a comfortable 96px; on a tall one they grow rather than
   leaving 380px of empty desk under the last of them. Either way all four are
   above the fold, which is the whole point of the compression above. */
.modes {
  display: grid;
  gap: var(--s2);
  grid-auto-rows: minmax(96px, 1fr);
  flex: 1 1 auto;
  width: 100%;
  min-height: 0;
}

.mode {
  position: relative;
  display: grid;
  grid-template-columns: 42px 1fr auto;
  grid-template-areas:
    "icon name meta"
    "icon desc meta";
  align-items: center;
  /* The tile grows to share the screen; its contents do not. Without this the
     two text rows spread to the tile's full height and the name drifts away
     from the line describing it. */
  align-content: center;
  gap: 2px var(--s3);
  padding: var(--s3) var(--s4) var(--s3) var(--s3);
  overflow: hidden;
  text-align: left;
  font: inherit;
  color: var(--text-1);
  cursor: pointer;
  border: 1px solid var(--line);
  border-radius: var(--r-panel);
  background: var(--felt);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06);
  transition: transform .18s var(--ease-snap), border-color .2s ease,
              background .2s ease, box-shadow .2s ease;
  animation: tile-in .4s var(--ease-out) both;
}

/* Staggered so the menu deals itself out like a hand of cards. Four steps,
   40ms apart — under a fifth of a second for the whole set. */
.mode:nth-child(1) { animation-delay: .02s; }
.mode:nth-child(2) { animation-delay: .06s; }
.mode:nth-child(3) { animation-delay: .10s; }
.mode:nth-child(4) { animation-delay: .14s; }

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

/* The accent rail. Each mode owns a colour, and this is the only place it is
   spent at full strength — enough to tell four buttons apart at a glance
   without turning the menu into a paint chart. */
.mode::before {
  content: '';
  position: absolute;
  left: 0; top: 16%; bottom: 16%;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--mode-color);
  opacity: .85;
  transition: top .24s var(--ease-out), bottom .24s var(--ease-out), opacity .2s ease;
}

.mode__icon {
  grid-area: icon;
  display: grid;
  place-items: center;
  width: 42px; height: 42px;
  border-radius: var(--r-control);
  background: color-mix(in srgb, var(--mode-color) 15%, transparent);
  border: 1px solid color-mix(in srgb, var(--mode-color) 28%, transparent);
  color: var(--mode-color);
}

.mode__icon svg {
  width: 21px; height: 21px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}

.mode__name {
  grid-area: name;
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -.015em;
  color: var(--mode-color);
}

/* One line, clamped. The rules of a mode are worth reading once; after that
   they are four paragraphs standing between a player and the game. */
.mode__desc {
  grid-area: desc;
  font-size: var(--t-meta);
  line-height: 1.4;
  color: var(--text-2);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The number a returning player actually came for — best score, cards left,
   words waiting. Moved out from under the description to the right edge, where
   it reads as a value rather than as more prose. */
.mode__meta {
  grid-area: meta;
  justify-self: end;
  align-self: center;
  padding: 5px 10px;
  border-radius: var(--r-pill);
  font-size: var(--t-micro);
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
  background: color-mix(in srgb, var(--mode-color) 13%, transparent);
  color: color-mix(in srgb, var(--mode-color) 78%, white);
}

/* Each tile wears the signature colour of the room it opens, so the menu
   teaches the palette before the first card is dealt. Zen plays in the lobby's
   own palette, so its tile is the lobby's teal. */
.mode--zen { --mode-color: var(--c-main-success); }
.mode--time { --mode-color: var(--c-time-urgency); }
.mode--death { --mode-color: var(--c-death-strike); }
.mode--review { --mode-color: var(--c-review-calm); }

.mode:active { transform: scale(.985); }

@media (hover: hover) {
  .mode:not(:disabled):hover {
    border-color: color-mix(in srgb, var(--mode-color) 42%, transparent);
    background: color-mix(in srgb, var(--mode-color) 9%, var(--felt));
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, .1),
      0 14px 34px -18px var(--mode-color);
  }
  .mode:not(:disabled):hover::before { top: 6%; bottom: 6%; opacity: 1; }
}

.mode:disabled {
  opacity: .42;
  cursor: default;
  transform: none;
}

/* ---------- Segmented controls ---------- */

.segmented {
  display: flex;
  padding: 3px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: rgba(0, 0, 0, .3);
}

.segmented__opt {
  position: relative;
  /* Content width, shrinking only when the row runs out. `flex: 1` made every
     option the same width, so "Mixed" kept slack it did not need while the
     long one truncated beside it. */
  flex: 0 1 auto;
  padding: 7px 12px;
  border: 0;
  border-radius: var(--r-pill);
  font: inherit;
  font-size: var(--t-meta);
  font-weight: 600;
  white-space: nowrap;
  color: var(--text-2);
  background: none;
  cursor: pointer;
  touch-action: manipulation;
  transition: color .18s ease, background .18s ease;
}

/* Drawn small, touched large. The visible pill is 30px tall; this invisible
   inset carries it to the 44px minimum without moving anything on screen. */
.segmented__opt::after {
  content: '';
  position: absolute;
  inset: -7px 0;
}

.segmented__opt.is-active {
  color: #0d0f1e;
  background: linear-gradient(160deg, #ffffff, #dcdff5);
  box-shadow: 0 4px 12px -6px rgba(0, 0, 0, .8);
}

.segmented__opt.is-locked { color: var(--text-3); }

.segmented__opt.is-locked::before {
  content: '';
  display: inline-block;
  width: 9px; height: 9px;
  margin-inline-end: 5px;
  vertical-align: -1px;
  background: currentColor;
  /* A padlock, as a mask, so it inherits the button's colour in every state. */
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 10V7a5 5 0 0 1 10 0v3' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round'/%3E%3Crect x='4' y='10' width='16' height='11' rx='2.5'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 10V7a5 5 0 0 1 10 0v3' fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round'/%3E%3Crect x='4' y='10' width='16' height='11' rx='2.5'/%3E%3C/svg%3E") center / contain no-repeat;
}

@media (hover: hover) {
  .segmented__opt:not(.is-active):hover { color: var(--text-1); background: var(--panel); }
}

/* ---------- Why a band is locked ----------
   A bubble anchored under the difficulty row, with an arrow app.js positions
   over whichever button refused. */

.picker--anchored { position: relative; }

.lockTip {
  position: absolute;
  top: calc(100% + 10px);
  left: 0; right: 0;
  z-index: 5;
  padding: 13px 15px 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  background: var(--felt-lift);
  box-shadow: var(--shadow-lift);
  text-align: left;
  animation: tip-in .22s var(--ease-out);
}

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

.lockTip__arrow {
  position: absolute;
  top: -6px;
  width: 10px; height: 10px;
  translate: -50% 0;
  rotate: 45deg;
  border-left: 1px solid var(--line-strong);
  border-top: 1px solid var(--line-strong);
  background: var(--felt-lift);
}

.lockTip__title {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 700;
  color: var(--gold-lit);
}

.lockTip__body {
  margin-top: 4px;
  font-size: var(--t-meta);
  line-height: 1.5;
  color: var(--text-2);
}

.lockTip__meter { display: flex; align-items: center; gap: 9px; margin-top: 11px; }

.lockTip__track {
  flex: 1;
  height: 5px;
  border-radius: var(--r-pill);
  background: rgba(0, 0, 0, .45);
  overflow: hidden;
}

.lockTip__fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--gold), var(--gold-lit));
  transition: width .4s var(--ease-out);
}

.lockTip__count {
  font-size: var(--t-label);
  font-weight: 800;
  letter-spacing: .06em;
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}

.lockTip__foot {
  margin-top: 8px;
  font-size: var(--t-micro);
  line-height: 1.5;
  color: var(--text-3);
}

/* ---------- The player row ----------
   Name, level, progress and trophies on ONE line. It used to be a three-row
   panel; nothing in it is worth three rows on the screen where the player is
   trying to reach a mode button. */

.player {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 10px var(--s3);
}

.player__name {
  font-size: var(--t-body);
  font-weight: 700;
  max-width: 9ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.player__level {
  flex: none;
  padding: 3px 8px;
  border-radius: var(--r-pill);
  font-size: var(--t-micro);
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--accent-lit);
  background: rgba(111, 108, 247, .16);
}

.player__track {
  flex: 1;
  height: 5px;
  border-radius: var(--r-pill);
  background: rgba(0, 0, 0, .45);
  overflow: hidden;
}

.player__fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--accent), var(--accent-lit));
  transition: width .5s var(--ease-out);
}

/* The trophy count IS the leaderboard button. Two controls that go to the same
   place is one control too many. */
.linkBtn {
  display: flex;
  align-items: center;
  gap: 5px;
  flex: none;
  padding: 5px 10px;
  border: 1px solid rgba(251, 191, 36, .28);
  border-radius: var(--r-pill);
  background: rgba(251, 191, 36, .1);
  font: inherit;
  font-size: var(--t-meta);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--gold-lit);
  cursor: pointer;
  touch-action: manipulation;
  transition: background .18s ease, transform .18s var(--ease-snap);
}

.linkBtn:active { transform: scale(.94); }

.linkBtn__icon {
  width: 13px; height: 13px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}

@media (hover: hover) {
  .linkBtn:hover { background: rgba(251, 191, 36, .18); }
}

/* ---------- Why a button just said no ---------- */

.notice {
  padding: 9px 13px;
  border-radius: var(--r-control);
  border: 1px solid rgba(251, 191, 36, .25);
  background: rgba(251, 191, 36, .09);
  font-size: var(--t-meta);
  line-height: 1.5;
  color: var(--gold-lit);
  text-align: center;
}

/* ============================== GAME ==============================
   Purpose: judge cards, fast.

   The old screen had six rows and three of them were sentences telling the
   player what to do. Those are gone: the gate rails and the coach mark say it
   once, visually, and the screen is now top bar, meters, arena, gates. */

/* A column, for the same reason the menu is one: the combo bar and the clock
   are absent in three of the four modes, and a fixed `grid-template-rows`
   would hand the growing row to whichever slot they left empty. */
.screen--game {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}

.stage { flex: 1 1 auto; }

.topbar {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.iconBtn {
  position: relative;
  display: grid;
  place-items: center;
  touch-action: manipulation;
  width: 36px; height: 36px;
  flex: none;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--felt);
  color: var(--text-1);
  cursor: pointer;
  transition: background .18s ease, border-color .18s ease, transform .18s var(--ease-snap);
}

.iconBtn:active { transform: scale(.9); }

@media (hover: hover) {
  .iconBtn:hover { background: var(--felt-lift); border-color: var(--line-strong); }
}

/* 36px drawn, 44px touchable. The inset is measured from the padding box, so
   the 1px border on each side has to be paid for too: 34 + 5 + 5 = 44. */
.iconBtn::after {
  content: '';
  position: absolute;
  inset: -5px;
}

.iconBtn svg {
  width: 18px; height: 18px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}

.iconBtn--quiet { color: var(--text-3); }

/* One button, two glyphs, one of them hidden. Swapping paths in JS would mean
   the markup for an icon lived in a string in the controller. */
.iconBtn__off { display: none; }
.iconBtn.is-off .iconBtn__on { display: none; }
.iconBtn.is-off .iconBtn__off { display: block; }

.modeTag {
  flex: none;
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -.01em;
  color: var(--text-2);
  white-space: nowrap;
}

.hud {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s3);
  margin-left: auto;
  min-width: 0;
}

/* ---------- Lives ---------- */

.lives { display: flex; gap: 5px; }

.life {
  width: 11px; height: 11px;
  border-radius: 50%;
  background: var(--life);
  box-shadow: 0 0 12px -1px var(--life);
  transition: background .3s ease, transform .3s var(--ease-snap), box-shadow .3s ease;
}

.life.is-lost {
  background: var(--life-lost);
  box-shadow: none;
  transform: scale(.6);
}

.stat { display: grid; justify-items: end; gap: 0; }

.stat__value {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.05;
  transition: color .25s ease, text-shadow .25s ease;
}

.stat__value--yes { color: var(--yes-ink); }

.stat__label {
  font-size: var(--t-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--text-3);
}

/* The streak warms up as it grows. Nothing is written down about it — the
   player just notices their own number has started to glow. */
.stat--streak.is-hot .stat__value {
  color: var(--gold);
  text-shadow: 0 0 16px rgba(251, 191, 36, .7);
}

.stat--streak.is-blazing .stat__value {
  color: #ffd166;
  text-shadow: 0 0 10px rgba(255, 145, 60, .9), 0 0 26px rgba(255, 100, 40, .6);
}

.multiplier {
  padding: 3px 9px;
  border-radius: var(--r-pill);
  font-size: var(--t-meta);
  font-weight: 800;
  color: #201400;
  background: linear-gradient(140deg, var(--gold-lit), var(--gold));
  box-shadow: 0 0 20px -4px rgba(251, 191, 36, .9);
  animation: pop .34s var(--ease-snap);
}

@keyframes pop { from { transform: scale(.55); } to { transform: scale(1); } }

/* ---------- Combo bar ---------- */

.combo {
  height: 3px;
  border-radius: var(--r-pill);
  background: var(--line);
  overflow: hidden;
}

.combo__fill {
  display: block;
  width: 0;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--yes), var(--gold));
  box-shadow: 0 0 12px rgba(251, 191, 36, .8);
  transition: width .32s var(--ease-out);
}

.combo.is-max .combo__fill {
  background: linear-gradient(90deg, var(--gold), #ff7a4d);
  animation: comboPulse 1.5s ease-in-out infinite;
}

@keyframes comboPulse { 50% { opacity: .55; } }

/* ---------- Time Attack clock ----------
   Thicker than it was, and the number reads at a glance rather than as a
   caption. Under pressure the eyes are on the card; this has to be legible in
   the corner of them. */

.clock { display: flex; align-items: center; gap: 11px; }

.clock__track {
  flex: 1;
  height: 9px;
  border-radius: var(--r-pill);
  background: rgba(0, 0, 0, .45);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .5);
  overflow: hidden;
}

.clock__fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--clock-a), var(--clock-b));
  box-shadow: 0 0 14px color-mix(in srgb, var(--clock-a) 60%, transparent);
  transform-origin: left center;
  will-change: transform;
}

.clock.is-low .clock__fill {
  background: linear-gradient(90deg, var(--clock-low-a), var(--clock-low-b));
  box-shadow: 0 0 16px color-mix(in srgb, var(--clock-low-a) 80%, transparent);
  animation: comboPulse 900ms ease-in-out infinite;
}

.clock.is-low .clock__value { color: var(--clock-low-ink); }

.clock__value {
  width: 4.6ch;
  text-align: right;
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ---------- Stage and arena ---------- */

.stage {
  display: grid;
  grid-template-rows: 1fr auto;
  align-items: center;
  justify-items: center;
  min-height: 0;
}

/* The card between its two doors. The rails are laid out as real columns
   rather than absolutely positioned, so the card is centred by the grid and
   the gates cannot overlap it at any width. */
.arena {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  justify-items: center;
  gap: 10px;
  width: 100%;
  min-height: 0;
}

/* The doors. At rest they are a hint — a 6% rail you read as "there is
   something on this side". Under a drag they take their colour from --swipe,
   which is the same number that decides the answer, so the door the card is
   heading for is the door that lights. */
.gate {
  width: 4px;
  height: min(46vh, 300px);
  border-radius: var(--r-pill);
  background: linear-gradient(180deg, transparent, currentColor 22%, currentColor 78%, transparent);
  opacity: .22;
  transition: opacity .12s linear, box-shadow .18s ease;
}

.gate--no {
  color: var(--no);
  justify-self: end;
  opacity: clamp(.22, calc(var(--swipe) * -1), 1);
  box-shadow: 0 0 calc(clamp(0, calc(var(--swipe) * -1), 1) * 24px) var(--no);
}

.gate--yes {
  color: var(--yes);
  justify-self: start;
  opacity: clamp(.22, var(--swipe), 1);
  box-shadow: 0 0 calc(clamp(0, var(--swipe), 1) * 24px) var(--yes);
}

/* Both doors go quiet when the card is showing its answer: the only live
   control at that moment is Continue, and it is on the card. */
.stage.is-teaching .gate { opacity: .05; box-shadow: none; }

.deck {
  position: relative;
  width: var(--card-w);
  height: var(--card-h);
  perspective: 1200px;
}

/* While the 3-2-1 runs, the cards are there to be read and not to be thrown. */
.deck.is-frozen { pointer-events: none; }

/* One line, and only after an answer. It used to be permanent and to sit
   between two other permanent lines saying the same thing. */
.feedback {
  min-height: 18px;
  padding-top: 10px;
  font-size: var(--t-meta);
  font-weight: 600;
  color: var(--text-3);
  text-align: center;
  transition: color .2s ease, opacity .2s ease;
}

.feedback.is-yes { color: var(--yes-ink); }
.feedback.is-no { color: var(--no-ink); }

/* ---------- The one-time coach ----------
   Shown on the first card of a player's first run and never again. It is the
   replacement for three permanent lines of instruction: it demonstrates the
   gesture instead of describing it, then leaves. */

.coach {
  position: absolute;
  inset: 0;
  z-index: 40;
  display: grid;
  place-content: center;
  gap: 14px;
  justify-items: center;
  padding: var(--gutter);
  background: rgba(7, 8, 15, .82);
  animation: screen-in .3s var(--ease-out) both;
}

.coach__demo {
  display: flex;
  align-items: center;
  gap: 18px;
}

.coach__card {
  width: 74px; height: 100px;
  border-radius: 12px;
  background: linear-gradient(168deg, var(--surface), var(--surface-edge));
  box-shadow: var(--shadow-card);
  animation: coach-swing 2.6s var(--ease-out) infinite;
}

@keyframes coach-swing {
  0%, 12%   { transform: translateX(0) rotate(0); }
  30%       { transform: translateX(34px) rotate(9deg); }
  48%, 52%  { transform: translateX(0) rotate(0); }
  70%       { transform: translateX(-34px) rotate(-9deg); }
  88%, 100% { transform: translateX(0) rotate(0); }
}

.coach__side {
  display: grid;
  justify-items: center;
  gap: 6px;
  font-size: var(--t-label);
  font-weight: 800;
  letter-spacing: .12em;
  text-transform: uppercase;
}

.coach__side--no { color: var(--no); }
.coach__side--yes { color: var(--yes); }

.coach__side svg {
  width: 22px; height: 22px;
  fill: none; stroke: currentColor; stroke-width: 2.4;
  stroke-linecap: round; stroke-linejoin: round;
}

.coach__line {
  max-width: 26ch;
  text-align: center;
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--text-2);
  text-wrap: balance;
}

/* ---------- Confirm dialog ----------
   The coach's scrim with a panel on it rather than a second overlay recipe.
   One way of covering the game screen, used by both things that cover it. */

.modal {
  position: absolute;
  inset: 0;
  z-index: 45;
  display: grid;
  place-content: center;
  padding: var(--gutter);
  background: rgba(7, 8, 15, .82);
  animation: screen-in .22s var(--ease-out) both;
}

.modal__panel {
  display: grid;
  gap: var(--s3);
  width: min(88vw, 340px);
  padding: var(--s5) var(--s4) var(--s4);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-panel);
  background: var(--felt-lift);
  box-shadow: var(--shadow-lift);
  text-align: center;
}

.modal__title {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -.02em;
}

.modal__body {
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--text-2);
  text-wrap: balance;
}

/* Staying is the safe answer, so it is the filled one. Leaving keeps a button
   of its own rather than hiding behind the scrim — a dialog whose destructive
   option is hard to find is a dialog that gets dismissed by accident. */
.modal__actions { display: grid; gap: var(--s2); }

.btn--danger {
  color: var(--no-ink);
  border-color: rgba(255, 90, 120, .32);
}

@media (hover: hover) {
  .btn--danger:hover { background: rgba(255, 90, 120, .1); border-color: rgba(255, 90, 120, .5); }
}

/* ---------- Card ---------- */

.card {
  position: absolute;
  inset: 0;
  will-change: transform, opacity;
  pointer-events: none;
  /* Centre pivot. The release rolls the card through one to two full turns,
     and a pivot below the card would swing it through a huge arc instead of
     rolling it about itself. The origin cannot change mid-flight either —
     switching it while a rotation is applied jumps the card sideways. */
  transform-origin: 50% 50%;
  /* Applies to .card__inner below, so the flip has depth of its own. */
  perspective: 1000px;
}

.card.is-top { pointer-events: auto; touch-action: none; cursor: grab; }
.card.is-dragging { cursor: grabbing; }
.card.is-revealed { cursor: default; }

.card__inner {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transition: transform 560ms cubic-bezier(.2, .7, .3, 1);
}

.card.is-revealed .card__inner { transform: rotateY(180deg); }

.card__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  padding: 24px 22px;
  border-radius: var(--r-card);
  color: var(--ink);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}

/* Not flat white. A very slightly cool paper with a warm lift at the top left
   is what reads as a physical card under the room's light rather than a div. */
/* The block sits in the optical centre of the card. The slack a short answer
   leaves over is split above and below it, which reads as a card's margins;
   pinned to the top it read as a hole underneath. */
.card__face--front { justify-content: center; }

.card__face--front {
  background:
    radial-gradient(120% 80% at 12% 0%, var(--surface) 0%, transparent 55%),
    linear-gradient(168deg, var(--surface) 0%, var(--surface-edge) 100%);
}

.card__face--back {
  transform: rotateY(180deg);
  background:
    radial-gradient(120% 80% at 12% 0%, #fffdf8 0%, transparent 55%),
    linear-gradient(168deg, #fffaf0 0%, #f2e9d9 100%);
}

.card__kicker {
  font-size: var(--t-micro);
  font-weight: 800;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--accent);
}

.card__kicker--miss { color: #cf6a20; }

/* The word is the reason the app exists, so it gets the display face at its
   most characterful and nothing else on the card comes near its weight. */
.card__word {
  font-family: var(--font-display);
  font-size: var(--t-hero);
  font-weight: 700;
  font-variation-settings: 'SOFT' 20, 'WONK' 1, 'opsz' 120;
  letter-spacing: -.025em;
  line-height: 1.05;
  word-break: break-word;
}

.card__word--back { font-size: clamp(25px, 7.2vw, 32px); margin-bottom: 12px; }

/* ---------- The sentence on the front ----------
   The context that says WHICH sense is being asked about. Quieter than the
   word above it and quieter than the answer below it: it is evidence, not the
   question and not the claim. The one bold run in it is the word being taught,
   which is the whole reason the line is there. */
.card__usage {
  margin-top: 12px;
  font-size: 15px;
  line-height: 1.5;
  font-style: italic;
  color: var(--ink-soft);
}

.card__usage b {
  font-style: normal;
  font-weight: 700;
  color: var(--ink);
  /* A rule under the word rather than a highlight behind it: the card is a
     printed thing, and a marker pen on it would be the only element in the
     interface pretending to be a different medium. */
  box-shadow: inset 0 -2px 0 rgba(111, 108, 247, .35);
}

.card__phonetic {
  margin-top: 6px;
  font-size: var(--t-body);
  color: var(--ink-faint);
  font-style: italic;
  letter-spacing: .01em;
}

/* ---------- The comparison ----------
   THE change on this screen. The word and the meaning are the two things the
   player is asked to weigh against each other, and they used to sit at
   opposite ends of the card with 350px of blank paper between them — the eye
   could not hold both at once, which is the entire task.

   They are now one block: word, hairline, meaning, tight, sharing a left
   axis. Whatever slack the box has left over falls below them, where empty
   paper reads as a card rather than as a mistake. */

.card__divider {
  height: 1px;
  margin: 16px 0 14px;
  background: linear-gradient(90deg, rgba(22, 24, 44, .22), transparent 70%);
}

.card__label {
  font-size: var(--t-micro);
  font-weight: 800;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 7px;
}

.card__face--back .card__label { margin-top: 13px; }
.card__face--back .card__label:first-of-type { margin-top: 0; }

.card__meaning,
.card__definition {
  font-size: var(--t-lead);
  line-height: 1.5;
  color: var(--ink-soft);
  /* `start` rather than `left`: the meaning carries its own dir attribute, and
     a Hebrew or Arabic line has to sit against the correct edge of the card. */
  text-align: start;
  unicode-bidi: isolate;
}

/* A translation is the answer, not a description — give it the weight of one,
   and a face that was drawn for the script it is written in. */
.card__meaning[dir="rtl"] {
  font-family: var(--font-he);
  font-size: 28px;
  font-weight: 600;
  line-height: 1.4;
}

.card__meaning--truth { color: var(--ink); font-weight: 600; }

.card__gloss { display: contents; }

.card__example {
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink-soft);
  font-style: italic;
}

/* Continue is the only thing to do here, so it is the only thing that looks
   like it. "Hear it" becomes a quiet square beside it rather than an equal
   half of the row. */
.card__actions {
  margin-top: auto;
  padding-top: 16px;
  display: flex;
  gap: var(--s2);
}

/* Stamps that fade in while dragging.
   will-change keeps the fade on the compositor: without it every frame of the
   drag repaints the stamp and its border into the card's texture. */
.card__stamp {
  position: absolute;
  will-change: opacity;
  top: 22px;
  padding: 7px 15px;
  border: 3px solid currentColor;
  border-radius: 12px;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 900;
  letter-spacing: .07em;
  opacity: 0;
  pointer-events: none;
}

.card__stamp--yes { right: 18px; color: var(--yes-deep); transform: rotate(14deg); }
.card__stamp--no  { left: 18px;  color: var(--no-deep);  transform: rotate(-14deg); }

/* Tint overlay driven by drag progress */
.card__tint {
  position: absolute;
  inset: 0;
  opacity: 0;
  will-change: opacity;
  pointer-events: none;
  border-radius: inherit;
}

/* The bright pair, washed in from the edge the card is heading for. They come
   from the theme, so the same drag tints teal in the lobby, calm mint in Review
   and warning red in Sudden Death. The stamps above print the -deep shades,
   which are the ones that read as text on white. */
.card__tint--yes { background: linear-gradient(250deg, color-mix(in srgb, var(--yes) 42%, transparent), transparent 66%); }
.card__tint--no  { background: linear-gradient(110deg, color-mix(in srgb, var(--no) 38%, transparent), transparent 66%); }

/* ---------- Buttons ---------- */

.btn {
  font: inherit;
  border: 0;
  cursor: pointer;
  color: var(--text-1);
  background: none;
}

.controls {
  display: flex;
  justify-content: center;
  gap: var(--s5);
  padding: var(--s1) 0 0;
}

/* ---------- The gates ----------
   The two most-pressed controls in the game used to be dark grey discs — the
   weakest-looking elements on a screen whose entire purpose is pressing them.
   They now wear the colour they mean: a filled ground, a full-strength ring
   and a heavy glyph, sized 72px so a thumb finds them without looking. */

.btn--gate {
  position: relative;
  display: grid;
  touch-action: manipulation;
  place-items: center;
  width: 72px; height: 72px;
  border-radius: 50%;
  color: var(--gate-ink, var(--gate-color));
  background:
    radial-gradient(120% 120% at 50% 0%, color-mix(in srgb, var(--gate-color) 22%, transparent), transparent 70%),
    var(--felt);
  border: 2px solid color-mix(in srgb, var(--gate-color) 55%, transparent);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .1),
    0 0 24px -12px var(--gate-color),
    0 10px 24px -14px #000;
  /* No backdrop-filter here: cards fly across these buttons, and a live blur
     forces the area behind them to re-rasterise on every frame of that pass. */
  transition: transform .16s var(--ease-snap), background .18s ease,
              border-color .18s ease, box-shadow .18s ease;
}

.btn--gate svg {
  width: 30px; height: 30px;
  fill: none;
  stroke: currentColor;
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The ring and glow take the bright colour; the glyph takes the -ink one, which
   is the same colour in the dark rooms and a deeper shade in Review's pale one. */
.btn--no  { --gate-color: var(--no);  --gate-ink: var(--no-ink); }
.btn--yes { --gate-color: var(--yes); --gate-ink: var(--yes-ink); }

/* Presses sink rather than shrink. A button that gets smaller reads as
   retreating; one that gets darker and settles reads as taking the press. */
.btn--gate:active {
  transform: translateY(2px) scale(.96);
  background:
    radial-gradient(120% 120% at 50% 0%, color-mix(in srgb, var(--gate-color) 38%, transparent), transparent 70%),
    var(--felt);
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, .5), 0 0 30px -10px var(--gate-color);
}

@media (hover: hover) {
  .btn--gate:not(:disabled):hover {
    border-color: var(--gate-color);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, .12),
      0 0 34px -10px var(--gate-color),
      0 10px 24px -14px #000;
  }
}

.btn--gate:disabled { opacity: .25; cursor: default; transform: none; }

/* On the card's back face — dark text on a light ground, unlike the rest. */
.btn--speak, .btn--continue {
  padding: 13px 16px;
  border-radius: var(--r-control);
  font-size: var(--t-body);
  font-weight: 700;
  transition: background .18s ease, transform .18s var(--ease-snap);
}

.btn--speak {
  flex: none;
  display: grid;
  place-items: center;
  width: 48px;
  color: var(--ink);
  border: 1px solid rgba(22, 24, 44, .18);
  background: rgba(22, 24, 44, .05);
}

.btn--speak svg {
  width: 19px; height: 19px;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}

.btn--continue {
  flex: 1;
  color: #fff;
  background: linear-gradient(140deg, #2a2c4d, var(--ink));
  box-shadow: 0 8px 20px -12px #000;
}

.btn--speak:active, .btn--continue:active { transform: scale(.96); }

/* No speech synthesis in this browser — don't offer a button that does nothing. */
.no-tts .btn--speak { display: none; }

.btn--primary {
  padding: 15px 28px;
  border-radius: var(--r-pill);
  font-size: 15px;
  font-weight: 700;
  color: var(--primary-ink);
  background: linear-gradient(160deg, var(--primary-a), var(--primary-b));
  box-shadow: 0 10px 26px -12px var(--primary-glow);
  transition: transform .18s var(--ease-snap), box-shadow .18s ease;
}

.btn--primary:active { transform: scale(.97); }

.btn--ghost {
  padding: 15px 22px;
  border-radius: var(--r-pill);
  border: 1px solid var(--line);
  background: var(--felt);
  font-size: var(--t-body);
  font-weight: 600;
  transition: background .18s ease, border-color .18s ease, transform .18s var(--ease-snap);
}

.btn--ghost:active { transform: scale(.97); }

@media (hover: hover) {
  .btn--ghost:hover { background: var(--felt-lift); border-color: var(--line-strong); }
}

/* ======================= THE PRAISE BURST =======================
   The loudest thing in the game, on screen for under a second.

   Fixed to the viewport, and lifted OFF the card's centre. Dead centre is
   where the word being learned is printed, and a burst there prints across the
   one thing the player is trying to read. It now lands over the card's upper
   edge, clear of the word and clear of the meaning under it.

   Everything rests at opacity 0. Nothing here is visible unless it is playing.
   ================================================================ */

.burst {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* Up and out of the card's text. The card is centred in the stage, so the
     burst is pushed into the band above its top edge: the padding takes 46vh
     off the bottom of the flex box, which leaves its centre at 27% of the
     viewport. --burst-y carries that same number to the ring and the flare
     below, because a word in one place and its halo in another is worse than
     either of them being in the wrong place. */
  padding-bottom: 46vh;
  --burst-y: 27%;
  gap: var(--s2);
  /* Cards are thrown across this exact rectangle. Load-bearing. */
  pointer-events: none;

  /* praise.js writes this per burst: 0 for a shrug, 1 for a milestone. */
  --heat: .5;
}

/* Built from the theme's answer colours, so the burst agrees with the tint the
   card showed a moment earlier. */
.burst[data-kind="yes"] {
  --burst-a: color-mix(in srgb, var(--yes) 72%, white);
  --burst-b: var(--yes);
  --burst-glow: color-mix(in srgb, var(--yes) 50%, transparent);
}

.burst[data-kind="no"] {
  --burst-a: color-mix(in srgb, var(--no) 72%, white);
  --burst-b: var(--no);
  --burst-glow: color-mix(in srgb, var(--no) 45%, transparent);
}

.burst[data-kind="gold"] {
  --burst-a: var(--gold-lit);
  --burst-b: #ff9e3d;
  --burst-glow: rgba(251, 191, 36, .55);
}

/* The word. Display face, heaviest weight, italic — the one piece of type in
   the whole interface allowed to shout. The size rides `--heat`, so a streak of
   sixteen is visibly a bigger event than a streak of one without any extra copy
   having to say so. */
.burst__word {
  font-family: var(--font-display);
  font-size: clamp(30px, calc(7.5vw + var(--heat) * 5vw), 66px);
  font-weight: 900;
  font-style: italic;
  font-variation-settings: 'SOFT' 0, 'WONK' 1, 'opsz' 144;
  letter-spacing: -.035em;
  line-height: .96;
  text-align: center;
  text-wrap: balance;
  max-width: 92vw;
  opacity: 0;

  background: linear-gradient(158deg, var(--burst-a) 0%, var(--burst-b) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* drop-shadow rather than text-shadow: the glyphs are painted by a clipped
     background, and text-shadow would draw the shadow of transparent text.
     The tight black one is not decoration — it is the last line of contrast
     where a mint word crosses the white card underneath. */
  filter:
    drop-shadow(0 0 26px var(--burst-glow))
    drop-shadow(0 2px 3px rgba(0, 0, 0, .5))
    drop-shadow(0 6px 16px rgba(0, 0, 0, .55));
}

/* Not uppercased, unlike every other label in the interface. The detail line
   carries "+2s", and `text-transform` would render that as "+2S" — a unit that
   means something else. Tracking and weight carry the same emphasis without
   rewriting the text. */
.burst__sub {
  font-size: 13.5px;
  font-weight: 700;
  letter-spacing: .07em;
  color: var(--burst-a);
  text-shadow: 0 2px 10px rgba(0, 0, 0, .6);
  opacity: 0;
}

.burst__flare,
.burst__ring,
.burst__shards {
  position: absolute;
  top: var(--burst-y); left: 50%;
  translate: -50% -50%;
  opacity: 0;
}

/* Two gradients doing two jobs. The colour is the celebration; the dark core
   under it is what makes the celebration readable — the card behind this is a
   near-white rectangle, and a mint word laid straight over it is a mint word on
   white. Dimming the stage for six hundred milliseconds is also just what a
   spotlight does, so the fix and the drama are the same thing. */
.burst__flare {
  width: min(150vw, 720px);
  aspect-ratio: 1;
  border-radius: 50%;
  background:
    radial-gradient(circle, rgba(6, 7, 14, .8) 0%, rgba(6, 7, 14, .46) 30%, transparent 56%),
    radial-gradient(circle, var(--burst-glow) 0%, transparent 46%);
}

.burst__ring {
  width: min(56vw, 250px);
  aspect-ratio: 1;
  border: 2.5px solid var(--burst-a);
  border-radius: 50%;
}

/* A zero-sized anchor at dead centre; every shard is positioned off it. */
.burst__shards { width: 0; height: 0; opacity: 1; }

.burst__shard {
  position: absolute;
  top: -4px; left: -4px;
  width: 8px; height: 8px;
  border-radius: 2px;
  background: linear-gradient(140deg, var(--burst-a), var(--burst-b));
  box-shadow: 0 0 10px var(--burst-glow);
  opacity: 0;
}

/* A wash of colour in from the edges. Under the burst and over everything else,
   and clear of the middle of the screen so it never fights the word. */
.flash {
  position: fixed;
  inset: 0;
  z-index: 55;
  pointer-events: none;
  opacity: 0;
}

.flash[data-kind="yes"] {
  background: radial-gradient(125% 85% at 50% 50%, transparent 46%, color-mix(in srgb, var(--yes) 26%, transparent) 100%);
}

.flash[data-kind="no"] {
  background: radial-gradient(125% 85% at 50% 50%, transparent 42%, color-mix(in srgb, var(--no) 32%, transparent) 100%);
}

/* ============================ SUMMARY ============================= */

/* Same trap as .screen--menu: a long "words to review" list pushes the score
   off the top. */
.screen--summary {
  align-content: center;
  align-content: safe center;
  overflow-y: auto;
  justify-items: center;
  gap: var(--s3);
  text-align: center;
}

.summary__kicker {
  font-size: var(--t-label);
  font-weight: 800;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--text-3);
}

.summary__title {
  font-family: var(--font-display);
  font-size: var(--t-display);
  font-weight: 700;
  letter-spacing: -.02em;
}

.summary__headline { display: flex; align-items: baseline; gap: 9px; margin-top: 2px; }

.summary__score {
  font-family: var(--font-display);
  font-size: 62px;
  font-weight: 900;
  line-height: .95;
  letter-spacing: -.04em;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(150deg, #fff 10%, var(--accent-lit) 60%, var(--cyan) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 8px 26px rgba(111, 108, 247, .45));
}

.summary__scoreLabel { font-size: var(--t-meta); font-weight: 600; color: var(--text-3); }

.summary__best {
  padding: 5px 13px;
  border-radius: var(--r-pill);
  font-size: var(--t-meta);
  font-weight: 800;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: #201400;
  background: linear-gradient(140deg, var(--gold-lit), var(--gold));
  box-shadow: 0 0 24px -6px rgba(251, 191, 36, .9);
}

.summary__trophies {
  font-size: var(--t-body);
  font-weight: 700;
  color: var(--gold-lit);
}

/* Accuracy is the stat that says whether the run went well — a score depends
   on how long you played, accuracy does not. It gets the full width and the
   display face; the other three share the row beneath it. */
.summary__grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s2);
  width: 100%;
}

.summary__cell {
  padding: 11px 8px;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  background: var(--felt);
}

.summary__cell dt {
  font-size: var(--t-micro);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-3);
}

.summary__cell dd {
  margin-top: 3px;
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.summary__cell--hero {
  grid-column: 1 / -1;
  border-color: rgba(111, 108, 247, .3);
  background: linear-gradient(160deg, rgba(111, 108, 247, .14), var(--felt));
}

.summary__cell--hero dd { font-size: 34px; }

.summary__review {
  width: 100%;
  text-align: left;
  display: grid;
  gap: var(--s2);
}

.summary__reviewTitle {
  font-size: var(--t-label);
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-3);
}

.summary__reviewList { list-style: none; display: grid; gap: 5px; }

.summary__reviewItem {
  padding: 9px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  background: var(--felt);
  font-size: var(--t-body);
  color: var(--text-2);
}

.summary__reviewItem b { color: var(--text-1); font-weight: 700; }

.summary__actions { display: grid; gap: var(--s2); width: 100%; margin-top: var(--s1); }

/* Two buttons on one row, the third — Review — full width above them when
   there is something to review. It is the most valuable thing a player can do
   after a bad run, and it used to be a mode button two screens away. */
.summary__row { display: flex; gap: var(--s2); }
.summary__row .btn { flex: 1; }

.btn--review {
  width: 100%;
  padding: 15px 20px;
  border-radius: var(--r-pill);
  border: 1px solid rgba(155, 152, 255, .4);
  background: linear-gradient(150deg, rgba(155, 152, 255, .22), rgba(111, 108, 247, .12));
  font-size: 15px;
  font-weight: 700;
  color: #cfceff;
  transition: transform .18s var(--ease-snap), background .18s ease;
}

.btn--review:active { transform: scale(.97); }

@media (hover: hover) {
  .btn--review:hover { background: linear-gradient(150deg, rgba(155, 152, 255, .3), rgba(111, 108, 247, .18)); }
}

/* ============================== AUTH ============================== */

.screen--auth {
  align-content: center;
  align-content: safe center;
  overflow-y: auto;
  justify-items: center;
  gap: var(--s5);
}

.auth__head { display: grid; justify-items: center; gap: var(--s3); text-align: center; }

.auth {
  display: grid;
  gap: var(--s3);
  width: 100%;
  padding: var(--s5) var(--s4);
}

.field { display: grid; gap: 6px; }

.field__label {
  font-size: var(--t-micro);
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-3);
}

.field__input {
  width: 100%;
  padding: 13px 15px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  background: rgba(0, 0, 0, .32);
  font: inherit;
  font-size: 15px;
  color: var(--text-1);
  transition: border-color .18s ease, background .18s ease;
}

.field__input::placeholder { color: var(--text-3); }

.field__input:focus {
  outline: none;
  border-color: var(--accent-lit);
  background: rgba(0, 0, 0, .45);
}

.auth__error {
  padding: 9px 12px;
  border-radius: var(--r-control);
  background: rgba(255, 90, 120, .12);
  border: 1px solid rgba(255, 90, 120, .3);
  font-size: var(--t-meta);
  color: #ffb3c1;
  text-align: center;
}

/* =========================== LEADERBOARD ========================== */

.screen--board {
  grid-template-rows: auto 1fr auto;
  gap: var(--s4);
  align-content: start;
}

.board__head { display: flex; align-items: center; gap: var(--s3); }

.board__title {
  font-family: var(--font-display);
  font-size: 21px;
  font-weight: 700;
  letter-spacing: -.02em;
}

.board__count { margin-inline-start: auto; font-size: var(--t-meta); font-weight: 600; color: var(--text-3); }

.board__list { display: grid; gap: 6px; list-style: none; align-content: start; overflow-y: auto; }

.board__row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 11px 13px;
  border: 1px solid var(--line);
  border-radius: var(--r-control);
  background: var(--felt);
}

/* The player's own row, wherever it lands. Finding yourself in a list is the
   only reason most people open a leaderboard. */
.board__row.is-you {
  border-color: rgba(111, 108, 247, .45);
  background: linear-gradient(150deg, rgba(111, 108, 247, .16), var(--felt));
}

.board__rank {
  width: 2.2ch;
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 700;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

/* Gold, silver, bronze — the only place in the interface those three read as
   ranks rather than as decoration. */
.board__row:nth-child(1) .board__rank { color: var(--gold); }
.board__row:nth-child(2) .board__rank { color: #d6dae8; }
.board__row:nth-child(3) .board__rank { color: #d59b6a; }

.board__name { font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board__level { font-size: var(--t-micro); font-weight: 700; color: var(--text-3); }

.board__score {
  margin-inline-start: auto;
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 700;
  color: var(--gold-lit);
  font-variant-numeric: tabular-nums;
}

.board__empty,
.board__you {
  text-align: center;
  font-size: var(--t-body);
  color: var(--text-3);
  padding: var(--s5) 0;
}

/* ---------- Motion preferences ----------
   Everything above is either decoration or a signal that has a still form.
   This turns off the first kind and freezes the second. */

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

  .ambient__blob { animation: none; }
  .coach__card { animation: none; }
}
