/* D6 web prototype — top-down board with a real 3D CSS die that tumbles.
   Visual layer only; all game logic lives in js/core (DOM-free). */

/* --- Theme ------------------------------------------------------------------
   Palette mirrors okmotorcycles.com: black + white + one accent that flips with
   the theme — freeway green by day, work-zone orange by night, never both.
   Light is the default; dark is opt-in via data-theme="dark" on <html>, set by
   the boot script in index.html from the site's shared `theme` key.

   Because hue is down to a single accent, tile types are told apart by sign
   face (white vs inverted), keyline style (hairline / solid / dotted / dashed /
   hatched) and their glyph — the highway-sign grammar — rather than by colour. */
:root {
  --cell: 86px;   /* tile footprint AND die size (1:1, like the original) */
  --gap: 0px;     /* tiles touch; separation is drawn with an inset ring */
  --cube: 86px;
  --half: 43px;   /* half of --cube */
  --roll-ms: 260ms;

  /* --- Camera ----------------------------------------------------------------
     A pitched, axis-aligned three-quarter view: rotateX(52deg) with a yaw of
     ZERO. The camera looks straight down a grid axis, so the die presents its
     top face and the one face turned toward the viewer, and its east and west
     faces are exactly edge-on — they project to nothing and cannot be read at
     all. Turning the camera with SPACE is the only way to bring them into view.
     Hiding two of the die's faces behind a deliberate action is the point.

     This is NOT isometric, and the difference is the yaw, not the pitch: a 45deg
     yaw is what makes a view isometric (all three cube axes foreshortened
     equally, two side faces permanently in sight), which is precisely the
     property this view is rejecting. 52deg pitch is the original board tilt —
     enough of the top face to read the pips the task tiles score, enough of the
     front face for the die to read as a cube rather than a sticker.

     --yaw is the ONLY thing SPACE changes. It accumulates (0, -90, -180, -270,
     ...) instead of wrapping at 360, so every press continues turning the same
     way round instead of unwinding three quarters of a turn.

     BOTH angles live on .camera. They must NEVER be moved onto .board: the
     die's tumble (ROLL_ROT / SPIN_ROT / EDGE_ORIGIN in js/ui/render.js) is
     expressed in the board's own frame and is in lockstep with the pure
     permutations in js/core/dice.js. .board stays an unrotated plane; the
     camera is a separate wrapper above it, so the die maths is untouched and
     stays correct at every stop for free. */
  /* Pitch only — the yaw starts at zero so the camera faces straight down a
     grid axis and the die's side faces are edge-on until SPACE turns them into
     view. 52deg is the original board tilt: enough of the top face to read the
     pips that task tiles score, enough of the front face to read as a cube. */
  --tilt: 52deg;
  --yaw: 0deg;
  --cam-ms: 420ms;
  --cam-ease: cubic-bezier(0.35, 0.02, 0.15, 1);

  /* --- The camera is anchored on the DIE, not the board ----------------------
     --die-px/--die-py are the die's CELL centre in board space, measured from
     the board's centre. The camera transform reads

         translate(anchor) . rotateX(tilt) . rotateZ(yaw) . translate(-die)

     so the die's cell is carried onto the rotation origin, turned there, and
     then dropped at the anchor: the die is the PIVOT and the board swings
     around it. Crucially the anchor does not depend on --yaw, so SPACE changes
     rotateZ and nothing else and the die is pinned to the pixel through a
     whole turn. aimCamera in js/ui/render.js drives the pivot, one cell at a
     time. */
  --die-px: 0px;
  --die-py: 0px;

  /* Uniform zoom applied to the camera so a board that is ~1.4x wider once it
     is turned 45deg still fits a narrow embed (the game is iframed into a note
     page). Set from JS; perspective is scaled by the same factor so the zoom
     is exact rather than a change of projection. */
  --fit: 1;
  --persp: 1900px;

  /* Board footprint and the stage box that holds its projection; both are set
     from JS per level (see sizeStage in js/ui/render.js). */
  --board-w: 430px;
  --board-h: 430px;
  --stage-w: 660px;
  --stage-h: 470px;
  /* THE ANCHOR: the screen point, relative to the stage's centre, that the die
     is held at. Measured by sizeStage in js/ui/render.js as the offset that
     centres the board's projection in the stage — a die standing on the board's
     centre should sit in the middle of the picture, and everything else follows
     from moving the pivot out from under it. Constant for the life of a level;
     see the framing notes in js/ui/render.js for why it must not track --yaw. */
  --cam-ax: 0px;
  --cam-ay: 0px;

  --bg: #f2f1ea;          /* page ground (site --bg) */
  --bg-2: #fafafa;        /* raised panels: picker, win card */
  --ink: #17181b;
  --muted: #83847a;
  --line: #dcdbd0;        /* hairline borders */

  --face: #fafafa;        /* the default white sign face a tile is cut from */
  --sign-invert: #17181b;      /* the "black sign" face (portals) */
  --sign-invert-ink: #f3f3ec;

  --accent: #157a43;           /* highway green */
  --accent-hover: #0e5e33;
  --accent-bright: #34a95b;
  --accent-soft: rgba(21, 122, 67, 0.14);
  --accent-glow: rgba(52, 169, 91, 0.5);
  --on-accent: #fafafa;        /* text/pips sitting on a filled accent face */

  /* The die is always the inverse of the tile faces, so it reads on any board. */
  --die: #17181b;
  --die-2: #26282e;
  --die-edge: #000;
  --pip: #f3f3ec;
  --pip-shade: #c9c9c0;

  --tile-shade: rgba(0, 0, 0, 0.10);
  --drop: rgba(0, 0, 0, 0.18);
  --scrim: rgba(30, 30, 26, 0.45);
}

