/* gems — phone first, because an 8×8 board of things you have to swipe between
 * is only difficult on a phone.
 *
 * TWO THEMES, AND LIGHT IS THE BASE, matching budget, pattern, mines and words.
 * The palette below is the light one on bare :root; dark is a token swap in two
 * guarded blocks — the media query for "system", the attribute for a choice the
 * user made. Nothing outside those blocks names a literal colour, so a rule
 * written once is right in both. The duplication between the two dark blocks is
 * unavoidable: a media query cannot be OR-ed with an attribute selector, and
 * defining a colour in only one of them is how a theme ends up half-applied.
 *
 * THE JEWELS ARE THE ONE EXCEPTION AND IT IS DELIBERATE. Their colours live in
 * gems.js, not here, and they do not change with the theme. A ruby has to be the
 * same red in both, or a player who switches to dark mode is learning the board
 * again — and worse, a screenshot of a game would not match the game. The board
 * around them changes; the stones do not.
 *
 * Insets go on PADDING, never on layout, and every one carries a 0px fallback —
 * `calc(6px + env(...))` with no fallback is an invalid length and drops the
 * whole declaration on every desktop browser.
 */

:root {
  /* iOS system greys: a grouped background with white cards sitting on it. */
  --bg:       #dfe4ec;       /* THE GROUND IS A COOL SLATE, and everything on it is glass — conquest's arrangement (2026-09-14; the flat greys with white cards were the original's) */
  --panel:    #ffffff;
  --panel-2:  #f6f6f9;
  --glass:    rgba(255, 255, 255, .78);
  --glass-2:  rgba(255, 255, 255, .55);
  --gold:     #d9a520;       /* GOLD MEANS YOU — conquest's and wizard's rule: the primary button, the supporter's mark, the hint ring */
  --gold-lit: #ffd45c;
  --line:     rgba(60, 60, 67, .16);
  --line-2:   rgba(60, 60, 67, .08);
  --ink:      #000000;
  --ink-2:    #3c3c43;
  --dim:      #8a8a8e;
  --dim-2:    #b0b0b6;
  --pill:     #efeff0;
  --good:     #1a9e4b;
  --warn:     #b8730a;
  --bad:      #e5322b;
  --link:     #007aff;
  --shadow:   0 1px 2px rgba(0,0,0,.04), 0 6px 20px rgba(0,0,0,.05);

  /* The board: a recessed slate the jewels sit in. */
  --felt:      #e7e7ee;
  --felt-2:    #dcdce6;
  --socket:    rgba(60,60,67,.07);
  --select:    var(--gold);   /* GOLD MEANS YOU: the jewel in your hand */

  --r-card: 18px;
  --r-mid:  14px;
  --r-sm:   10px;
  --r-cell: 10px;

  --font: -apple-system, BlinkMacSystemFont, system-ui, "SF Pro Text",
          "Helvetica Neue", sans-serif;
  /* The score and the move count only. A proportional font makes a running
     number jitter sideways on every change, which is distracting in exactly the
     moment the number matters. */
  --font-num: ui-rounded, -apple-system, "SF Pro Rounded", system-ui, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;

  /* One place decides how fast the board moves. app.js reads these back out of
     the computed style so the JavaScript timings and the CSS transitions cannot
     drift apart — a fall that finishes 40ms after its animation is a gem that
     visibly teleports the last few pixels. */
  --t-swap: 170ms;
  --t-pop:  200ms;
  --t-fall: 240ms;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:       #121a24;
    --panel:    #1c1c1e;
    --glass:    rgba(28, 28, 30, .86);
    --glass-2:  rgba(44, 44, 46, .75);
    --gold:     #e8c05a;
    --gold-lit: #ffd77a;
    --panel-2:  #2c2c2e;
    --line:     rgba(84, 84, 88, .5);
    --line-2:   rgba(84, 84, 88, .28);
    --ink:      #ffffff;
    --ink-2:    #ebebf5;
    --dim:      #8e8e93;
    --dim-2:    #636366;
    --pill:     #2c2c2e;
    --good:     #30d158;
    --warn:     #ffd60a;
    --bad:      #ff453a;
    --link:     #0a84ff;
    --shadow:   0 1px 2px rgba(0,0,0,.5), 0 6px 20px rgba(0,0,0,.4);

    --felt:      #16161a;
    --felt-2:    #202026;
    --socket:    rgba(255,255,255,.05);
    --select:    var(--gold);
  }
}
:root[data-theme="dark"] {
  --bg:       #121a24;
  --panel:    #1c1c1e;
  --glass:    rgba(28, 28, 30, .86);
  --glass-2:  rgba(44, 44, 46, .75);
  --gold:     #e8c05a;
  --gold-lit: #ffd77a;
  --panel-2:  #2c2c2e;
  --line:     rgba(84, 84, 88, .5);
  --line-2:   rgba(84, 84, 88, .28);
  --ink:      #ffffff;
  --ink-2:    #ebebf5;
  --dim:      #8e8e93;
  --dim-2:    #636366;
  --pill:     #2c2c2e;
  --good:     #30d158;
  --warn:     #ffd60a;
  --bad:      #ff453a;
  --link:     #0a84ff;
  --shadow:   0 1px 2px rgba(0,0,0,.5), 0 6px 20px rgba(0,0,0,.4);

  --felt:      #16161a;
  --felt-2:    #202026;
  --socket:    rgba(255,255,255,.05);
  --select:    var(--gold);
}

