/* 2048 — Pixel 11 build.
 *
 * Gabriele Cirulli's original game, retuned for one device: a 6.3" 1080x2424
 * OLED (about 412 x 915 CSS px), 60-120Hz adaptive, P3 wide gamut, 3000 nits,
 * Tensor G6. Decisions here optimise for that panel rather than for the wide
 * range of phones a generic build has to survive.
 *
 * What upstream did and why it is gone:
 *   - The board was pinned to 280px on anything under 520px wide, leaving a
 *     third of this screen empty. Here the board is one length, --board, that
 *     fills the display width, and every dimension derives from it.
 *   - Tiles were positioned with fixed px transforms per breakpoint. Here they
 *     are percentages of the tile's own box, so the geometry is resolution
 *     independent and the 497ppi panel gets sub-pixel-exact placement.
 *
 * Grid math, all as a fraction of --board:
 *   gap g = 3%, cell c = 21.25%   (4c + 5g = 100%)
 *   cell origin along an axis = 3% + (index - 1) * 24.25%
 * translate() resolves % against the element's OWN box, so each board-percentage
 * is divided by 21.25:  3% -> 14.1176%,  one step 24.25% -> 114.1176%.
 */

@font-face {
  font-family: "Clear Sans";
  src: url("fonts/ClearSans-Light-webfont.woff") format("woff");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Clear Sans";
  src: url("fonts/ClearSans-Regular-webfont.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Clear Sans";
  src: url("fonts/ClearSans-Bold-webfont.woff") format("woff");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Structural colours — the cream identity, unchanged. The night palette lower
     down aliases these same tokens, so every colour is painted explicitly and
     none is inherited from the host page in either theme. */
  color-scheme: light;

  --ground: #faf8ef;
  --ink: #776e65;
  --ink-muted: rgba(119, 110, 101, 0.85);
  --ink-light: #f9f6f2;
  --board-bg: #bbada0;
  --cell-empty: rgba(238, 228, 218, 0.35);
  --button: #8f7a66;
  --button-active: #9f8b77;
  --score-label: #eee4da;
  --score-add: rgba(119, 110, 101, 0.9);
  --overlay: rgba(238, 228, 218, 0.73);
  --overlay-won: rgba(237, 194, 46, 0.5);

  /* Tile ramp, sRGB fallback. The P3 block below restates all of it.
   *
   * A temperature gradient: orchid at 2, cooling through periwinkle and blue to
   * cyan, then warming through green, chartreuse and gold to the hot red-orange
   * at 2048.
   *
   * Spliced from two solves by tools/make_gradient.py rather than one. The low
   * end (2-16) comes from a run with a hard lightness climb, which is what the
   * blues need — cyan is perceptually compressed, so rotating hue through it
   * buys far less separation than the same rotation through greens or golds,
   * and those tiles need lightness to tell them apart. The high end (32-2048)
   * comes from a gentler run, whose shallower approach to the terminal red
   * avoids the ΔE 32 cliff the steeper one left between 1024 and 2048.
   *
   * The result beats either source ramp: worst adjacent pair ΔE 12.07, worst
   * step 2->4 down to 14.06, and no two tiles anywhere on the board — adjacent
   * on the ramp or not — closer than ΔE 12. Upstream's gold run bottomed out at
   * 2.18. Re-measure with tools/check_ramp.py.
   */
  --t2: #daaddc;
  --t4: #bebbff;
  --t8: #95d1ff;
  --t16: #39e7ff;
  --t32: #00d5c3;
  --t64: #00db97;
  --t128: #63d754;
  --t256: #a4c800;
  --t512: #d2ae00;
  --t1024: #f28b00;
  --t2048: #ff6035;
  --tsuper: #3c3a32;

  /* One ink for every tile. Upstream switched from dark to light text partway
     up the ramp, which puts nine of its twelve tiles under the 3:1 that large
     bold text has to meet — as low as 1.42:1 on the 128. With lightness moving
     the way it does here there is no single switch point that works anyway, and
     this near-black clears 5.43:1 against the worst tile on the ramp, past even
     the 4.5:1 normal-text bar. */
  --tile-ink: #241f1a;

  /* 12px gutters rather than 16 — on a 412px screen every pixel of board is
     worth more than the margin. */
  --gutter: 12px;
  /* Everything on the page but the board: heading, the game, tile and
     auto-fill switches, the gaps between them and the container's padding.
     Measured in the browser at 412px wide, not estimated; if it runs short, a
     screen short enough for height to size the board cuts off the bottom rows. */
  --chrome: 281px;
  --board-cap: 560px;

  --avail-h: calc(100vh - var(--chrome));
  --board: min(calc(100vw - 2 * var(--gutter)), var(--avail-h), var(--board-cap));

  --cell: calc(var(--board) * 0.2125);
  --radius: calc(var(--board) * 0.008);
  --radius-board: calc(var(--board) * 0.014);

  /* Motion. At 120Hz a 90ms slide is ~11 frames, enough to read as a glide
     rather than a jump, and short enough that a fast player is never waiting on
     the animation. The curve is a decelerating ease-out: quick departure, soft
     arrival, no overshoot to fight the next move. */
  --slide: 90ms;
  --slide-ease: cubic-bezier(0.22, 0.85, 0.28, 1);
}

/* On a phone 100vh is the tall, URL-bar-hidden height, which overflows while
   the bar is showing. 100svh is the honest one. */
@supports (height: 100svh) {
  :root {
    --avail-h: calc(100svh - var(--chrome));
  }
}

/* The leaderboard is injected only when a backend is configured, so its height
   is reserved only when it is really there. */
:root:has(.leaderboard) {
  --chrome: 388px; /* plus the leaderboard, measured at its taller name-entry state */
}

/* Wide gamut. The ramp is drawn at high chroma the whole way round, and the
   cyan-to-green stretch in particular sits well outside sRGB — these are the
   colours the ramp was actually solved for, with the hex values above as the
   clipped fallback. The board and ground stay as they are: they carry the
   game's identity and gain nothing from the extra gamut. */
@supports (color: color(display-p3 1 1 1)) {
  :root {
    --t2: color(display-p3 0.826 0.685 0.852);
    --t4: color(display-p3 0.739 0.732 1.000);
    --t8: color(display-p3 0.617 0.816 1.000);
    --t16: color(display-p3 0.405 0.899 1.000);
    --t32: color(display-p3 0.000 0.845 0.771);
    --t64: color(display-p3 0.321 0.856 0.593);
    --t128: color(display-p3 0.510 0.835 0.400);
    --t256: color(display-p3 0.667 0.780 0.228);
    --t512: color(display-p3 0.808 0.687 0.112);
    --t1024: color(display-p3 0.926 0.548 0.108);
    --t2048: color(display-p3 1.000 0.345 0.190);
  }
}

/* ---- original palette ----
 *
 * Gabriele Cirulli's exact sRGB values, offered as a switch. Deliberately not
 * extended into display-p3 the way the custom ramp is: "Original" should mean
 * the colours upstream actually ships, not an enhanced version of them.
 *
 * The ink reverts too. Upstream runs dark text on the two pale tiles and white
 * from 8 upward — the split the custom ramp does without. It is also where
 * upstream's legibility goes: nine of its twelve tiles put text under 3:1, the
 * floor being 1.42:1 on the 128. Faithful, and worth seeing next to the other
 * option.
 *
 * One thing is not reverted: the 2 and 4 tiles are still darkened for night
 * play. At #eee4da they are near-white, and sixteen of them on a near-black
 * ground is genuinely unpleasant. Upstream has no dark theme at all, so there
 * is no original behaviour to be faithful to here.
 */
:root[data-palette="original"] {
  --t2: #eee4da;
  --t4: #ede0c8;
  --t8: #f2b179;
  --t16: #f59563;
  --t32: #f67c5f;
  --t64: #f65e3b;
  --t128: #edcf72;
  --t256: #edcc61;
  --t512: #edc850;
  --t1024: #edc53f;
  --t2048: #edc22e;
  --tile-ink: #776e65;
}

:root[data-palette="original"] .tile-8 .tile-inner,
:root[data-palette="original"] .tile-16 .tile-inner,
:root[data-palette="original"] .tile-32 .tile-inner,
:root[data-palette="original"] .tile-64 .tile-inner,
:root[data-palette="original"] .tile-128 .tile-inner,
:root[data-palette="original"] .tile-256 .tile-inner,
:root[data-palette="original"] .tile-512 .tile-inner,
:root[data-palette="original"] .tile-1024 .tile-inner,
:root[data-palette="original"] .tile-2048 .tile-inner {
  color: var(--ink-light);
}

/* Darkening the two pale tiles for night also has to move the ink that sits on
   them. Upstream's #776e65 was chosen against near-white; on the darkened tile
   it lands at 2.01:1, which is unreadable. The tiles from 8 up keep upstream's
   white, since those colours are unchanged. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"])[data-palette="original"] {
    --t2: #4a4137;
    --t4: #5c5044;
    --tile-ink: var(--d-ink);
  }
}

:root[data-theme="dark"][data-palette="original"] {
  --t2: #4a4137;
  --t4: #5c5044;
  --tile-ink: var(--d-ink);
}

/* ---- night palette ----
 *
 * Defined here as --d-* and aliased onto the live tokens by the two blocks
 * below, so the dark values are written once rather than once per theme state.
 *
 * Only the structural colours change, plus the tile past 2048. The gradient is
 * identical in both themes: every one of its stops is a mid-lightness saturated
 * colour that holds against a cream board and a near-black one alike, which the
 * old ramp could not claim — its 2 and 4 were near-white and had to be darkened
 * for night play. What does change is the ground, a warm near-black rather than
 * a neutral grey, since grey next to this board reads as dirty and near-black
 * costs an OLED almost nothing to light.
 */
:root {
  --d-ground: #14110e;
  --d-ink: #d6cec3;
  --d-ink-muted: rgba(214, 206, 195, 0.68);
  --d-board-bg: #2f2822;
  --d-cell-empty: rgba(238, 228, 218, 0.04);
  --d-button: #8f7a66;
  --d-button-active: #a38d78;
  --d-score-label: rgba(238, 228, 218, 0.72);
  --d-score-add: rgba(214, 206, 195, 0.9);
  --d-overlay: rgba(20, 17, 14, 0.8);
  --d-overlay-won: rgba(237, 194, 46, 0.34);
  --d-tsuper: #0d0b09;
}

/* System dark, unless the page has been explicitly set to light. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --ground: var(--d-ground);
    --ink: var(--d-ink);
    --ink-muted: var(--d-ink-muted);
    --board-bg: var(--d-board-bg);
    --cell-empty: var(--d-cell-empty);
    --button: var(--d-button);
    --button-active: var(--d-button-active);
    --score-label: var(--d-score-label);
    --score-add: var(--d-score-add);
    --overlay: var(--d-overlay);
    --overlay-won: var(--d-overlay-won);
    --tsuper: var(--d-tsuper);
  }
}

/* Explicit dark, so the toggle wins over a light system setting. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --ground: var(--d-ground);
  --ink: var(--d-ink);
  --ink-muted: var(--d-ink-muted);
  --board-bg: var(--d-board-bg);
  --cell-empty: var(--d-cell-empty);
  --button: var(--d-button);
  --button-active: var(--d-button-active);
  --score-label: var(--d-score-label);
  --score-add: var(--d-score-add);
  --overlay: var(--d-overlay);
  --overlay-won: var(--d-overlay-won);
  --tsuper: var(--d-tsuper);
}

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
  overscroll-behavior: none; /* stop pull-to-refresh eating a down-swipe */
  background: var(--ground);
  color: var(--ink);
  font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif;
  font-size: 18px;
  -webkit-text-size-adjust: 100%;
}