:root[data-theme="dark"] {
  --bg: #16171b;
  --bg-2: #1f2027;
  --ink: #ecece4;
  --muted: #6f7168;
  --line: #2c2e37;

  --face: #1f2027;
  --sign-invert: #ecece4;
  --sign-invert-ink: #16171b;

  --accent: #f5852b;           /* work-zone orange */
  --accent-hover: #ff9d4e;
  --accent-bright: #ff9838;
  --accent-soft: rgba(245, 133, 43, 0.18);
  --accent-glow: rgba(255, 152, 56, 0.45);
  --on-accent: #16171b;

  --die: #fafafa;
  --die-2: #e4e4dc;
  --die-edge: #ffffff;
  --pip: #17181b;
  --pip-shade: #4a4b52;

  --tile-shade: rgba(0, 0, 0, 0.30);
  --drop: rgba(0, 0, 0, 0.5);
  --scrim: rgba(10, 12, 18, 0.6);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  /* The camera holds the die still and lets the board slide, and past the room
     the stage reserves for that (FRAME_ALLOW in js/ui/render.js) the board's far
     edge runs out of frame. Off-camera has to mean off-camera: without this the
     overhang widens the document and the page — or, embedded, the iframe — grows
     a horizontal scrollbar over a board the player cannot usefully scroll to.
     Kept off .stage and everything below it: overflow is a grouping property
     there and would flatten the die. */
  overflow-x: hidden;
  /* Repeated tapping on a game surface should not select text, flash a grey
     highlight box, or trigger double-tap-to-zoom. */
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

#app {
  min-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  padding: 28px 16px 48px;
}

/* ---- HUD ---- */
.hud {
  display: flex;
  align-items: baseline;
  gap: 18px;
  flex-wrap: wrap;
  justify-content: center;
}
.hud .title {
  font-weight: 700;
  letter-spacing: 0.14em;
  font-size: 20px;
}
.hud .title span { color: var(--accent); }
.hud .level { color: var(--muted); }
.hud .tasks { color: var(--ink); }
.hud .tasks.done { color: var(--accent); font-weight: 600; }
.hud .moves { color: var(--muted); font-variant-numeric: tabular-nums; }
/* Medal disc: the tier the run is still on course for. Sits with the move
   counter because moves are what it is scored on. */
.hud .medal { align-self: center; }
.medal {
  width: 11px;
  height: 11px;
  border-radius: 50%;
}
.medal.lg { width: 16px; height: 16px; }
/* Flat single colours, no rim and no gradient. Each is picked dark enough to
   hold its own against the pale board without needing an outline — platinum in
   particular is a cool grey rather than the near-white it "should" be, because
   near-white on an off-white page is not a disc, it is nothing. */