* { box-sizing: border-box; }

/* `[hidden]` MUST WIN, AND BY DEFAULT IT DOES NOT. The browser's own
 * `[hidden] { display: none }` lives in the UA stylesheet, so any author rule
 * setting `display` on the same element beats it outright — no `!important`,
 * no specificity war, just author-over-UA. In `mines` both
 * `.controls { display: flex }` and `.btn-wide { display: block }` did, so
 * `el.hidden = true` did nothing at all and the element sat there in plain
 * sight while the PROPERTY read true — which is why the check that asserted on
 * `el.hidden` agreed with the code and disagreed with the screen. The
 * `.something[hidden]` patches further down this file are each one day
 * somebody hit it without naming it. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  min-height: 100%;
  font-family: var(--font);
  color: var(--ink);
  background-color: var(--bg);
  -webkit-font-smoothing: antialiased;
}

/* The whole app refuses text selection and the iOS long-press callout: both
   interrupt a swipe, and the callout puts a magnifier over the board. */
body {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
input, textarea { -webkit-user-select: text; user-select: text; }

/* ── the glass at the top of the screen ────────────────────────────────────

   iOS 26 blurs the top edge of a full-screen web app against the status bar:
   content does not stop under the bar, it goes SOFT beneath it, and anything in
   that band is unreadable rather than hidden.

   `env(safe-area-inset-top)` does not describe it. That inset is the status
   bar's own height — it is what keeps content out from UNDER the bar, and it is
   already applied everywhere below. The blur reaches past it, so the two are
   different measurements and both are needed. Adding them is the same fix
   `budget` and `mines` made, and 20px is the same number.

   It costs a browser tab nothing — 20px of background at the top of a scrolling
   page reads as margin — so it is not gated on display-mode. A media query that
   silently fails to match is worse than a margin nobody notices. */
:root { --glass-t: 20px; }

/* ── shell ─────────────────────────────────────────────────────────────── */

main { padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }

/* THE GAME SCREEN IS THE SCREEN (2026-09-15). While the game is up the
   document cannot scroll or rubber-band and nothing on the view pans: it is
   exactly the viewport tall, there is nothing below it to reach, and a
   board you are trying not to mis-swipe must not move under a finger. The
   home scrolls (five cards); the sheet scrolls inside itself. */
html.in-game, html.in-game body { overflow: hidden; height: 100%; overscroll-behavior: none; }
html.in-game main { padding-bottom: 0; }

.view {
  padding: calc(8px + var(--glass-t) + env(safe-area-inset-top, 0px)) 16px 24px;
  max-width: 560px;
  margin: 0 auto;
}
.view[hidden] { display: none; }

.view-head { padding: 12px 4px 18px; }
/* THE BAR — conquest's lobby header: the title on the left, one round glass
   menu button on the right, and nothing else on the screen to navigate by. */
.view-head.bar { display: flex; align-items: center; gap: 10px; }
.bar-title { flex: 1; min-width: 0; }
.icon-btn.glass {
  background: var(--glass); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  border: .5px solid var(--line); box-shadow: var(--shadow);
}
.icon-btn svg { display: block; }
.view-head h1 { margin: 0; font-size: 34px; font-weight: 700; letter-spacing: -.6px; }
.view-sub { margin: 4px 0 0; color: var(--dim); font-size: 15px; }

h2.sec {
  margin: 22px 4px 10px;
  font-size: 13px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .06em; color: var(--dim);
}

/* THE GLASS. One recipe for every panel in the app — translucent, blurred,
   a hairline edge, a soft shadow — conquest's, verbatim, so the games read
   as one house. */
.card, .mode-card, .chain, .overlay-card, .gate-card, .icon-btn, .seg {
  background: var(--glass); backdrop-filter: blur(12px) saturate(140%); -webkit-backdrop-filter: blur(12px) saturate(140%);
  border: .5px solid var(--line); box-shadow: var(--shadow);
}
.card {
  border-radius: var(--r-card);
  overflow: hidden;
}

.fineprint {
  margin: 14px 6px 0;
  color: var(--dim);
  font-size: 12.5px;
  line-height: 1.55;
}
.fineprint strong { color: var(--ink-2); font-weight: 600; }

/* ── buttons ───────────────────────────────────────────────────────────── */

/* Conquest's buttons: glass with a hairline, 14px corners; the one thing to
   do next is GOLD (their "gold means you"), and a quiet button is a link. */
.btn {
  appearance: none; font: inherit; font-size: 16px; font-weight: 600;
  padding: 12px 18px;
  border: .5px solid var(--line); border-radius: var(--r-mid);
  background: var(--glass-2); color: var(--ink);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  box-shadow: var(--shadow);
  cursor: pointer;
}
.btn:active { opacity: .7; }
.btn-primary { background: var(--gold); color: #1a1200; border-color: transparent; }
.btn-danger  { background: transparent; border-color: transparent; box-shadow: none; backdrop-filter: none; -webkit-backdrop-filter: none; color: var(--bad); }
.btn-quiet   { background: transparent; border-color: transparent; box-shadow: none; backdrop-filter: none; -webkit-backdrop-filter: none; color: var(--link); }
.btn-wide    { display: block; width: 100%; margin-top: 18px; padding: 14px; font-size: 17px; }
.btn:disabled { opacity: .4; }

.icon-btn {
  appearance: none; font: inherit; font-size: 20px; line-height: 1;
  width: 40px; height: 40px; padding: 0;
  display: grid; place-items: center;
  border-radius: 20px; color: var(--ink);
  cursor: pointer;
}
.icon-btn:active { opacity: .7; }

/* ── the gate ──────────────────────────────────────────────────────────── */

.gate { min-height: 100dvh; display: grid; place-items: center; padding: 24px; }
.gate[hidden] { display: none; }
.gate-card { max-width: 340px; text-align: center; padding: 28px 22px; border-radius: var(--r-card); }
.gate-mark { font-size: 46px; color: var(--gold); }
.gate-card h1 { margin: 8px 0 6px; font-size: 32px; letter-spacing: -.5px; }
.gate-sub { margin: 0 0 24px; color: var(--dim); font-size: 15px; line-height: 1.5; }
.gate-note { margin-top: 16px; color: var(--dim); font-size: 12.5px; line-height: 1.5; }
.gate-msg {
  margin: 0 0 18px; padding: 11px 13px;
  border-radius: var(--r-mid);
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  color: var(--warn);
  font-size: 13px; line-height: 1.45; text-align: left;
}
.gate-msg[hidden] { display: none; }

/* ── the mode card ─────────────────────────────────────────────────────── */

.mode-cards { display: grid; gap: 12px; }

.mode-card {
  display: block; width: 100%; text-align: left;
  font: inherit; color: inherit; cursor: pointer;
  padding: 18px;
  border-radius: var(--r-card);
}
.mode-card:active { opacity: .8; }
.mode-card-top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.mode-card h3 { margin: 0; font-size: 21px; font-weight: 700; letter-spacing: -.3px; }
.mode-card .shape { color: var(--dim); font-size: 13px; font-family: var(--font-mono); }
.mode-card .blurb { margin: 6px 0 0; color: var(--ink-2); font-size: 14px; line-height: 1.45; }

.mode-card-stats {
  display: flex; gap: 18px; margin-top: 14px;
  padding-top: 13px; border-top: 1px solid var(--line-2);
}
.mode-card-stats div { display: flex; flex-direction: column; gap: 1px; }
.mode-card-stats .v { font-family: var(--font-num); font-size: 18px; font-weight: 600; font-variant-numeric: tabular-nums; }
.mode-card-stats .k { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: .4px; }
.mode-card-stats .v.none { color: var(--dim-2); }

/* What a tap does, when it is not simply "play": continue, see the board, or
   Support. A locked card (a supporter's mode, for anyone else) is dimmed but
   still a card — it says what it is, and the tap goes to the product page. */
.mode-card-cta { margin: 12px 0 0; font-size: 13px; color: var(--link); font-weight: 600; }
.mode-card.locked { opacity: .72; }
.mode-card.locked .mode-card-cta { color: var(--gold); }
.mode-card.done .mode-card-cta { color: var(--dim); font-weight: 500; }

/* ── the scene ─────────────────────────────────────────────────────────── */

/* Under the app (z 0 against the app's 1), over the ground. The game view
   marks itself `scenic` while a scene is up, and its text goes light with a
   shadow: every scene is dark enough for white, and none is light enough
   for the slate's grey. */
.backdrop {
  position: fixed; inset: 0; z-index: 0;
  background-size: cover; background-position: center; background-repeat: no-repeat;
  pointer-events: none;
}
.backdrop[hidden] { display: none; }
#app { position: relative; z-index: 1; }
#view-game.scenic { color: #fff; }
#view-game.scenic .game-stat-k { color: rgba(255,255,255,.72); }
#view-game.scenic .game-stat-v { text-shadow: 0 1px 3px rgba(0,0,0,.6); }
/* THE TRANSLUCENT BOARD, as the original's: a dark glass over the scene and
   a checkerboard of sockets showing through it. */
#view-game.scenic .board {
  background: rgba(8, 8, 20, .42);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.14), 0 12px 40px rgba(0,0,0,.45);
}
#view-game.scenic .socket { background: rgba(255,255,255,.06); }
#view-game.scenic .socket.alt { background: rgba(0,0,0,.22); }
/* The glass cards on a scene are the dark glass in either theme. */
#view-game.scenic .chain, #view-game.scenic .icon-btn, #view-game.scenic .btn-quiet {
  background: rgba(20, 20, 30, .62); color: #fff; border-color: rgba(255,255,255,.14);
}
#view-game.scenic .chain small { color: rgba(255,255,255,.7); }
.seg-wrap { flex-wrap: wrap; }
.seg-wrap button { flex: 1 1 30%; }
.seg-wrap button[disabled] { opacity: .45; cursor: default; }