body {
  display: flex;
  justify-content: center;
  overflow: hidden;
  touch-action: none; /* swipes are ours, not the browser's */
  -webkit-tap-highlight-color: transparent;
}

.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 12px;
  width: var(--board);
  padding-block: 14px;
  padding-inline: 0;
}

/* ---- heading ---- */

/* The logo takes the left of the heading at the full height of both rows on
   the right (scores, then the controls), and is sized to fill whatever width
   those leave it: the title box is a size container, and "2048" in Clear Sans
   Bold runs 2.35em wide, so 100cqi / 2.4 fits it with a hair to spare. Should
   a six-digit score widen the boxes, the logo gives way rather than colliding. */
.heading {
  display: flex;
  align-items: center;
  gap: 12px;
}

.title-box {
  flex: 1 1 0;
  min-width: 0;
  container-type: inline-size;
}

h1.title {
  margin: 0;
  font-size: clamp(24px, calc(100cqi / 2.4), 150px);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  white-space: nowrap;
}

.heading-side {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.scores-container {
  display: flex;
  gap: 6px;
}

.score-container,
.best-container,
.moves-container {
  position: relative;
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  justify-content: center;
  min-width: 62px;
  padding: 8px 12px;
  border-radius: 4px;
  background: var(--board-bg);
  color: var(--ink-light);
  font-size: clamp(17px, calc(var(--board) * 0.055), 26px);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* "Score" / "Best" / "Moves" caption; column-reverse puts it above the number. */
.score-container::after,
.best-container::after,
.moves-container::after {
  content: "Score";
  color: var(--score-label);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.best-container::after {
  content: "Best";
}

.moves-container::after {
  content: "Moves";
}

.score-container .score-addition {
  position: absolute;
  right: 12px;
  z-index: 2;
  color: var(--score-add);
  font-size: 22px;
  font-weight: 700;
  line-height: 22px;
  animation: move-up 600ms ease-in;
  animation-fill-mode: both;
  pointer-events: none;
}

/* Percentages of the score box, so the +N starts level with the number and
   clears the box entirely on its way up rather than crossing the SCORE label. */
@keyframes move-up {
  0% {
    top: 46%;
    opacity: 1;
  }
  100% {
    top: -62%;
    opacity: 0;
  }
}

/* ---- controls row ---- */

/* Just the theme toggle and New Game, right-aligned under the scores. */
.above-game {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
}

.restart-button {
  flex: 0 0 auto;
  padding: 0 20px;
  border-radius: 4px;
  background: var(--button);
  color: var(--ink-light);
  font-size: 15px;
  font-weight: 700;
  line-height: 46px; /* a comfortable thumb target */
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  transition: background-color 160ms ease, color 240ms ease, transform 120ms var(--slide-ease);
}

/* Waiting for the confirming second tap: the filled button turns into an
   outline. A fill swap is too subtle in the day palette, where the ink and
   button browns sit close together; an outline reads in both themes. */
.restart-button.is-armed {
  background: var(--ground);
  color: var(--ink);
  box-shadow: inset 0 0 0 2px var(--ink);
}

.restart-button:active {
  background: var(--button-active);
  transform: scale(0.96);
}

.restart-button:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 2px;
}

/* Secondary to New Game, so it takes the board's muted brown rather than the
   button brown. Injected by theme.js — absent in hosts that own the theme. */
.theme-toggle {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: var(--board-bg);
  color: var(--ink-light);
  cursor: pointer;
  transition: background-color 160ms ease, color 240ms ease, transform 120ms var(--slide-ease);
}

.theme-toggle:active {
  transform: scale(0.94);
}

.theme-toggle:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 2px;
}

.theme-toggle svg {
  width: 21px;
  height: 21px;
  display: block;
}

/* Cross-fade the surfaces when the theme flips, so the switch reads as the
   lights going down rather than as a page reload. Tiles are excluded: the
   actuator recreates them every move, so a colour transition there would never
   have an old value to animate from. */
body,
.game-container,
.grid-cell,
.score-container,
.best-container,
.moves-container {
  transition-property: background-color, color;
  transition-duration: 240ms;
  transition-timing-function: ease;
}

/* ---- board ---- */

.game-container {
  position: relative;
  box-sizing: border-box;
  width: var(--board);
  height: var(--board);
  border-radius: var(--radius-board);
  background: var(--board-bg);
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.grid-container,
.tile-container,
.fx-layer {
  position: absolute;
  inset: 0;
}

.grid-container {
  z-index: 1;
}

.tile-container {
  z-index: 2;
}

/* Merge particles. Above the tiles, never in the way of a swipe. */
.fx-layer {
  z-index: 3;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-board);
  pointer-events: none;
}

.grid-cell {
  position: absolute;
  width: 21.25%;
  height: 21.25%;
  border-radius: var(--radius);
  background: var(--cell-empty);
}

.grid-cell[data-col="1"] { left: 3%; }
.grid-cell[data-col="2"] { left: 27.25%; }
.grid-cell[data-col="3"] { left: 51.5%; }
.grid-cell[data-col="4"] { left: 75.75%; }

.grid-cell[data-row="1"] { top: 3%; }
.grid-cell[data-row="2"] { top: 27.25%; }
.grid-cell[data-row="3"] { top: 51.5%; }
.grid-cell[data-row="4"] { top: 75.75%; }

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

.tile {
  position: absolute;
  top: 0;
  left: 0;
  width: 21.25%;
  height: 21.25%;
  transition: transform var(--slide) var(--slide-ease);
  will-change: transform; /* keep all 16 on their own compositor layers */
}

.tile .tile-inner {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: var(--radius);
  /* Each tile class sets --c to its own colour; background and glow both read
     from it, so a tile's hue lives in exactly one place. */
  background: var(--c, var(--t2));
  color: var(--tile-ink);
  font-size: calc(var(--cell) * 0.52);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* Position stops — derivation in the file header. */
.tile.tile-position-1-1 { transform: translate(14.1176%, 14.1176%); }
.tile.tile-position-1-2 { transform: translate(14.1176%, 128.2353%); }
.tile.tile-position-1-3 { transform: translate(14.1176%, 242.3529%); }
.tile.tile-position-1-4 { transform: translate(14.1176%, 356.4706%); }
.tile.tile-position-2-1 { transform: translate(128.2353%, 14.1176%); }
.tile.tile-position-2-2 { transform: translate(128.2353%, 128.2353%); }
.tile.tile-position-2-3 { transform: translate(128.2353%, 242.3529%); }
.tile.tile-position-2-4 { transform: translate(128.2353%, 356.4706%); }
.tile.tile-position-3-1 { transform: translate(242.3529%, 14.1176%); }
.tile.tile-position-3-2 { transform: translate(242.3529%, 128.2353%); }
.tile.tile-position-3-3 { transform: translate(242.3529%, 242.3529%); }
.tile.tile-position-3-4 { transform: translate(242.3529%, 356.4706%); }
.tile.tile-position-4-1 { transform: translate(356.4706%, 14.1176%); }
.tile.tile-position-4-2 { transform: translate(356.4706%, 128.2353%); }
.tile.tile-position-4-3 { transform: translate(356.4706%, 242.3529%); }
.tile.tile-position-4-4 { transform: translate(356.4706%, 356.4706%); }

.tile-2 .tile-inner    { --c: var(--t2); }
.tile-4 .tile-inner    { --c: var(--t4); }
.tile-8 .tile-inner    { --c: var(--t8); }
.tile-16 .tile-inner   { --c: var(--t16); }
.tile-32 .tile-inner   { --c: var(--t32); }
.tile-64 .tile-inner   { --c: var(--t64); }
.tile-128 .tile-inner  { --c: var(--t128); }
.tile-256 .tile-inner  { --c: var(--t256); }
.tile-512 .tile-inner  { --c: var(--t512); }
.tile-1024 .tile-inner { --c: var(--t1024); }
.tile-2048 .tile-inner { --c: var(--t2048); }

/* Numerals shrink as the digits multiply. */
.tile-128 .tile-inner,
.tile-256 .tile-inner,
.tile-512 .tile-inner  { font-size: calc(var(--cell) * 0.42); }

.tile-1024 .tile-inner,
.tile-2048 .tile-inner { font-size: calc(var(--cell) * 0.34); }

/* The bloom takes the tile's own colour rather than a fixed gold, so a green
   256 glows green and the red 2048 glows red. It also strengthens up the ramp,
   which is what made the original's high tiles feel like a reward. */
.tile-128 .tile-inner {
  box-shadow: 0 0 30px 10px color-mix(in srgb, var(--c) 22%, transparent),
              inset 0 0 0 1px rgba(255, 255, 255, 0.14);
}

.tile-256 .tile-inner {
  box-shadow: 0 0 30px 10px color-mix(in srgb, var(--c) 28%, transparent),
              inset 0 0 0 1px rgba(255, 255, 255, 0.19);
}

.tile-512 .tile-inner {
  box-shadow: 0 0 30px 10px color-mix(in srgb, var(--c) 34%, transparent),
              inset 0 0 0 1px rgba(255, 255, 255, 0.24);
}

.tile-1024 .tile-inner {
  box-shadow: 0 0 30px 10px color-mix(in srgb, var(--c) 40%, transparent),
              inset 0 0 0 1px rgba(255, 255, 255, 0.29);
}

.tile-2048 .tile-inner {
  box-shadow: 0 0 30px 10px color-mix(in srgb, var(--c) 48%, transparent),
              inset 0 0 0 1px rgba(255, 255, 255, 0.33);
}

/* Past 2048 the ramp has nowhere warmer to go, so the tile drops out of it
   entirely — near-black, the one value the gradient never visits. */
.tile-super .tile-inner {
  --c: var(--tsuper);
  color: var(--ink-light);
  font-size: calc(var(--cell) * 0.29);
}

/* The panel reports HDR headroom, so let the top of the ramp use it: the bloom
   goes wider and hotter than an SDR screen could show. */
@media (dynamic-range: high) {
  .tile-512 .tile-inner,
  .tile-1024 .tile-inner,
  .tile-2048 .tile-inner {
    box-shadow: 0 0 44px 14px color-mix(in srgb, var(--c) 52%, transparent),
                inset 0 0 0 1px rgba(255, 255, 255, 0.34);
  }
}

.tile-new .tile-inner {
  animation: appear 160ms var(--slide-ease) 60ms;
  animation-fill-mode: backwards;
}

@keyframes appear {
  0% {
    opacity: 0;
    transform: scale(0.2);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

/* The merge pop lands just after the slide finishes, so the two read as one
   motion rather than as two competing animations. */
.tile-merged .tile-inner {
  z-index: 20;
  animation: pop 220ms var(--slide-ease) 80ms;
  animation-fill-mode: backwards;
}

@keyframes pop {
  0% {
    transform: scale(0.92);
  }
  42% {
    transform: scale(1.18);
  }
  100% {
    transform: scale(1);
  }
}

/* ---- back-gesture note ----
 * Shown by back_guard.js when a back swipe was absorbed. Ink on ground,
 * inverted, so it reads in both themes without a new colour. */
.back-toast {
  position: fixed;
  left: 50%;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 28px);
  z-index: 10;
  padding: 10px 16px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--ground);
  font-size: 14px;
  font-weight: 700;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, 8px);
  transition: opacity 180ms ease, transform 180ms var(--slide-ease);
}

.back-toast.is-shown {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* ---- leaderboard ----
 * Injected by leaderboard.js, and only when a backend is configured, so the
 * reserved chrome grows conditionally rather than leaving a hole when it is
 * absent. */
.leaderboard {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.lb-title {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* One grid for the whole list, shared by every row through subgrid, so the
   columns line up from row to row however wide a name or score runs. */
.lb-list {
  display: grid;
  grid-template-columns: 1.2em minmax(0, 1fr) auto auto;
  column-gap: 10px;
  row-gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* When the table cannot be reached, the board shows this device's own numbers
   rather than an error — dimmed, so it does not read as the shared state. */
.lb-list[data-offline="true"] {
  opacity: 0.55;
}

.lb-row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
}

.lb-row {
  padding: 7px 10px;
  border-radius: 5px;
  background: var(--cell-empty);
  font-size: 14px;
}

.lb-row.is-me {
  background: var(--board-bg);
  color: var(--ink-light);
}

.lb-rank {
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  opacity: 0.6;
}

.lb-name {
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Once a player has reached 2048, the best-tile pill gives way to counts of
   games that reached 2048 and 4096, each a chip in that tile's colour. */
.lb-badges {
  display: flex;
  gap: 6px;
  justify-self: end;
}

.lb-badge {
  padding: 2px 6px;
  border-radius: 4px;
  background: var(--c);
  color: var(--tile-ink);
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.lb-badge[data-tile="2048"] { --c: var(--t2048); }

/* Upstream sets its gold 2048 in light type; match the tile in play. */
:root[data-palette="original"] .lb-badge[data-tile="2048"] {
  color: var(--ink-light);
}

/* The super-tile colour is near-black at night, so it gets a faint rim to
   stand off the dark row. */
.lb-badge[data-tile="4096"] {
  --c: var(--tsuper);
  color: var(--ink-light);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink-light) 28%, transparent);
}

/* The player's best tile, in that tile's own colour — the ramp doing double
   duty as a legend. */
.lb-tile {
  justify-self: end;
  text-align: center;
  padding: 1px 7px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.14);
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  opacity: 0.85;
}

.lb-score {
  justify-self: end;
  min-width: 4.2em;
  text-align: right;
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* first run: name entry */

.lb-setup {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 9px 10px;
  border-radius: 5px;
  background: var(--cell-empty);
}

.lb-setup label {
  font-size: 12px;
  color: var(--ink-muted);
}

.lb-setup-row {
  display: flex;
  gap: 6px;
}

.lb-setup input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 7px 10px;
  border: 1px solid var(--rule, rgba(119, 110, 101, 0.3));
  border-radius: 4px;
  background: var(--ground);
  color: var(--ink);
  font-family: inherit;
  font-size: 14px;
}

.lb-setup input:focus-visible {
  outline: 2px solid var(--button);
  outline-offset: 1px;
}

.lb-setup button {
  flex: 0 0 auto;
  padding: 0 16px;
  border: 0;
  border-radius: 4px;
  background: var(--button);
  color: var(--ink-light);
  font-family: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}

/* ---- win / lose overlay ---- */

.game-message {
  position: absolute;
  inset: 0;
  z-index: 100;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 26px;
  border-radius: var(--radius-board);
  background: var(--overlay);
  text-align: center;
  animation: fade-in 800ms ease 1200ms;
  animation-fill-mode: both;
}

.game-message p {
  margin: 0;
  font-size: calc(var(--board) * 0.125);
  font-weight: 700;
}

.game-message .lower {
  display: flex;
  gap: 10px;
}

.game-message a {
  padding: 0 20px;
  border-radius: 4px;
  background: var(--button);
  color: var(--ink-light);
  font-size: 15px;
  font-weight: 700;
  line-height: 46px;
  text-decoration: none;
  cursor: pointer;
}

.game-message a:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 2px;
}

.game-message a.keep-playing-button {
  display: none;
}

.game-message.game-won {
  background: var(--overlay-won);
  color: var(--ink-light);
}

.game-message.game-won a.keep-playing-button {
  display: inline-block;
}

.game-message.game-won,
.game-message.game-over {
  display: flex;
}

@keyframes fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

/* ---- footer ---- */

/* The game, tile and auto-fill switches sit under the board, and under the
   leaderboard when there is one — a choice of game and preferences, not moves.
   The game switch is plain links, since choosing a game is going to another
   page. There is no credit line: the MIT notice ships in LICENSE.txt. */
.game-switch,
.palette-switch {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.palette-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.game-switch .seg,
.palette-switch .seg {
  display: inline-flex;
  padding: 2px;
  border-radius: 999px;
  background: var(--cell-empty);
}

.game-switch .seg a,
.palette-switch .seg button {
  padding: 4px 12px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-muted);
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
  text-decoration: none;
  transition: background-color 160ms ease, color 160ms ease;
}

.game-switch .seg a[aria-current="page"],
.palette-switch .seg button[aria-pressed="true"] {
  background: var(--board-bg);
  color: var(--ink-light);
}

.game-switch .seg a:focus-visible,
.palette-switch .seg button:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* Landscape: reclaim the chrome so the board keeps its size. */
@media (max-height: 560px) {
  /* Restated for :has(.leaderboard), which outranks a bare :root and would
     otherwise keep reserving room for a leaderboard this block hides. */
  :root,
  :root:has(.leaderboard) {
    --chrome: 150px;
  }
  .container {
    gap: 8px;
  }
  .game-switch,
  .palette-switch,
  .leaderboard {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tile,
  .tile .tile-inner,
  .game-message,
  .score-addition,
  .restart-button {
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
  }
}