.medal.platinum { background: #9fb3c0; }
.medal.gold     { background: #d8a227; }
.medal.silver   { background: #98a2a9; }
.medal.bronze   { background: #a86a34; }
.medal.unranked { background: #c4c4bc; }

.hud .time { color: var(--ink); font-variant-numeric: tabular-nums; }
.hud .hint { color: var(--muted); font-size: 13px; }
.hud .back,
.hud .retry {
  font: inherit;
  font-size: 13px;
  padding: 4px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--bg-2);
  color: var(--ink);
  cursor: pointer;
  touch-action: manipulation;
}
.hud .back:hover,
.hud .retry:hover { border-color: var(--accent); }
.hud .retry { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }

/* ---- Level picker ---- */
.picker {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: center;
}
.picker button {
  font: inherit;
  font-size: 13px;
  color: var(--muted);
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 5px 14px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.picker button:hover { color: var(--accent); border-color: var(--accent); }
.picker button.current {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
  font-weight: 600;
}

/* ---- Stage / camera / board ---- */
/* .stage is a plain, non-transformed box big enough to hold the board's
   projection at ANY camera stop. Because the four stops are 45deg apart on a
   rectangle, the projected footprint is identical at all four (a w x h board
   turned 45, 135, 225 or 315 degrees has the same axis-aligned bounding box,
   (w+h)/sqrt(2) square), so the layout never resizes and the view never lurches
   between stops. perspective-origin sits at the dead centre of that box for the
   same reason. */
.stage {
  position: relative;
  width: calc(var(--stage-w) * var(--fit));
  height: calc(var(--stage-h) * var(--fit));
  perspective: calc(var(--persp) * var(--fit));
  perspective-origin: 50% 50%;
}

/* THE CAMERA. Everything the player can look at from a different angle hangs
   below this element, and only this element carries the view rotation.
   scale3d is a uniform zoom (x, y AND z), and .stage scales its perspective by
   the same --fit, so shrinking to fit a small embed is an exact zoom of the
   same picture rather than a different projection.
   NEVER give this element opacity < 1, a filter, a clip-path or a mask: per CSS
   Transforms those are grouping properties and force transform-style: flat,
   which would collapse the die into the board plane.
   The element is board-sized and dead centre in the stage with no margin
   offset, so its transform-origin (50% 50%) sits exactly on the stage's centre,
   which is also the perspective-origin. Everything that shifts the view lives
   in the transform instead, which means --fit is an exact zoom about that one
   point rather than a scale about a nudged origin.
   Read the transform right to left: -die puts the die's cell on the origin, the
   two rotations turn the board about it, and the anchor places it on screen.
   The transition is set inline per movement by js/ui/render.js — a camera TURN
   and a cell-to-cell PAN want different durations — so the value here is only
   the fallback. */
.camera {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--board-w);
  height: var(--board-h);
  margin-left: calc(var(--board-w) / -2);
  margin-top: calc(var(--board-h) / -2);
  transform-style: preserve-3d;
  transform: scale3d(var(--fit), var(--fit), var(--fit))
             translate3d(var(--cam-ax), var(--cam-ay), 0)
             rotateX(var(--tilt))
             rotateZ(var(--yaw))
             translate3d(calc(-1 * var(--die-px)), calc(-1 * var(--die-py)), 0);
  transition: transform var(--cam-ms) var(--cam-ease);
}

/* The board itself is deliberately UNROTATED. Its local axes are the grid axes
   the engine speaks in, and the die's tumble is written in this frame. */
.board {
  position: relative;
  width: var(--board-w);
  height: var(--board-h);
  transform-style: preserve-3d;
  transition: width 0.2s, height 0.2s;
}

/* Each grid cell. Floor tiles get a raised 3D lip via a pseudo-element. */
.tile {
  position: absolute;
  width: var(--cell);
  height: var(--cell);
  border: 3px solid transparent;
  background-clip: padding-box; /* transparent border reads as a seam between tiles */
  border-radius: 14px;
  background-color: var(--face);
  box-shadow: inset 0 0 0 1px var(--line), inset 0 0 16px var(--tile-shade);
  transform-style: preserve-3d;
}
.tile.empty { background: none; box-shadow: none; }

.tile.start::after {
  content: "";
  position: absolute;
  inset: 26%;
  border-radius: 50%;
  border: 3px dashed var(--muted);
  opacity: 0.7;
}

/* Goal: chequered flag while locked, then the accent lights it up. */
.tile.goal {
  background: repeating-conic-gradient(var(--face) 0deg 90deg, var(--muted) 90deg 180deg) 50% / 24px 24px;
  background-clip: padding-box;
  box-shadow: inset 0 0 0 3px var(--ink), inset 0 0 16px var(--tile-shade);
}
.tile.goal.live {
  box-shadow: inset 0 0 0 3px var(--accent), 0 0 26px 2px var(--accent-glow);
  animation: goalPulse 1.4s ease-in-out infinite;
}
@keyframes goalPulse {
  50% { box-shadow: inset 0 0 0 3px var(--accent-bright), 0 0 40px 6px var(--accent-glow); }
}

/* Task tiles show a required face value: a regulatory sign (heavy ink keyline)
   that flips to a filled accent face once satisfied. */
.tile.task {
  background-color: var(--face);
  box-shadow: inset 0 0 0 3px var(--ink), inset 0 0 16px var(--tile-shade);
}
.tile.task.cleared {
  background-color: var(--accent-soft);
  box-shadow: inset 0 0 0 3px var(--accent), inset 0 0 16px var(--tile-shade);
}
.tile .req {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  transform-style: preserve-3d;
}
/* Stacked above the mini die rather than pinned to a corner: the label box is
   counter-rotated against the camera (see "Upright labels" below), and a corner
   anchor swings outside the tile at the 45deg stops. Centred content stays
   within the tile's inscribed square at every angle. */
.tile .req .badge {
  font-size: 11px;
  line-height: 1;
  letter-spacing: 0.1em;
  color: var(--ink);
  opacity: 0.85;
}
.tile.task.cleared .req .mini-die { opacity: 0.3; }
.tile.task.cleared .req .badge { color: var(--accent); opacity: 1; font-weight: 700; }
.tile.task.cleared .req .badge::after { content: " ✓"; }

/* Mini die face drawn on a task tile to show the target orientation. */
.mini-die {
  width: 46px;
  height: 46px;
  border-radius: 9px;
  background: var(--die);
  box-shadow: inset 0 0 0 2px var(--die-edge);
  display: grid;
  grid-template: repeat(3, 1fr) / repeat(3, 1fr);
  padding: 7px;
  gap: 2px;
}
.mini-die .pip { width: 8px; height: 8px; border-radius: 50%; background: var(--pip); align-self: center; justify-self: center; }

/* ---- Action tiles: move (conveyor), spin, portal ---- */
.tile .glyph {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 40px;
  line-height: 1;
  color: var(--ink);
  opacity: 0.9;
}
/* Conveyor: work-zone hatching, the one place stripes appear. */
.tile.move {
  background-color: var(--face);
  background-image: repeating-linear-gradient(
    -45deg, var(--accent-soft) 0 10px, transparent 10px 20px);
  background-clip: padding-box;
  box-shadow: inset 0 0 0 2px var(--accent), inset 0 0 16px var(--tile-shade);
}
.tile.move .arrow { color: var(--accent); }

/* Spin: same white face, dotted keyline — "turn in place". */
.tile.spin {
  background-color: var(--face);
  box-shadow: inset 0 0 0 2px var(--muted), inset 0 0 16px var(--tile-shade);
}
.tile.spin::after {
  content: "";
  position: absolute;
  inset: 7px;
  border: 2px dotted var(--ink);
  border-radius: 9px;
  opacity: 0.55;
}
.tile.spin .glyph { color: var(--ink); }

/* Portals: the inverted sign face, so a pair reads instantly across the board.
   The two ends differ by ring style, not colour (see .portal-mark). */
.tile.portal {
  background-color: var(--sign-invert);
  box-shadow: inset 0 0 0 2px var(--ink), inset 0 0 18px var(--tile-shade);
}
.tile.portal-a { background-color: var(--sign-invert); }
.tile.portal-b { background-color: var(--sign-invert); }
.portal-mark {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}
.portal-mark i {
  position: absolute;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 3px solid currentColor;
  opacity: 0.7;
  animation: portalSpin 3s linear infinite;
}
/* Both ends carry the accent; A is a solid outer ring, B a dashed one. */
.portal-mark { color: var(--accent-bright); }
.portal-b .portal-mark i { border-style: dashed; }
.portal-mark i::before {
  content: "";
  position: absolute;
  inset: 6px;
  border-radius: 50%;
  border: 2px dashed currentColor;
}
.portal-b .portal-mark i::before { border-style: solid; }
.portal-mark b {
  position: relative;
  font-size: 15px;
  font-weight: 700;
  color: var(--sign-invert-ink);
}
@keyframes portalSpin { to { transform: rotate(360deg); } }

/* ---- Upright labels -------------------------------------------------------
   Tile content is painted on the board plane, so it rides the camera and ends
   up sideways or upside down at the 90/180/270 stops. Cancelling the camera's
   yaw (and only the yaw — the tilt is kept so the label still lies flat on the
   board) holds every readable thing upright at all four stops. They share the
   camera's duration and easing so label and board turn as one movement. */
.tile .req,
.tile .glyph,
.tile .portal-mark {
  transform: rotateZ(calc(-1 * var(--yaw)));
  transform-origin: 50% 50%;
  transition: transform var(--cam-ms) var(--cam-ease);
}
/* ...with one deliberate exception. A conveyor arrow is not a label, it is a
   DIRECTION: it names the neighbouring cell the tile shoves the die into. Held
   "upright" it would point at the wrong cell as soon as the camera turned, so
   it rides the board like the tiles do and keeps telling the truth. An arrow
   has no upright to lose. */
.tile.move .glyph.arrow {
  transform: none;
}
/* The portal ring keeps its own spin animation; only its wrapper is
   counter-rotated, so the digit inside stays readable. */

/* ---- Level enter animation ---- */
.tile.enter { animation: enterTile 0.4s ease-out both; }
@keyframes enterTile {
  from { opacity: 0; transform: translateZ(-40px) scale(0.6); }
  to { opacity: 1; transform: none; }
}

/* ---- The die ---- */
/* .die is positioned at its cell and lifted so the cube rests on the board.
   .roller carries the per-roll edge-pivot (transform-origin at the leading
   bottom edge, set inline). .cube holds the accumulated orientation. */
.die {
  position: absolute;
  width: var(--cube);
  height: var(--cube);
  transform-style: preserve-3d;
  transform: translateZ(var(--half)); /* board-local +Z = world up */
  pointer-events: none;
  z-index: 5;
}
.roller {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}
.cube {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

/* The die's shadow: a single soft patch lying flat on the BOARD plane, pushed
   back down the height the .die is lifted by so it sits on the floor rather than
   riding at cube height. It never rotates with the cube — a shadow on the ground
   does not tumble — and it stays put during a roll, which reads correctly since
   the cube is pivoting about its contact edge. */
.die-shadow {
  position: absolute;
  inset: 7%;
  transform: translateZ(calc(-1 * var(--half) + 1px));
  background: radial-gradient(closest-side, var(--drop), transparent 76%);
  border-radius: 46%;
  pointer-events: none;
}

.face {
  position: absolute;
  width: var(--cube);
  height: var(--cube);
  border-radius: 13px;
  background: linear-gradient(155deg, var(--die), var(--die-2));
  /* Rim only. A drop shadow here is drawn in each FACE's own plane, so with the
     camera square-on to the board the top face casts its shadow straight down
     across the front face — a dark bar sitting on the die like a mouth. The
     die's shadow belongs on the ground, not on its own faces: see .die-shadow. */
  box-shadow: inset 0 0 0 2px var(--die-edge);
  display: grid;
  grid-template: repeat(3, 1fr) / repeat(3, 1fr);
  padding: 13px;
  gap: 3px;
  backface-visibility: hidden;
}
.face .pip {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--pip), var(--pip-shade));
  align-self: center;
  justify-self: center;
}

/* Pip placement by grid cell (1..9), classic die layouts. */
.p1 { grid-area: 1 / 1; } .p2 { grid-area: 1 / 2; } .p3 { grid-area: 1 / 3; }
.p4 { grid-area: 2 / 1; } .p5 { grid-area: 2 / 2; } .p6 { grid-area: 2 / 3; }
.p7 { grid-area: 3 / 1; } .p8 { grid-area: 3 / 2; } .p9 { grid-area: 3 / 3; }

/* Face placement in the die's body frame. Top = board-local +Z (world up);
   front faces board-local -Y (screen up / forward). */
.face.top    { transform: translateZ(var(--half)); }
.face.bottom { transform: rotateX(180deg) translateZ(var(--half)); }
.face.front  { transform: rotateX(90deg)  translateZ(var(--half)); }
.face.back   { transform: rotateX(-90deg) translateZ(var(--half)); }
.face.right  { transform: rotateY(90deg)  translateZ(var(--half)); }
.face.left   { transform: rotateY(-90deg) translateZ(var(--half)); }

/* ---- Touch controls ----
   Hidden wherever there is a real pointer; the keyboard is better there and the
   cluster would only take up room. `pointer: coarse` with no hover is the pair
   that actually means "finger", rather than guessing from viewport width. */
.pad { display: none; }

@media (pointer: coarse) and (hover: none) {
  .pad {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 18px;
    position: sticky;
    bottom: 0;
    padding: 10px 12px calc(10px + env(safe-area-inset-bottom, 0px));
    background: linear-gradient(to top, var(--bg) 72%, transparent);
  }
  .dpad {
    display: grid;
    grid-template-areas: ". u ." "l d r";
    gap: 6px;
  }
  .dpad [data-touch="up"] { grid-area: u; }
  .dpad [data-touch="left"] { grid-area: l; }
  .dpad [data-touch="down"] { grid-area: d; }
  .dpad [data-touch="right"] { grid-area: r; }
  .pad-btn {
    /* 46px: above the 44pt minimum, with the gaps keeping the gutters honest. */
    width: 46px;
    height: 46px;
    font: 20px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    color: var(--ink);
    background: var(--bg-2);
    border: 1px solid var(--line);
    border-radius: 12px;
    touch-action: manipulation;
  }
  .pad-btn:active { background: var(--accent); border-color: var(--accent); color: #fff; }
  /* A tap should not leave a focus ring sitting on the button afterwards, but
     keyboard focus still has to be visible. */
  .pad-btn:focus { outline: none; }
  .pad-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

  /* Centre the board in what is left between the HUD and the controls. The pad
     is pushed to the bottom of the flex column, and without this the board stays
     pinned under the HUD with the whole difference — measured at 358px on a
     390x844 phone — as dead space between them. */
  .stage { margin-block: auto; }
  .pad-btn.rotate, .pad-btn.restart { border-radius: 50%; font-size: 19px; }

  /* The hints name keys a phone does not have. */
  .hud .hint { display: none; }
  .hud { gap: 12px; font-size: 14px; }
}

/* ---- Win banner ---- */
.banner {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--scrim);
  backdrop-filter: blur(3px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s;
}
.banner.show { opacity: 1; }
.banner .card {
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 26px 34px;
  text-align: center;
  box-shadow: 0 24px 60px var(--drop);
}
/* The scrim ignores pointer events so the board stays live behind it; the card
   has to opt back IN or its buttons are unclickable. */
.banner .card { pointer-events: auto; }
.banner .card h2 { margin: 0 0 6px; color: var(--accent); letter-spacing: 0.08em; }
.banner .card p { margin: 0; color: var(--muted); }
.banner .award {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 0 0 10px;
  color: var(--ink);
  font-size: 13px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.banner .actions { display: flex; gap: 10px; justify-content: center; margin-top: 16px; }
.banner .actions button {
  font: inherit;
  padding: 7px 18px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--bg-2);
  color: var(--ink);
  cursor: pointer;
}
.banner .actions button:hover { border-color: var(--accent); }
.banner .actions button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}