/* ── the game screen ───────────────────────────────────────────────────── */

/* NO SAFE-AREA INSET AND NO GLASS OFFSET HERE — both are on `.view`, which wraps
   this. `mines` had them on both, so a notched phone was pushed down by the
   status bar twice: about 120px of nothing above the title. One screen, one
   measurement, in the outer element every screen shares. */
.game-head {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 0 10px;
}
.game-stat { display: flex; flex-direction: column; align-items: center; line-height: 1.05; }
.game-stat-right { margin-left: auto; }
.game-stat-v {
  font-family: var(--font-num); font-size: 26px; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* The score jumps when it changes, so a cascade is visible in the number as
   well as on the board. Transform only — animating a font-size would reflow the
   header on every point scored. */
#stat-score { display: inline-block; transition: transform 140ms cubic-bezier(.2,1.6,.4,1); }
#stat-score.bump { transform: scale(1.22); }
.game-stat-k { font-size: 10.5px; color: var(--dim); text-transform: uppercase; letter-spacing: .5px; }
/* The clock (lightning): red in the last ten seconds, and a jump when a
   cascade buys time — the same bump the score does. */
#stat-moves.low { color: var(--bad); }
#stat-moves { display: inline-block; transition: transform 140ms cubic-bezier(.2,1.6,.4,1); }
#stat-moves.plus { transform: scale(1.22); }
.pts.clock-plus { right: 8px; top: -6px; left: auto; transform: none; color: var(--good); font-size: 20px; animation-name: clock-plus; }
@keyframes clock-plus {
  0%   { opacity: 0; transform: translateY(6px); }
  20%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-28px); }
}
.game-head .game-stat:first-of-type { margin-left: 4px; }

/* THE GAME SCREEN IS A COLUMN, and the play area takes what the header leaves.
   An 8×8 board on a tall phone is width-limited, so without this there is a
   third of a screen of nothing under the controls and the game looks abandoned
   at the top of its own page. Centring the play area is the whole fix — but see
   layoutBoard() in app.js, which must NOT measure the board's own offsetTop once
   that offset depends on the board's height. */
#view-game {
  display: flex;
  flex-direction: column;
  /* The whole screen, less the bottom inset (there is no tab bar since
     2026-09-15 — the menu is a sheet). The glass offset and the top inset are
     NOT subtracted here even though they look like they should be: they are
     already inside this box, as `.view`'s own padding-top, and box-sizing is
     border-box. Subtracting them again made the section end 20px short and
     pushed the whole centred group upward — which reads as the board sitting
     slightly too high with a band of nothing under it, and is very hard to
     attribute to arithmetic when you are looking at a picture. */
  height: 100dvh;
  box-sizing: border-box;
  padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  overflow: hidden;
  touch-action: none;
  /* Tighter at the sides than every other screen, because here the width IS the
     board: 16px of gutter costs a whole 2px off every jewel on a 390pt phone.
     layoutBoard() MEASURES this rather than carrying its own copy of the
     number. */
  padding-left: 10px;
  padding-right: 10px;
}
/* AND THIS LINE IS NOT OPTIONAL. Every other screen is hidden by
   `.view[hidden] { display: none }`, which is one class and one attribute —
   specificity 0,2,0. The rule above is an ID, 1,0,0, and an ID beats any number
   of classes regardless of order. So giving this one section a display in an
   ID rule silently un-hides it: the game screen renders underneath the home
   screen, on top of the leaderboard, and on the settings page, forever.

   The general trap is worth naming because it is easy to walk into twice: the
   moment you style ONE instance of a component by id, you have out-ranked every
   rule that manages that component by class — including the one that hides it. */
#view-game[hidden] { display: none; }
.play {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.board-wrap { position: relative; }

/* THE BOARD OWNS EVERY GESTURE INSIDE IT.
   `touch-action: none` is the single most important declaration in this file:
   without it a vertical swipe to move a gem down is claimed by the page
   scroller instead, the board jerks up the screen, and the swap never happens.
   It is also why the page cannot be scrolled by dragging on the board, which is
   correct — there is nothing below it worth scrolling to mid-game. */
.board {
  position: relative;
  margin: 0 auto;
  touch-action: none;
  background: linear-gradient(160deg, var(--felt), var(--felt-2));
  border-radius: var(--r-card);
  box-shadow: inset 0 1px 3px rgba(0,0,0,.10), inset 0 0 0 1px var(--line-2);
  overflow: hidden;      /* new jewels drop in from above and must not show */
}

/* One socket per cell, painted under the jewels so the board still reads as a
   grid when a column is mid-fall and genuinely has holes in it. */
.socket {
  position: absolute;
  border-radius: var(--r-cell);
  background: var(--socket);
  pointer-events: none;
}

.tile {
  position: absolute;
  display: grid; place-items: center;
  /* Positioned by transform, never by left/top: a transform is composited and a
     column of eight falling gems stays on the GPU, where sixty frames a second
     is free. Animating `top` would relayout the board eight times a frame. */
  will-change: transform;
  transition: transform var(--t-fall) cubic-bezier(.34, .06, .3, 1);
}
.tile .gem { width: 100%; height: 100%; display: block; overflow: visible; }

/* Nothing moves until the board says so. buildBoard() places every tile with
   this class on and removes it a frame later, so the opening deal appears in
   place instead of eight columns visibly sliding in from the ceiling. */
.tile.instant { transition: none; }

/* SELECTED — the gem you have picked up. It lifts and gets a ring; the ring is
   on a pseudo-element rather than the SVG so it traces the CELL, not the jewel's
   outline, which would put a triangular halo around the amethyst. */
.tile.sel { z-index: 3; }
.tile.sel::after {
  content: "";
  position: absolute; inset: -2px;
  border-radius: calc(var(--r-cell) + 3px);
  box-shadow: 0 0 0 3px var(--select), 0 4px 12px rgba(0,0,0,.22);
  animation: sel-pulse 1.4s ease-in-out infinite;
}
.tile.sel .gem { transform: scale(1.1); transition: transform 120ms ease; }
@keyframes sel-pulse {
  0%, 100% { box-shadow: 0 0 0 3px var(--select), 0 4px 12px rgba(0,0,0,.22); }
  50%      { box-shadow: 0 0 0 5px color-mix(in srgb, var(--select) 55%, transparent), 0 4px 12px rgba(0,0,0,.22); }
}

/* PRESSED — the gem under a finger that has not yet decided. Deliberately
   slighter than selection: it is a report of where the finger is, not a state
   the game is in. */
.tile.press .gem { transform: scale(.9); transition: transform 90ms ease; }

/* A swap that made no line. The two gems travel most of the way and come back,
   which says "that was understood, and it does not work" — where doing nothing
   at all would say "the app did not see you". */
.tile.reject { animation: reject var(--t-swap) ease-in-out 2 alternate; }
@keyframes reject { to { filter: brightness(1.25); } }

/* CLEARING. The jewel flashes white, swells, then collapses — the flash first
   is what makes it read as the gem breaking rather than merely leaving. */
.tile.pop { z-index: 2; animation: pop var(--t-pop) cubic-bezier(.3,.1,.5,1) forwards; }
@keyframes pop {
  0%   { transform: var(--pos) scale(1);    filter: brightness(1); }
  35%  { transform: var(--pos) scale(1.28); filter: brightness(2.4) saturate(.4); }
  100% { transform: var(--pos) scale(0);    filter: brightness(2.4) saturate(.4); opacity: 0; }
}

/* THE SPECIALS. A power gem is the same jewel, lit: a core in the sprite and
   a glow on the tile that breathes. A hypercube is its own stone and turns
   slowly, so it is never mistaken for a colour. Being born is one swell. */
.tile[data-kind="1"] .gem { filter: drop-shadow(0 0 5px rgba(255,255,255,.95)) drop-shadow(0 0 10px rgba(255,220,120,.7)); animation: powerglow 1.4s ease-in-out infinite alternate; }
@keyframes powerglow { from { filter: drop-shadow(0 0 3px rgba(255,255,255,.8)) drop-shadow(0 0 6px rgba(255,220,120,.4)); }
                       to   { filter: drop-shadow(0 0 6px rgba(255,255,255,1))  drop-shadow(0 0 14px rgba(255,220,120,.9)); } }
.tile[data-kind="2"] .gem { filter: drop-shadow(0 0 6px rgba(255,255,255,.9)); animation: hyperturn 6s linear infinite; }
@keyframes hyperturn { to { transform: rotate(360deg); } }
.tile.born .gem { animation: born var(--t-pop) ease-out 1, powerglow 1.4s ease-in-out infinite alternate; }
.tile[data-kind="2"].born .gem { animation: born var(--t-pop) ease-out 1, hyperturn 6s linear infinite; }
@keyframes born { 0% { transform: scale(.6); filter: brightness(3); } 60% { transform: scale(1.3); } 100% { transform: scale(1); } }
/* The ring a power gem leaves behind. */
.blast {
  position: absolute; border-radius: 50%; pointer-events: none;
  border: 3px solid rgba(255,255,255,.95);
  box-shadow: 0 0 18px rgba(255,220,120,.9), inset 0 0 18px rgba(255,220,120,.7);
  animation: blast 480ms ease-out forwards;
}
@keyframes blast { from { transform: scale(.25); opacity: 1; } to { transform: scale(1); opacity: 0; } }
.gem-key .special .gem { filter: drop-shadow(0 0 4px rgba(255,255,255,.9)); }

/* A long line gets its own flare behind it, so a five is visibly different from
   a three rather than merely worth more. */
.flare {
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle, rgba(255,255,255,.9), rgba(255,255,255,0) 70%);
  animation: flare 420ms ease-out forwards;
}
@keyframes flare {
  from { transform: scale(.3); opacity: .95; }
  to   { transform: scale(2.4); opacity: 0; }
}

/* Score popups and the cascade banner both live here, above the jewels. */
.fx { position: absolute; inset: 0; pointer-events: none; z-index: 4; }

.pts {
  position: absolute;
  font-family: var(--font-num);
  font-weight: 800;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,.55), 0 0 12px rgba(0,0,0,.35);
  transform: translate(-50%, -50%);
  animation: pts 900ms cubic-bezier(.2,.8,.3,1) forwards;
  white-space: nowrap;
}
@keyframes pts {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.6); }
  18%  { opacity: 1; transform: translate(-50%, -60%) scale(1.15); }
  35%  { transform: translate(-50%, -66%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -130%) scale(1); }
}

/* The cascade banner. It only appears from the second step of a chain, because
   a "×1" on every single match would be noise that means nothing. */
.combo {
  position: absolute; left: 50%; top: 42%;
  transform: translate(-50%, -50%);
  font-family: var(--font-num); font-weight: 800; font-size: 34px;
  color: #fff;
  text-shadow: 0 2px 10px rgba(0,0,0,.6);
  animation: combo 760ms cubic-bezier(.2,.9,.3,1) forwards;
  white-space: nowrap;
}
@keyframes combo {
  0%   { opacity: 0; transform: translate(-50%,-50%) scale(.5) rotate(-6deg); }
  22%  { opacity: 1; transform: translate(-50%,-50%) scale(1.15) rotate(2deg); }
  40%  { transform: translate(-50%,-50%) scale(1) rotate(0); }
  100% { opacity: 0; transform: translate(-50%,-90%) scale(1); }
}

/* HINT — the same shape of highlight `mines` uses, for the same reason: it has
   to be findable at a glance without being mistaken for a selection. */
.tile.hint::after {
  content: "";
  position: absolute; inset: -2px;
  border-radius: calc(var(--r-cell) + 3px);
  box-shadow: 0 0 0 3px var(--link);   /* blue, so a hint is never mistaken for the jewel in your hand (gold) */
  animation: hint 1.1s ease-out 3;
}
@keyframes hint {
  0%, 100% { box-shadow: 0 0 0 3px var(--link); }
  50%      { box-shadow: 0 0 0 6px color-mix(in srgb, var(--link) 50%, transparent); }
}

/* ── reduced motion ────────────────────────────────────────────────────────
   Honoured from the SYSTEM setting and from the app's own switch, which is why
   there are two selectors. The board still has to update — a match-3 with no
   state change is not a calmer game, it is a broken one — so this shortens
   everything rather than removing it, and drops the decorative layers (the
   pulse, the flare, the swelling combo) entirely.

   app.js reads --t-fall back out of the computed style to time its own waits,
   so shortening it here really does make the game run faster rather than
   leaving the JavaScript waiting on animations that already finished. That is
   the whole reason those three numbers are custom properties. */
@media (prefers-reduced-motion: reduce) {
  :root { --t-swap: 60ms; --t-pop: 70ms; --t-fall: 80ms; }
  .tile.sel::after, .tile.hint::after { animation: none; }
  .flare, .combo, .blast { display: none; }
  .tile[data-kind="1"] .gem, .tile[data-kind="2"] .gem, .tile.born .gem { animation: none; }
  .pts { animation-duration: 420ms; }
}
:root[data-calm="1"] { --t-swap: 60ms; --t-pop: 70ms; --t-fall: 80ms; }
:root[data-calm="1"] .tile.sel::after,
:root[data-calm="1"] .tile.hint::after { animation: none; }
:root[data-calm="1"] .flare,
:root[data-calm="1"] .combo, :root[data-calm="1"] .blast { display: none; }
:root[data-calm="1"] .tile[data-kind="1"] .gem, :root[data-calm="1"] .tile[data-kind="2"] .gem,
:root[data-calm="1"] .tile.born .gem { animation: none; }
:root[data-calm="1"] .pts { animation-duration: 420ms; }

/* ── controls ──────────────────────────────────────────────────────────── */

/* THE STRIP HAS ONE HEIGHT, FOR EVER (2026-09-15). The play area centres the
   board and this strip together in whatever the header leaves, so any change
   in the strip's height moved the board — a second line of text under the
   board, and every jewel jumped 8px up. The strip is now a fixed 72px: a
   title and up to two lines of sub, clamped, and anything that needs more
   room than that is a popup (.toast) that sits OVER the layout rather than
   in it. The board does not move because nothing under it can change size.
   72px is the strip's own content at two sub lines (10 + 18 + 1 + 2×16.2 +
   10, from the line-heights set below, which is why they are set), so
   nothing is clipped at the clamp. The Hint button keeps
   its natural height in the middle rather than stretching to a 72px slab. */
.controls { display: flex; gap: 8px; margin-top: 16px; align-items: center; height: 72px; flex: none; }
.controls .chain { align-self: stretch; }
.controls .btn-quiet {
  padding-left: 18px; padding-right: 18px; flex: none;
  /* The Hint button is the one quiet button that sits on the ground rather
     than in a card, so it keeps its glass. */
  background: var(--glass); border-color: var(--line); box-shadow: var(--shadow);
  backdrop-filter: blur(12px) saturate(140%); -webkit-backdrop-filter: blur(12px) saturate(140%);
  color: var(--ink);
}

.chain {
  flex: 1;
  display: flex; flex-direction: column; gap: 1px; justify-content: center;
  padding: 10px 16px;
  border-radius: var(--r-mid);
  min-width: 0;
}
.chain strong { font-size: 15px; line-height: 1.2; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chain small {
  font-size: 12px; line-height: 1.35; color: var(--dim);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* While a chain is running the strip takes the app's accent, so the one place
   a player is already looking says that something is still happening. */
.chain.live { background: var(--gold); border-color: transparent; }
.chain.live strong, .chain.live small { color: #1a1200; }
.chain.live small { color: rgba(26,18,0,.7); }

/* ── the end-of-game overlay ───────────────────────────────────────────── */

.overlay {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-radius: var(--r-card);
  z-index: 5;
}
.overlay[hidden] { display: none; }
.overlay-card {
  width: min(320px, 92%);
  padding: 24px 20px;
  text-align: center;
  border-radius: var(--r-card);
}
.overlay-mark { font-size: 34px; line-height: 1; }
.overlay-card h2 { margin: 8px 0 4px; font-size: 24px; letter-spacing: -.4px; }
.overlay-line { margin: 0; color: var(--dim); font-size: 14px; line-height: 1.5; }
.overlay-stats {
  display: flex; justify-content: center; gap: 20px;
  margin: 18px 0 0; padding: 14px 0 0;
  border-top: 1px solid var(--line-2);
}
.overlay-stats div { display: flex; flex-direction: column; gap: 2px; }
.overlay-stats dt { font-size: 10.5px; color: var(--dim); text-transform: uppercase; letter-spacing: .4px; order: 2; }
.overlay-stats dd { margin: 0; font-family: var(--font-num); font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; order: 1; }
.overlay-submit { margin: 14px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--dim); min-height: 17px; }
.overlay-submit.bad { color: var(--warn); }
.overlay-submit.good { color: var(--good); }
.overlay-btns { display: flex; gap: 10px; margin-top: 18px; }
.overlay-btns .btn { flex: 1; }

/* ── segmented control ─────────────────────────────────────────────────── */

/* Conquest's segmented control: a glass trough, the chosen segment gold. */
.seg {
  display: flex; gap: 3px; padding: 3px;
  background: var(--glass-2); border-radius: var(--r-sm); border-color: var(--line-2); box-shadow: none;
}
.seg button {
  flex: 1; font: inherit; font-size: 14px; font-weight: 500;
  padding: 8px 6px; border: 0; border-radius: 8px;
  background: transparent; color: var(--ink); cursor: pointer;
}
.seg button[aria-selected="true"], .seg button.is-on {
  background: var(--gold); color: #1a1200; font-weight: 600;
  box-shadow: var(--shadow);
}
.seg-sm { max-width: 230px; }
/* One mode means one tab, which is a control that cannot be operated. Hidden
   rather than deleted, so a second mode makes it reappear on its own. */
.seg:has(button:only-child) { display: none; }

/* ── the leaderboard ───────────────────────────────────────────────────── */

.board-list { margin-top: 12px; }
.board-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line-2);
}
.board-row:last-child { border-bottom: 0; }
.board-row.you { background: color-mix(in srgb, var(--gold) 14%, transparent); }
.board-rank {
  width: 26px; flex: none; text-align: right;
  font-family: var(--font-num); font-weight: 700; font-size: 15px;
  font-variant-numeric: tabular-nums; color: var(--dim);
}
.board-row.you .board-rank { color: var(--gold); }
/* The supporter's mark: a diamond, in gold — this game's own shape (the tab,
   the gate, the overlay all wear it), where conquest and wizard use a star. */
.mark { color: var(--gold); font-size: .8em; margin-left: 4px; }
#support-card .btn-wide { margin-top: 0; }
.support-note { margin-top: 10px; }
.board-who { flex: 1; min-width: 0; }
.board-name { font-size: 15px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.board-meta { font-size: 11.5px; color: var(--dim); margin-top: 1px; }
.board-score {
  font-family: var(--font-num); font-size: 18px; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.board-empty { padding: 30px 20px; text-align: center; color: var(--dim); font-size: 14px; line-height: 1.5; }

/* ── settings rows ─────────────────────────────────────────────────────── */

.rows > .row {
  padding: 14px 16px;
  border-bottom: 1px solid var(--line-2);
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
}
.rows > .row:last-child { border-bottom: 0; }
/* Matched at the same depth as the rule above, not as a bare `.row-stack`:
   `.rows > .row` is more specific, so its `align-items: center` would win and
   every stacked row's header would shrink to its content and sit in the middle
   of the card instead of spanning it. `mines` shipped exactly that. */
.rows > .row.row-stack { flex-direction: column; align-items: stretch; gap: 8px; }
.row-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
/* The Test buttons beside Haptics and Sound: small, and pushed up against the
   switch so the label keeps the left edge. */
.btn-test { margin-left: auto; padding: 7px 14px; font-size: 14px; }
.row-k { font-size: 15px; font-weight: 500; }
.row-v { font-family: var(--font-num); font-weight: 600; font-variant-numeric: tabular-nums; }
.row-note { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--dim); }
/* Settings › Name (README "Supporters"): a row you tap. A <button> so the
 * haptic switch lands on it like every other control; the value sits at the
 * right like a settings row anywhere, the chevron says it opens. */
.rows > button.row-tap {
  appearance: none; -webkit-appearance: none; width: 100%; background: none; border: 0;
  border-bottom: 1px solid var(--line-2); font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.rows > button.row-tap:active { background: var(--pill); }
.row-tap .row-v { margin-left: auto; font-family: inherit; font-weight: 500; color: var(--dim); display: inline-flex; align-items: center; gap: 6px; }
.row-tap .row-v.own { color: var(--ink); }
.row-tap .row-v .mark { margin-left: 0; }
.row-tap .chev { color: var(--dim); font-size: 22px; line-height: 1; margin-top: -2px; transition: transform .15s; }
.row-tap[aria-expanded="true"] .chev { transform: rotate(90deg); }
.name-actions { display: flex; gap: 8px; align-items: center; }
.name-actions .btn { padding: 8px 14px; font-size: 14px; }
/* 16px: below that iOS zooms the page to the field, and the sheet is a
 * scroll away from where it was. */
.name-input {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 16px; padding: 10px 12px;
  color: var(--ink); background: var(--pill); border: 1px solid var(--line); border-radius: 10px;
}
.name-input:focus { outline: 2px solid var(--gold, #d9a520); outline-offset: 0; border-color: transparent; }

.switch { position: relative; display: inline-block; width: 51px; height: 31px; flex: none; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; z-index: 2; }
.switch span {
  position: absolute; inset: 0; border-radius: 999px;
  background: var(--pill); transition: background .2s ease;
}
.switch span::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 27px; height: 27px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.25);
  transition: transform .2s ease;
}
.switch input:checked + span { background: var(--good); }
.switch input:checked + span::after { transform: translateX(20px); }
.switch input:disabled + span { opacity: .4; }

.prose { padding: 16px; }
.prose p { margin: 0 0 12px; font-size: 14px; line-height: 1.6; color: var(--ink-2); }
.prose p:last-child { margin-bottom: 0; }
.prose-meta { font-size: 12px !important; color: var(--dim) !important; font-family: var(--font-mono); }

.score-table {
  width: 100%; border-collapse: collapse; margin-bottom: 14px;
  font-family: var(--font-num); font-variant-numeric: tabular-nums;
}
.score-table th {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .4px;
  color: var(--dim); font-weight: 600; text-align: left;
  padding-bottom: 6px; border-bottom: 1px solid var(--line-2);
}
.score-table td { padding: 5px 0; font-size: 15px; color: var(--ink-2); }
.score-table td:nth-child(2), .score-table td:nth-child(4) { font-weight: 700; color: var(--ink); }
.score-table td:nth-child(3), .score-table th:nth-child(3) { padding-left: 20px; }

/* The jewel key. A grid rather than a list, because the point is comparing the
   seven shapes side by side. */
.gem-key {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(72px, 1fr));
  gap: 4px; padding: 16px 12px 4px;
}
.gem-key div { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.gem-key .gem { width: 38px; height: 38px; overflow: visible; }
.gem-key span { font-size: 11px; color: var(--dim); }
.gem-key-note { padding: 10px 16px 16px; }

/* ── the menu sheet ────────────────────────────────────────────────────── */
/* Conquest's, verbatim in shape: a full-screen sheet over whatever was
   there, a head with the title and a round ✕, and a body that scrolls.
   Closing it puts you back exactly where you were — on the home, or on the
   game mid-move — which is the whole reason a sheet beats a tab bar for a
   game: there is no other screen to get lost on. */
.sheet {
  position: fixed; inset: 0; z-index: 30; background: var(--bg);
  display: flex; flex-direction: column;
}
.sheet[hidden] { display: none; }
.sheet-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: calc(12px + env(safe-area-inset-top, 0px)) calc(16px + env(safe-area-inset-right, 0px)) 10px calc(16px + env(safe-area-inset-left, 0px));
  border-bottom: .5px solid var(--line-2);
  background: color-mix(in srgb, var(--bg) 70%, transparent);
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
}
.sheet-head h2 { margin: 0; font-size: 18px; }
.sheet-body {
  flex: 1; overflow: auto; -webkit-overflow-scrolling: touch;
  padding: 4px calc(16px + env(safe-area-inset-right, 0px)) calc(24px + env(safe-area-inset-bottom, 0px)) calc(16px + env(safe-area-inset-left, 0px));
  max-width: 560px; width: 100%; margin: 0 auto; box-sizing: border-box;
}
.sheet-body h2.sec:first-child { margin-top: 14px; }
.fineprint.top { margin: 0 4px 12px; }
/* ── the toast ─────────────────────────────────────────────────────────
   A refusal, a hint, the mode's note: the messages that used to be the
   strip's second line and made it taller. A toast is absolutely positioned
   at the bottom of the play area, so it is out of the layout entirely — on a
   tall phone it sits in the empty space under the strip, on a short one it
   lies over the strip — and the board never learns it exists. It goes on its
   own, or on the next touch of the board, or on a tap. */
.play { position: relative; }
.toast {
  position: absolute; left: 4px; right: 4px; bottom: 0;
  z-index: 6;
  padding: 10px 14px;
  border-radius: var(--r-mid);
  background: var(--ink); color: var(--bg);
  font-size: 12.5px; line-height: 1.45;
  box-shadow: var(--shadow);
  cursor: pointer;
  animation: toast-in 180ms ease-out;
}
.toast[hidden] { display: none; }
.toast.warn { color: #ff8a8a; }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
:root[data-calm="1"] .toast { animation: none; }

/* ── the banner ────────────────────────────────────────────────────────── */

.banner {
  position: fixed; left: 12px; right: 12px;
  bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px;
  background: var(--ink); color: var(--bg);
  border-radius: var(--r-mid);
  font-size: 13.5px; line-height: 1.45;
  box-shadow: var(--shadow);
  z-index: 20;
}
.banner[hidden] { display: none; }

/* The banner's Retry. An installed home-screen app has no address bar and no
   reload button, so a message about a failed read used to be the end of the
   conversation. */
.banner .banner-retry {
  margin-left: auto;
  padding: 6px 13px;
  font-size: 12.5px; font-weight: 600; line-height: 1.2;
  border-radius: 999px;
  background: var(--bg); color: var(--ink);
  border: 0; cursor: pointer;
}
.banner .banner-retry[hidden] { display: none; }
.banner .banner-retry:disabled { opacity: .45; }
.banner .banner-retry + button { margin-left: 4px; }
.banner button {
  margin-left: auto; flex: none;
  font: inherit; font-size: 20px; line-height: 1;
  border: 0; background: transparent; color: inherit; cursor: pointer;
}

/* The troubleshooting list. Numbered because the order is the advice: try the
   cheap thing first, and clearing site data is genuinely last. */
.tshoot { margin: 0; padding-left: 20px; }
.tshoot li { margin-bottom: 10px; line-height: 1.5; font-size: 14px; color: var(--ink-2); }
.tshoot li:last-child { margin-bottom: 0; }
.tshoot strong { font-weight: 600; }
/* The measured shell line is a different kind of statement from the advice above
   it — a fact about this device, not a step to take — so it gets a rule above it
   rather than reading as a sixth instruction. */
.tshoot + .prose-meta {
  margin-top: 14px; padding-top: 12px;
  border-top: 1px solid var(--line-2, var(--line));
}


/* The one line in the app that is asking for something to be done, so not dim
   like the fineprint it sits under. */
.update-note { color: var(--warn); }
