:root {
  --bg: #2e1c12;
  --panel: #3d2618;
  --panel2: #56341f;
  --edge: #d9a45c;
  --edge2: #8a5a30;
  --ink: #f7ecd6;
  --ink2: #cdb48f;
  --gold: #f2c231;
  --sand: #c8402f;
  --sand-edge: #ef8a72;
  --sand-shadow: #7d1f16;
  --shadow: #24150d;
  --font: ui-monospace, Menlo, Consolas, monospace;
  --safe-top: env(safe-area-inset-top);
  --safe-bottom: env(safe-area-inset-bottom);
  --tap: 44px;
  /* How wide a landscape rail is, and how far in from its edge it sits. The inset is
     written by Game.layout once the board has been sized, so on a wide screen the rails
     hug the board instead of pinning themselves to the corners. */
  --rail-w: clamp(108px, 15vw, 190px);
  --rail-in: 10px;
  --rail-timer-top: 160px;
  --die: clamp(42px, 12vw, 54px);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  padding: 0;
  overflow: hidden;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}

canvas#game {
  position: fixed;
  inset: 0;
  z-index: 0;
}

/* There was not one focus style in this stylesheet, and Hud.blur() drops focus after every
   press — so the only person who could ever see a ring was somebody using the keyboard, and
   they saw nothing at all. That matters most on the menu, where the name field is focused
   from script: a caret in an empty box on a dark card is not an answer to "type here".

   :focus-visible rather than :focus, so a pointer press does not leave a ring behind it. */
:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 2px;
}

.mname:focus-visible { outline-offset: 0; }

/* home-link:start */
.home-link {
  position: fixed;
  z-index: 12;
  left: 10px;
  top: calc(var(--safe-top) + 8px);
  color: var(--ink2);
  font: 700 clamp(10px, 2.6vw, 12px)/1 var(--font);
  letter-spacing: .08em;
  text-decoration: none;
  opacity: .7;
}
/* home-link:end */

/* #hud is pointer-events:none, so nothing clickable may live inside it: a nested button
   inherits it and becomes invisibly dead. The mute, the menu pill and the controls are
   siblings. */
#hud {
  position: fixed;
  z-index: 10;
  left: 10px;
  top: calc(var(--safe-top) + 28px);
  pointer-events: none;
  text-shadow: 0 2px 0 #24150d;
  max-width: 58vw;
}

/* Both of these are written by the loop and are EMPTY when Game.layout measures the
   HUD to decide where the landscape turn timer hangs. Reserving their line box makes the
   HUD's height independent of whether its text has arrived yet — without it the timer was
   placed against a HUD two lines shorter than the one that existed a frame later, and
   landed on top of the roster. */
#turnLine {
  font: 800 clamp(12px, 3.4vw, 15px)/1.5 var(--font);
  letter-spacing: .06em;
  min-height: 1.5em;
}

#roster { margin-top: 2px; }

.rrow {
  display: flex;
  align-items: center;
  gap: 6px;
  font: 700 clamp(10px, 2.8vw, 12px)/1.6 var(--font);
  color: var(--ink2);
  opacity: .68;
}

.rrow.now { opacity: 1; color: var(--ink); }

/* Left the match. Struck through rather than hidden: their piece is still on the board and
   the ladders and snakes they built are still working, so the roster has to keep explaining
   a colour that is still in play. */
.rrow.left { opacity: .38; text-decoration: line-through; }
.rrow.left.now { opacity: .38; }

/* Dropped, but the grace is still running and they may yet come back. Dimmed and italic
   rather than struck through — struck through is for gone, and the difference matters to
   somebody deciding whether to keep waiting. */
.rrow.away { opacity: .5; font-style: italic; }

.rdot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex: none;
}

.rname {
  max-width: 8ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rcount { color: var(--gold); font-weight: 800; }

#tally {
  margin-top: 3px;
  font: 700 clamp(9px, 2.4vw, 11px)/1.4 var(--font);
  color: #b08f68;
  letter-spacing: .1em;
  min-height: 1.4em;
}

#timerWrap {
  position: fixed;
  z-index: 11;
  left: 50%;
  transform: translateX(-50%);
  top: calc(var(--safe-top) + 8px);
  width: clamp(88px, 28vw, 128px);
  height: clamp(16px, 4vw, 20px);
  background: rgba(46, 28, 18, .55);
  border: 2px solid var(--edge);
  border-radius: 10px;
  overflow: hidden;
  pointer-events: none;
  display: none;
  align-items: center;
}

#timerWrap.show { display: flex; }

#timerBar {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  background: #5fd977;
}

#timerBar.mid { background: #f4c94f; }
#timerBar.low { background: #ff6b5c; }

#timerNum {
  position: relative;
  z-index: 1;
  width: 100%;
  text-align: center;
  font: 800 clamp(9px, 2.4vw, 11px)/1 var(--font);
  color: #0e1710;
  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
}

.mini {
  position: fixed;
  z-index: 15;
  width: clamp(34px, 9vw, 42px);
  height: clamp(34px, 9vw, 42px);
  border-radius: 50%;
  background: rgba(61, 38, 24, .72);
  border: 2px solid var(--edge);
  color: var(--ink);
  font: 700 clamp(13px, 3.6vw, 17px)/1 var(--font);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 0 var(--shadow);
  cursor: pointer;
}

.mini:active { transform: translateY(2px); box-shadow: none; }

#mute { right: 8px; top: calc(var(--safe-top) + 8px); }
#menuBtn { right: 8px; top: calc(var(--safe-top) + 8px + clamp(40px, 10.5vw, 50px)); font-size: clamp(9px, 2.4vw, 11px); letter-spacing: .04em; }

/* Positioned from script: both sit clear of a board whose height is decided at run time.

   Both carry a FIXED height of one line box, not a min-height, and for the same reason
   #turnLine reserves its own: Game.layout MEASURES them to size the landscape strip above
   the board. An empty one collapses and a long one wraps, so with either left free the
   board moved every time a message appeared, cleared, or ran a few characters longer —
   enough to change a tile size mid-turn. A fixed height cannot do that. It is also why a
   long line clips rather than wraps here: a strip that reflowed the board would be worse
   than one that loses a word. */
#status {
  position: fixed;
  z-index: 11;
  left: 0;
  right: 0;
  text-align: center;
  pointer-events: none;
  height: 1.25em;
  overflow: hidden;
  font: 800 clamp(13px, 4.4vw, 20px)/1.2 var(--font);
  color: var(--ink);
  text-shadow: 0 2px 0 #241408;
  padding: 0 10px;
  opacity: 0;
  transition: opacity .18s ease;
}

#status.show { opacity: 1; }

#popup {
  position: fixed;
  z-index: 12;
  left: 50%;
  transform: translateX(-50%);
  max-width: min(92vw, 420px);
  height: calc(1.3em + 18px);
  overflow: hidden;
  padding: 7px 16px;
  border-radius: 999px;
  background: rgba(10, 12, 28, .88);
  border: 2px solid var(--gold);
  color: #fff;
  font: 700 clamp(11px, 3vw, 14px)/1.3 var(--font);
  text-align: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity .18s ease;
}

#popup.show { opacity: 1; }

/* The control stack. pointer-events is off on the wrapper and turned back on for the
   buttons, so the dead space around them does not eat taps meant for the board. */
#ctr {
  position: fixed;
  z-index: 10;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 8px 12px calc(var(--safe-bottom) + 10px);
  pointer-events: none;
}

#hint {
  color: var(--ink2);
  font: 700 clamp(9px, 2.6vw, 12px)/1 var(--font);
  letter-spacing: .04em;
  min-height: 0;
}

#tools {
  display: flex;
  gap: 10px;
  align-items: center;
}

.btn {
  pointer-events: auto;
  border: 2px solid var(--edge);
  border-radius: 18px;
  background: var(--panel2);
  color: var(--ink);
  font: 800 clamp(12px, 3.4vw, 16px)/1 var(--font);
  letter-spacing: .04em;
  padding: 0 12px;
  min-width: clamp(76px, 24vw, 104px);
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  box-shadow: 0 3px 0 var(--shadow);
  cursor: pointer;
}

.btn:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--shadow); }
.btn.off { opacity: .38; }
.btn.on { background: #c8402f; border-color: #f2c231; }

.bicn { font-size: clamp(17px, 5vw, 22px); }

.btn.big {
  min-width: clamp(112px, 36vw, 168px);
  min-height: clamp(62px, 15vw, 76px);
  border-radius: 22px;
  background: var(--sand);
  border-color: var(--sand-edge);
  box-shadow: 0 3px 0 var(--sand-shadow);
  padding: 0;
}

.btn.big:active { box-shadow: 0 1px 0 var(--sand-shadow); }

/* The die is a real cube.

   --die is one value used for every axis of it, so it cannot come out of a tier oblong,
   and the faces sit half of it away from the centre. */
#diceBox {
  position: relative;
  width: var(--die);
  height: var(--die);
  perspective: calc(var(--die) * 4);
}

/* The shadow the cube cannot cast for itself. It lives on the perspective container, so
   it stays flat on the button while the die turns above it. */
#diceBox::after {
  content: '';
  position: absolute;
  left: 13%;
  right: 13%;
  bottom: -13%;
  height: 13%;
  border-radius: 50%;
  background: rgba(0, 0, 0, .3);
  filter: blur(3px);
}

/* The cube. Its transform is written from script — a resting tilt, then the rotation that
   brings the rolled face to the front — and --roll-t is how long it takes to get there.

   A custom property rather than an inline transition, so the reduced-motion tier can
   switch the whole thing off: an inline transition-duration would outrank a stylesheet. */
#diceDots {
  position: absolute;
  inset: 0;
  /* Stacked explicitly rather than floating the shadow to z-index -1: the shadow is a
     pseudo-element of this element's parent and the order of the two is worth stating. */
  z-index: 1;
  transform-style: preserve-3d;
  transition: transform var(--roll-t, 0s) cubic-bezier(.16, .62, .18, 1);
}

/* The corners are barely rounded on purpose. Six rounded rectangles meeting at right
   angles do not close: each pair leaves a notch at the shared corner and the button
   shows through it, and the notch grows with the radius. A real die of this size is
   nearly square-cornered anyway. */
.dface {
  position: absolute;
  inset: 0;
  border-radius: 9%;
  backface-visibility: hidden;
  background: radial-gradient(118% 128% at 30% 20%, var(--dl) 0%, var(--dm) 55%, var(--dd) 100%);
  --dl: #fffefa;
  --dm: #f7f0e1;
  --dd: #e7dbc5;
}

/* Opposite faces sum to seven: 1/6 front and back, 2/5 right and left, 3/4 top and
   bottom. Hud's FACE table is the inverse of this and the two have to agree. */
.f1 { transform: translateZ(calc(var(--die) / 2)); }
.f6 { transform: rotateY(180deg) translateZ(calc(var(--die) / 2)); }
.f2 { transform: rotateY(90deg) translateZ(calc(var(--die) / 2)); }
.f5 { transform: rotateY(-90deg) translateZ(calc(var(--die) / 2)); }
.f3 { transform: rotateX(90deg) translateZ(calc(var(--die) / 2)); }
.f4 { transform: rotateX(-90deg) translateZ(calc(var(--die) / 2)); }

/* Shading is baked into each face's own gradient rather than applied with `filter`,
   because a filter forces its element out of the parent's 3D context. */
.f2, .f5 { --dl: #f8f3e8; --dm: #ece2cd; --dd: #d8c9ae; }
.f3 { --dl: #fffffd; --dm: #fbf6ea; --dd: #eee3ce; }
.f4 { --dl: #efe7d7; --dm: #e0d4bb; --dd: #c9b797; }
.f6 { --dl: #f3ecde; --dm: #e6dac3; --dd: #d1c0a2; }

/* The pips are PLACED, not laid out.

   They used to be nine cells of a 3x3 grid with a percentage gap. That is square on
   paper and ragged on screen: a die is 42-54px, so a track works out at something like
   9.7px and the three of them round to 10, 9, 10 device pixels. Circles one pixel apart
   in diameter are visibly different circles, and their centres drift with them.

   Every pip is now the same two percentages of the same square box, so they round the
   same way, and the six positions are stated rather than derived.

   The face's inner margin is an INSET, not a padding. A percentage padding resolves
   against the containing block's width — the roll button's, not the die's — so the pip
   field was 12% of a button that is 112px on a phone and 168px on a desktop, while the
   die itself is 42-54px. It shrank as the button grew: at 412px wide the field came out
   14.8px inside a 49px die and the pips landed at 3px. */
.pip {
  position: absolute;
  width: 16%;
  height: 16%;
  /* Percentage margins resolve against the container's WIDTH on all four sides, and the
     face is square, so this centres the pip on its coordinate exactly. */
  margin: -8% 0 0 -8%;
  border-radius: 50%;
  background: transparent;
}

.pip:nth-child(1) { left: 28%; top: 28%; }
.pip:nth-child(2) { left: 50%; top: 28%; }
.pip:nth-child(3) { left: 72%; top: 28%; }
.pip:nth-child(4) { left: 28%; top: 50%; }
.pip:nth-child(5) { left: 50%; top: 50%; }
.pip:nth-child(6) { left: 72%; top: 50%; }
.pip:nth-child(7) { left: 28%; top: 72%; }
.pip:nth-child(8) { left: 50%; top: 72%; }
.pip:nth-child(9) { left: 72%; top: 72%; }

/* Flat. The old white inset highlight was a 1px band on a 10px circle, so it read as a
   different amount of pip on every one of them. */
.pip.on { background: #262b40; }

/* The cube does the turning; the container does the throw. Keeping them on separate
   elements is what lets the rotation run as one long transition to a known face while
   the hop and the settle stay ordinary CSS animations that can be swapped mid-flight.

   One hop cycle is 0.3s and CFG.diceSpin is 0.9, so three of them land exactly on the
   beat the cube stops and the handover to the settle starts from rest. The easing is per
   keyframe because a throw is ballistic: up fast, slow at the top, down accelerating. */
#diceBox.spin { animation: diceHop .3s linear infinite; }
#diceBox.land { animation: diceSet .34s linear; }

@keyframes diceHop {
  0% { transform: translateY(0) scale(1, 1); animation-timing-function: cubic-bezier(.1, .72, .34, 1); }
  34% { transform: translateY(-17%) scale(.97, 1.04); animation-timing-function: cubic-bezier(.5, 0, .86, .4); }
  82% { transform: translateY(0) scale(1.08, .92); animation-timing-function: cubic-bezier(.3, .6, .5, 1); }
  100% { transform: translateY(0) scale(1, 1); }
}

/* The die is already almost stopped when this runs, so it is a set rather than an
   impact: one flat, one small lift, done inside CFG.resultHold's 0.4s beat. */
@keyframes diceSet {
  0% { transform: scale(1, 1) translateY(0); animation-timing-function: cubic-bezier(.3, .8, .4, 1); }
  30% { transform: scale(1.15, .87) translateY(4%); animation-timing-function: cubic-bezier(.3, .7, .4, 1); }
  64% { transform: scale(.97, 1.04) translateY(-4%); animation-timing-function: cubic-bezier(.4, .6, .4, 1); }
  100% { transform: scale(1, 1) translateY(0); }
}

/* The face still arrives, it just arrives at once. */
@media (prefers-reduced-motion: reduce) {
  #diceDots { transition-property: none; }
  #diceBox.spin,
  #diceBox.land { animation: none; }
  #diceBox.spin { opacity: .84; }
}

/* The hand, as a popover ABOVE the control band rather than on top of it.

   `--ctr-h` is #ctr's measured height, written by the layout pass. Sharing the band's own
   bottom edge — which is what this did — laid the panel over the roll button and over the
   card button that opens it, so the one tap that closes it again was underneath the panel
   it had just opened. It covers board instead, which is the right thing for a transient
   panel to cover: the board is not going anywhere and the buttons are what you need next.

   It rises out of the button it belongs to, so where it came from is not a guess. */
#cardPanel {
  position: fixed;
  z-index: 14;
  left: 50%;
  bottom: calc(var(--ctr-h, 120px) + 8px);
  transform: translateX(-50%) translateY(6px);
  transform-origin: bottom center;
  width: min(340px, 90vw);
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 20px;
  box-shadow: 0 8px 22px rgba(20, 11, 6, .55);
  padding: 8px;
  display: none;
  flex-direction: column;
  gap: 8px;
  opacity: 0;
  transition: opacity .16s ease-out, transform .16s ease-out;
}

#cardPanel.open { display: flex; }
/* One frame after display flips, so the transition has a from-state to run out of. */
#cardPanel.up { opacity: 1; transform: translateX(-50%) translateY(0); }

.cardItem {
  width: 100%;
  text-align: left;
  padding: 9px 12px;
  border-radius: 14px;
  background: var(--panel2);
  border: 2px solid var(--edge2);
  color: var(--ink);
  font-family: var(--font);
  display: flex;
  flex-direction: column;
  gap: 2px;
  cursor: pointer;
}

.cardItem:active { transform: translateY(2px); }
.ci-head { font: 800 clamp(12px, 3.4vw, 14px)/1.2 var(--font); color: var(--gold); }
.ci-desc { font: 700 clamp(10px, 2.8vw, 12px)/1.3 var(--font); color: var(--ink2); }
.cardEmpty { color: var(--ink2); text-align: center; font-size: 13px; padding: 6px; }

/* The placement guide.

   It never covers the board and it never changes the height Game.layout measured for it.
   In portrait it takes over the control band exactly — --guide-top is that band's top
   edge, written by the layout pass — so the buttons you cannot use mid-placement are
   replaced by the words telling you what to do, and CANCEL is one tap. In landscape the
   left rail already has an empty column under the timer, which costs nothing at all. */
#guide {
  position: fixed;
  z-index: 16;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  left: 0;
  right: 0;
  top: auto;
  bottom: 0;
  min-height: var(--ctr-h, 30%);
  padding: 7px 16px calc(var(--safe-bottom) + 8px);
  background: linear-gradient(180deg, #4a2d1c, #33200f);
  border-top: 3px solid var(--gold);
  text-align: center;
}

#guide.open { display: flex; }
#guide.snk { border-top-color: #7fd07a; }

#guideHead {
  display: flex;
  align-items: center;
  gap: 8px;
  font: 900 clamp(13px, 4vw, 17px)/1.1 var(--font);
  color: var(--gold);
  letter-spacing: .06em;
}

#guide.snk #guideHead { color: #93cf6b; }
#guideIcon { font-size: clamp(17px, 5.4vw, 23px); }

#guideStep {
  font: 800 clamp(9px, 2.6vw, 11px)/1 var(--font);
  color: var(--ink2);
  letter-spacing: .18em;
}

#guideBody {
  font: 700 clamp(11px, 3.2vw, 14px)/1.35 var(--font);
  color: var(--ink);
  max-width: 34em;
}

#guideCancel {
  pointer-events: auto;
  margin-top: 1px;
  padding: 0 18px;
  min-height: var(--tap);
  border: 2px solid var(--edge2);
  border-radius: 14px;
  background: transparent;
  color: var(--ink2);
  font: 800 clamp(10px, 2.8vw, 12px)/1 var(--font);
  letter-spacing: .12em;
  cursor: pointer;
}

#guideCancel:active { transform: translateY(2px); }

/* Overlays: the game-over card and the menu are one pattern. The prototype auto-restarted
   after 2.6 seconds and had no way back to the menu short of a reload. */
.sheet {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 18px;
  overflow-y: auto;
  background: linear-gradient(160deg, #4a2d1c, #241509);
}

#over {
  display: none;
  background: rgba(8, 10, 24, .8);
}

#over.open { display: flex; }
#menu.gone { display: none; }

#overTitle {
  font: 900 clamp(24px, 9vw, 44px)/1.1 var(--font);
  letter-spacing: .04em;
  text-align: center;
  margin: 0 0 6px;
}

/* The card says how it went FOR THE PLAYER AT THIS SCREEN, not which seat won: it used to take
   the winner's seat colour, so against the CPU a win and a loss could come out the same red.
   Green and a pop for a win, red and a thud for a loss. */
#overTitle.win {
  color: #5fd977;
  animation: overWin .7s cubic-bezier(.2, 1.6, .4, 1) both, overGlow 1.8s ease-in-out .7s infinite;
}

#overTitle.lose {
  color: #ff6b5c;
  text-shadow: 0 3px 0 #4a1512;
  animation: overLose .6s ease-out both;
}

@keyframes overWin {
  0% { transform: scale(.3) rotate(-8deg); opacity: 0; }
  60% { transform: scale(1.15) rotate(2deg); opacity: 1; }
  100% { transform: scale(1) rotate(0); }
}

@keyframes overGlow {
  0%, 100% { text-shadow: 0 3px 0 #1d4a26, 0 0 0 rgba(95, 217, 119, 0); }
  50% { text-shadow: 0 3px 0 #1d4a26, 0 0 24px rgba(95, 217, 119, .8); }
}

@keyframes overLose {
  0% { transform: translateY(-22px); opacity: 0; }
  50% { transform: translateY(4px); opacity: 1; }
  66% { transform: translateX(-7px); }
  82% { transform: translateX(6px); }
  100% { transform: none; }
}

#overSub {
  font: 700 clamp(12px, 3.4vw, 15px)/1.4 var(--font);
  color: var(--ink2);
  margin-bottom: 20px;
  text-align: center;
  /* The loss line and the tally are two lines of one string. */
  white-space: pre-line;
}

#menu h1 {
  font: 900 clamp(19px, 6vw, 28px)/1.2 var(--font);
  color: var(--gold);
  margin: 0 0 20px;
  text-align: center;
  text-shadow: 0 2px 0 #24150d;
}

.mlabel {
  font: 700 clamp(10px, 2.8vw, 12px)/1 var(--font);
  color: var(--ink2);
  letter-spacing: .12em;
  margin-bottom: 7px;
  width: 100%;
  max-width: 340px;
}

.mrow {
  display: flex;
  gap: 9px;
  width: 100%;
  max-width: 340px;
  margin-bottom: 15px;
}

.mopt {
  flex: 1;
  padding: 12px 6px;
  border: 2px solid var(--edge2);
  border-radius: 16px;
  background: var(--panel);
  color: #e8d8bb;
  text-align: center;
  font: 700 clamp(12px, 3.4vw, 15px)/1 var(--font);
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.mopt.sel { background: #c8402f; border-color: #f2c231; color: #fff; }

/* Three modes share the row now and PASS-N-PLAY is the long one, so this row alone gets a
   smaller type ramp and tighter side padding rather than letting it wrap to two lines. */
#modeRow .mopt { font-size: clamp(10px, 2.7vw, 13px); padding: 12px 4px; letter-spacing: 0; }

#names { width: 100%; max-width: 340px; }

.mname {
  width: 100%;
  padding: 10px 12px;
  border-radius: 13px;
  border: 2px solid var(--edge2);
  background: var(--panel);
  color: #fff;
  font: 700 14px var(--font);
  margin-bottom: 9px;
}

/* A chip button at the start of a name row, beside the field it belongs to. The row is exactly
   as tall as the field: the canvas is taken out of flow, because the pass-n-play card is
   already at its height limit on a 568px phone and one extra pixel a row is four under START.
   The reset's weight is load-bearing: the short tier's `#menu.has-names .mname` margin comes
   LATER in this file, so at equal specificity it would land inside the row and grow every one
   of them by 4px. `input` is what puts this one ahead of it. */
.nrow { display: flex; gap: 8px; align-items: stretch; margin-bottom: 9px; }
#menu .nrow input.mname { margin-bottom: 0; flex: 1 1 auto; min-width: 0; }

.cbtn {
  position: relative;
  flex: none;
  width: 44px;
  border-radius: 13px;
  border: 2px solid var(--edge2);
  background: var(--panel);
  padding: 0;
  overflow: hidden;
  cursor: pointer;
}

.cbtn canvas {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 38px;
  height: 38px;
  transform: translate(-50%, -50%);
  display: block;
  pointer-events: none;
}

.cbtn:active { transform: translateY(2px); }

.cta {
  width: 100%;
  max-width: 340px;
  padding: 14px;
  border-radius: 18px;
  border: 2px solid var(--sand-edge);
  background: var(--sand);
  color: #fff5e8;
  font: 900 clamp(14px, 4vw, 18px)/1 var(--font);
  letter-spacing: .1em;
  box-shadow: 0 3px 0 var(--sand-shadow);
  text-align: center;
  margin-top: 8px;
  min-height: var(--tap);
  cursor: pointer;
}

.cta:active { transform: translateY(2px); box-shadow: 0 1px 0 var(--sand-shadow); }

.ghost {
  background: transparent;
  border-color: var(--edge2);
  color: var(--ink2);
  box-shadow: none;
  margin-top: 10px;
}

.ghost:active { transform: translateY(2px); box-shadow: none; }

/* ---- online: the room, which is a face of this card and not a screen ---- */

/* Hidden by script rather than by a class, because the menu card's own layout rules key off
   `has-names` and adding a second state class to that would make the two disagree. */
/* Sits under the mode row, one line per mode, and it is the only place the difference
   between PASS-N-PLAY and ONLINE is stated — which is the pair that gets confused. The lead
   is the device count and carries the colour, because that IS the difference; the sentence
   after it is grey and only elaborates.

   It reserves two lines whether it needs them or not: without that, switching modes moved
   every row under it by a line height, which on a card people are reading is worse than a
   little air under a short sentence. */
#modeHelp {
  width: 100%;
  max-width: 340px;
  min-height: 2.9em;
  margin: -2px 0 12px;
  color: var(--ink2);
  font: 700 clamp(11px, 3vw, 12px)/1.45 var(--font);
}

#modeHelpLead { color: var(--gold); }

/* In a room the mode is settled and the roster is what the card is for. */
#menu.in-room #modeHelp { display: none; }

#online { width: 100%; max-width: 340px; }

/* The one swap the whole flow rests on: out of a room you get HOST and JOIN, in one you get
   the code and a way out. Same box, same place on the card, nothing navigates. */
#online:not(.in-room) #roomShut { display: none; }
#online.in-room #roomOpen { display: none; }
/* No room, no roster: the label and the "nobody has joined" line described a table that
   did not exist yet, which read as a lobby that had failed to load. */
#online:not(.in-room) #roomFolk { display: none; }
/* In a room the code has its own button. Leaving it on the label as well printed the four
   characters three times on one card. */
#online.in-room #roomValue { display: none; }
/* The help line explains HOST and JOIN. Once you are in a room both are gone and it is
   describing buttons that are no longer on screen — and this is the tall face of the card,
   where those two lines are the difference between START being on the screen and being
   under the fold on a 568px phone. */
#online.in-room #onlineHelp { display: none; }

#onlineHelp {
  color: var(--ink2);
  font: 700 clamp(11px, 3vw, 12px)/1.45 var(--font);
  margin: 0 0 12px;
}

#nameHint {
  color: var(--ink2);
  font: 700 10px/1.3 var(--font);
  opacity: .8;
  margin: -4px 0 10px;
}

/* The name is fixed for the life of the room — it went out with the join. An editable box
   that changes nothing is worse than a disabled one. */
#online.in-room #yourName { opacity: .6; }

.orow { display: flex; gap: 8px; align-items: stretch; margin-bottom: 12px; }
.orow .mname { margin-bottom: 0; }

.obtn {
  flex: 0 0 auto;
  padding: 10px 14px;
  border-radius: 13px;
  border: 2px solid var(--edge2);
  background: var(--panel);
  color: #e8d8bb;
  font: 800 12px var(--font);
  letter-spacing: .06em;
  min-height: var(--tap);
  cursor: pointer;
}

.obtn.go { border-color: var(--sand-edge); background: var(--sand); color: #fff5e8; }
.obtn:active { transform: translateY(2px); }
.obtn:disabled { opacity: .45; }

/* The door that was pressed, while the room server is answering. The label is hidden rather
   than replaced so the button keeps the width it had — a row that reflows under your thumb
   reads as a mis-tap. Below .obtn:disabled on purpose: syncGates disables this button too,
   and at equal specificity the later rule is the only thing that wins. */
.obtn.busy {
  position: relative;
  color: transparent;
  opacity: 1;
}

.obtn.busy::after {
  content: '';
  position: absolute;
  inset: 0;
  width: 17px;
  height: 17px;
  margin: auto;
  border: 2px solid var(--edge2);
  border-top-color: var(--ink);
  border-radius: 50%;
  animation: obtn-spin .7s linear infinite;
}

.obtn.go.busy::after {
  border-color: rgba(255, 245, 232, .35);
  border-top-color: #fff5e8;
}

@keyframes obtn-spin { to { transform: rotate(360deg); } }

#roomOpen .mname { flex: 1 1 auto; min-width: 0; }

/* The code takes the row; LEAVE takes a square. */
.codebtn {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 4px 10px;
  padding: 8px 12px;
  border-color: var(--gold);
  background: linear-gradient(180deg, #59371f, #402716);
}

.codebtn-code {
  font: 900 clamp(22px, 7vw, 30px)/1.1 ui-monospace, Menlo, Consolas, monospace;
  letter-spacing: .3em;
  text-indent: .3em;
  color: var(--gold);
}

.codebtn-state {
  flex-basis: 100%;
  font: 800 9px var(--font);
  letter-spacing: .16em;
  color: var(--ink2);
}

.codebtn.done { border-color: #5fd977; }
.codebtn.done .codebtn-state { color: #5fd977; }
.codebtn.failed .codebtn-state { color: #ff9f43; letter-spacing: .06em; }

/* Icon-only, so it carries an aria-label and a title rather than a word. Leaving a room is
   the one destructive thing on this card, so it is set apart from the code it sits beside
   rather than dressed to match it. */
.iconbtn {
  flex: 0 0 auto;
  width: var(--tap);
  min-width: var(--tap);
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-color: #8f4437;
  color: #ff9f43;
}

.oicn { width: 18px; height: 18px; display: block; }
.codebtn .oicn { width: 15px; height: 15px; opacity: .75; }

/* A room code is four characters read out loud down a phone, so it is spaced and monospaced:
   the point is telling C from G, not looking neat. */
.code {
  text-align: center;
  font: 900 16px ui-monospace, Menlo, Consolas, monospace;
  letter-spacing: .24em;
  text-indent: .24em;
}

/* The value that rides on a section label — the code, the head count — the way
   Programming.io's field heads carry theirs. */
.mlabel b {
  float: right;
  color: #f2c231;
  font: 900 12px ui-monospace, Menlo, Consolas, monospace;
  letter-spacing: .16em;
}

.mlabel b.muted { color: var(--ink2); letter-spacing: .06em; font-weight: 700; }

#netMsg {
  color: var(--ink2);
  font: 700 clamp(10px, 3vw, 12px)/1.4 var(--font);
  min-height: 1.4em;
  margin: 2px 0 4px;
  text-align: center;
}

/* Something the player has to act on, as opposed to a progress or roster line. --sand-edge,
   not --sand: the darker red is the button fill and does not carry as 10px text on --panel. */
#netMsg.bad { color: var(--sand-edge); }

#lobbyList { width: 100%; margin: 0 0 10px; }

.lrow {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 10px;
  border-radius: 12px;
  border: 2px solid var(--edge2);
  background: var(--panel);
  font: 800 13px var(--font);
  margin-bottom: 6px;
}

.lrow span { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lrow b { color: #ff9f43; font: 800 10px var(--font); letter-spacing: .12em; }
.lrow.off { opacity: .55; }

/* Short screens, and this block has to sit BELOW the rules it trims. It used to be written
   above `.lrow`, and a media query adds no specificity — so the plain rule further down the
   file won every one of these and the roster was never trimmed at all. Four seats is the
   ceiling, so the cap almost never bites; what it guarantees is that START stays on the card
   rather than under the fold, behind a scroll the body's `touch-action: none` would eat. */
@media (max-height: 560px) {
  #lobbyList { max-height: 34vh; overflow-y: auto; margin-bottom: 4px; }
  .lrow { padding: 5px 8px; font-size: 11px; margin-bottom: 4px; }
  .orow { margin-bottom: 8px; }
  .obtn { padding: 7px 10px; font-size: 11px; }
  .codebtn { padding: 5px 10px; }
  #onlineHelp { margin-bottom: 8px; line-height: 1.35; }
  /* A phone laid flat has 320px of height for a nine-row card. The hint is the one line
     here that nothing depends on, so it is the first to go — the help line above the name
     already says what the name is for. */
  #nameHint { display: none; }
  .mlabel { margin-bottom: 4px; }
  #netMsg { margin: 0 0 2px; min-height: 1.2em; }
}

/* A phone laid flat, which is 320px of height for the whole card. 34vh of it is 109px and
   the roster is capped there anyway, so the reserved height comes down with it rather than
   holding room for a fourth row that is going to scroll regardless. Above this the cap is
   132px, which is more than the four trimmed seats need. */
@media (max-height: 360px) {
  #lobbyList { max-height: 30vh; }
}

/* Nobody is in the room yet. A line of type says so rather than an empty box that reads as
   a layout bug. */
#lobbyList:empty::after {
  content: 'Nobody has joined yet.';
  display: block;
  color: var(--ink2);
  font: 700 12px var(--font);
  text-align: center;
  padding: 6px 0 2px;
}

/* Only the host sets the board and the head count; everyone else is shown what was chosen. */
#menu.guest #countRow, #menu.guest #sizeRow { opacity: .5; pointer-events: none; }

.cta:disabled { opacity: .45; }

/* ---- the shop, the coins, and the in-match board-and-music panel ----
   Written above the layout tiers on purpose: a media query adds no specificity, so a tier
   only wins over a rule that comes before it in the file. */

.coin {
  display: inline-block;
  width: 1em;
  height: 1em;
  border-radius: 50%;
  vertical-align: -0.15em;
  background: radial-gradient(circle at 35% 35%, #fff3b0 0 18%, #f7c843 19% 60%, #c98a14 61%);
  box-shadow: inset 0 0 0 1.5px #8a5a07;
  flex: none;
}

/* Left of the mute, on the same row: the column under the mute already holds MENU, and a
   third pill down it ran into the status line on a 320px phone. */
#styleBtn { right: calc(8px + clamp(34px, 9vw, 42px) + 6px); top: calc(var(--safe-top) + 8px); }
#styleBtn svg { width: 58%; height: 58%; }
#styleBtn.on { background: var(--gold); color: var(--bg); }

#style {
  position: fixed;
  z-index: 16;
  right: 8px;
  top: calc(var(--safe-top) + 8px + clamp(34px, 9vw, 42px) + 8px);
  width: min(320px, calc(100vw - 16px));
  max-height: calc(100vh - var(--safe-top) - 72px);
  overflow-y: auto;
  touch-action: pan-y;
  overscroll-behavior: contain;
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 14px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, .55);
  padding: 12px;
  display: none;
}

#style.open { display: block; }

#styleHead {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  font: 800 13px var(--font);
  letter-spacing: .08em;
}

#styleHead small { font: 700 10px var(--font); color: var(--ink2); letter-spacing: 0; }

.stlab { font: 700 10px var(--font); letter-spacing: .1em; color: var(--ink2); margin: 10px 0 5px; }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }

.chip {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 36px;
  padding: 4px 8px 4px 5px;
  border-radius: 9px;
  border: 2px solid var(--edge2);
  background: var(--bg);
  color: var(--ink);
  font: 700 11px var(--font);
  cursor: pointer;
}

.chip.sel { border-color: var(--gold); background: var(--panel2); }
.chip .tick { color: var(--gold); }

.chip .sw {
  width: 22px;
  height: 22px;
  border-radius: 5px;
  overflow: hidden;
  display: grid;
  grid-template-columns: 1fr 1fr;
  border: 2px solid;
  flex: none;
}

.chip .nt {
  width: 22px;
  height: 22px;
  border-radius: 5px;
  background: #1d120b;
  color: var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}

.chip .nt svg { width: 15px; height: 15px; }

#styleMore { margin-top: 10px; font: 700 10px/1.4 var(--font); color: var(--ink2); }

/* Pinned to the bottom of the panel: with everything owned the chips outgrow a phone laid
   flat, and the way out must not scroll away with them. */
#styleDone {
  position: sticky;
  bottom: 0;
  /* Above it a fade, below it a solid band over the panel's bottom padding, so no chip
     shows scrolling past either side of it. */
  box-shadow: 0 -8px 10px var(--panel), 0 12px 0 var(--panel);
  margin-top: 10px;
  width: 100%;
  min-height: 38px;
  border-radius: 10px;
  border: 2px solid var(--gold);
  background: var(--sand);
  color: #fff;
  font: 800 13px var(--font);
  letter-spacing: .08em;
  cursor: pointer;
}

/* The SHOP button under START: secondary, so it wears the ghost look, and the balance on it
   is the one number on the menu card worth reading. */
#shopBtn { display: flex; align-items: center; justify-content: center; gap: 10px; }
#shopBtn .sbal { display: inline-flex; align-items: center; gap: 5px; color: var(--gold); }
/* In a room the card is at its tallest and the room is what it is for. */
#menu.in-room #shopBtn { display: none; }

#shop {
  display: none;
  overflow: hidden;
  padding: calc(var(--safe-top) + 12px) 14px calc(var(--safe-bottom) + 12px);
}

#shop.open { display: flex; }

#shopBox {
  width: 100%;
  max-width: 420px;
  height: 100%;
  max-height: 780px;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

#shopTop { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
#shopTitle { font: 900 15px var(--font); letter-spacing: .12em; color: var(--gold); }

#shopBack, #shopPurse {
  height: 36px;
  border-radius: 18px;
  border: 2px solid var(--edge);
  background: var(--panel);
  color: var(--ink);
  font: 700 12px var(--font);
  padding: 0 12px;
}

#shopBack { cursor: pointer; }
#shopPurse { display: flex; align-items: center; gap: 6px; color: var(--gold); font-size: 16px; font-weight: 800; }

#shopTabs { display: flex; gap: 6px; margin-bottom: 10px; }

.stab {
  flex: 1;
  min-height: 40px;
  border-radius: 10px;
  border: 2px solid var(--edge2);
  background: var(--bg);
  color: var(--ink);
  font: 800 12px var(--font);
  letter-spacing: .08em;
  cursor: pointer;
}

.stab.sel { background: var(--sand); border-color: var(--gold); color: #fff; }

/* The one thing on the page that scrolls, so it opts back in to a vertical pan and keeps
   the pan to itself. */
#shopGrid {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  touch-action: pan-y;
  overscroll-behavior: contain;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  align-content: start;
  padding: 0 6px 6px 0;
}

#shopGrid.list { grid-template-columns: minmax(0, 1fr); gap: 6px; }

/* The shop's own scrollbar: a gold thumb in a dark track, like the pills. Chrome and Safari
   take the ::-webkit rules; Firefox has no such selector and takes the standard pair, which
   is fenced off because Chrome IGNORES the ::-webkit rules once scrollbar-color is set. */
#shopGrid::-webkit-scrollbar, #style::-webkit-scrollbar { width: 8px; }
#shopGrid::-webkit-scrollbar-track, #style::-webkit-scrollbar-track { background: rgba(46, 28, 18, .9); border-radius: 4px; }
#shopGrid::-webkit-scrollbar-thumb, #style::-webkit-scrollbar-thumb { background: var(--edge); border-radius: 4px; border: 2px solid rgba(46, 28, 18, .9); }
#shopGrid::-webkit-scrollbar-thumb:active { background: var(--gold); }
@supports not selector(::-webkit-scrollbar) {
  #shopGrid, #style { scrollbar-width: thin; scrollbar-color: var(--edge) rgba(46, 28, 18, .9); }
}

.sitem, .srow {
  background: var(--panel);
  border: 2px solid var(--edge2);
  border-radius: 12px;
}

.sitem { padding: 8px; display: flex; flex-direction: column; gap: 6px; }
.srow { padding: 7px 7px 7px 12px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.sitem.eq, .srow.eq { border-color: var(--gold); }

.sthumb { width: 100%; aspect-ratio: 4 / 3; border-radius: 8px; display: block; }
.snm { font: 800 12px var(--font); }
.srow .snm { font-size: 13px; }
.sbl { font: 700 10px/1.35 var(--font); color: var(--ink2); min-height: 2.7em; }

.sact {
  min-height: 36px;
  border-radius: 9px;
  border: 2px solid var(--gold);
  background: var(--sand);
  color: #fff;
  font: 800 12px var(--font);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 12px;
}

.srow .sact { min-width: 118px; }
.sact .pr { display: inline-flex; align-items: center; gap: 6px; }
.sact.own { background: var(--bg); border-color: var(--edge); color: var(--ink); }
.sact.eq { background: var(--bg); color: var(--gold); cursor: default; opacity: 1; }
.sact.poor { background: var(--bg); border-color: #5a4330; color: #8f7a60; cursor: default; }

#shopFoot { margin-top: 8px; text-align: center; font: 700 11px var(--font); color: var(--ink2); }

/* A chip card is the chip and its name: no table behind it, one colour, no blurb. */
.sitem canvas.cthumb { width: 100%; aspect-ratio: 3 / 2; display: block; }
.sitem.chipitem .snm { text-align: center; }

/* The palette panel's chip row carries a little live chip where the boards carry a swatch. */
.chip .cs { width: 22px; height: 22px; display: block; flex: none; }

/* The chip picker, over the menu card. See-through like the daily card, so the name row it was
   opened from stays in view behind it. */
#chipPick { display: none; background: rgba(8, 10, 24, .6); z-index: 60; }
#chipPick.open { display: flex; }

#pickCard {
  width: 100%;
  max-width: 340px;
  max-height: 100%;
  overflow-y: auto;
  touch-action: pan-y;
  overscroll-behavior: contain;
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 16px;
  box-shadow: 0 14px 34px rgba(0, 0, 0, .6);
  padding: 14px;
}

#pickTitle { font: 900 14px var(--font); letter-spacing: .1em; color: var(--gold); text-align: center; }
#pickSub { font: 700 11px/1.4 var(--font); color: var(--ink2); text-align: center; margin: 4px 0 10px; }
#pickGrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }

.pk {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 6px 4px 7px;
  border-radius: 12px;
  border: 2px solid var(--edge2);
  background: var(--bg);
  color: var(--ink);
  font: 800 11px var(--font);
  cursor: pointer;
}

.pk canvas { width: 56px; height: 56px; display: block; }
.pk.sel { border-color: var(--gold); background: var(--panel2); }
#pickMore { margin-top: 10px; font: 700 10px/1.4 var(--font); color: var(--ink2); text-align: center; }

/* The daily streak card. Its own sheet, but see-through, so the menu it is about to hand back
   to stays in view behind it. */
#daily {
  display: none;
  background: rgba(8, 10, 24, .6);
}

#daily.open { display: flex; }

#dailyCard {
  width: 100%;
  max-width: 348px;
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 18px;
  padding: 16px 14px 14px;
  box-shadow: 0 16px 36px rgba(0, 0, 0, .6);
  text-align: center;
}

#dailyTitle { font: 900 13px var(--font); letter-spacing: .16em; color: var(--ink2); }
#dailyDay { font: 900 clamp(28px, 9vw, 36px)/1.1 var(--font); color: var(--gold); text-shadow: 0 3px 0 #7d5a07; margin-top: 4px; }
#dailySub { font: 700 11px/1.4 var(--font); color: var(--ink2); min-height: 1.4em; }
#dailySub.lost { color: #ff9f43; }

#dailyStrip { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; margin: 14px 0 12px; }

.dslot {
  min-width: 0;
  overflow: hidden;
  border-radius: 9px;
  border: 2px solid var(--edge2);
  background: var(--bg);
  padding: 5px 0 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
}

.dslot .dn { font: 700 8px var(--font); color: var(--ink2); letter-spacing: .02em; white-space: nowrap; }
.dslot .da { font: 800 11px var(--font); white-space: nowrap; }
.dslot.past { opacity: .55; }
.dslot.past .da::after { content: '\2713'; color: #5fd977; margin-left: 2px; }
.dslot.today { border-color: var(--gold); background: var(--panel2); box-shadow: 0 0 0 2px rgba(242, 194, 49, .25); }
.dslot.today .dn { color: var(--gold); font-weight: 800; }
/* Where the daily step goes up. */
.dslot.jump { border-style: dashed; }

#dailyAmt {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font: 900 clamp(32px, 11vw, 40px) var(--font);
  color: var(--gold);
  text-shadow: 0 3px 0 #7d5a07;
}

#dailyGo { max-width: none; margin-top: 12px; }
#dailyGo.done { background: var(--bg); border-color: var(--gold); color: var(--gold); box-shadow: none; }
#dailyNext { margin-top: 10px; font: 700 11px/1.45 var(--font); color: var(--ink2); }

/* The winner's card: the payout counts up in gold under the subtitle. */
.opay {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font: 900 clamp(24px, 8vw, 32px)/1 var(--font);
  color: var(--gold);
  text-shadow: 0 3px 0 #7d5a07;
  margin: -8px 0 6px;
}

.oline { text-align: center; font: 700 clamp(10px, 3vw, 12px)/1.4 var(--font); color: var(--ink2); margin-bottom: 12px; }


/* ============================================================================
   Two layouts, chosen on ORIENTATION, and Game.layout asks the same query before it
   measures so the script and the stylesheet can never disagree about which one is up.

   Everything above this line is the stacked layout: chrome above the board and below it,
   board across the full width. It is right for a portrait phone and wrong for anything
   wide and short — at 844x390 it spent 47% of the height on a 135px-wide text column and
   left the board 14px tiles.
   ============================================================================ */

/* Portrait, but short. Trim the two stacks rather than restructure them: every pixel
   taken off the chrome is a pixel of tile, and a 10-row board on a 640px screen is
   bound entirely by the height it is given. */
@media (orientation: portrait) and (max-height: 720px) {
  :root { --tap: 42px; }

  #hud { top: calc(var(--safe-top) + 24px); }
  #turnLine { line-height: 1.3; min-height: 1.3em; }
  .rrow { line-height: 1.32; }
  #tally { margin-top: 2px; font-size: clamp(9px, 2.2vw, 10px); }

  #ctr { gap: 6px; padding: 6px 12px calc(var(--safe-bottom) + 8px); }
  #hint { display: none; }

  /* Four stacked rows do not fit a control band this shallow at full size. */
  #guide { gap: 3px; padding: 6px 12px calc(var(--safe-bottom) + 6px); }
  #guideBody { line-height: 1.35; }
  #guideCancel { min-height: 34px; margin-top: 0; }
  .btn.big { min-height: clamp(54px, 13vw, 66px); }
  :root { --die: clamp(38px, 11vw, 48px); }

  /* The online card is the tallest thing this game puts on a screen: three setting rows,
     a name, a room and a four-seat roster, and then START under all of it. On a 568px
     phone that ran 98px past the fold — behind a scroll the body's `touch-action: none`
     would first have to be talked out of eating, which is the one place a START button
     must never be.

     Pass-n-play is the second-tallest, four name fields deep, so it shares the trims that
     are not about the room. Plain CPU is four rows shorter than either and is left alone —
     there is no reason to crowd a card that fits. The touch targets are NOT part of what is
     traded away: every row here keeps var(--tap); what goes is the air between them. */
  #menu.has-online,
  #menu.has-names { padding: 10px 16px; }
  #menu.has-online h1,
  #menu.has-names h1 { margin-bottom: 8px; font-size: clamp(14px, 4.4vw, 20px); }
  #menu.has-online .mlabel,
  #menu.has-names .mlabel { margin-bottom: 3px; }
  #menu.has-online .mrow,
  #menu.has-names .mrow { margin-bottom: 7px; }
  #menu.has-online .mname,
  #menu.has-names .mname { margin-bottom: 4px; }
  #menu.has-online .nrow,
  #menu.has-names .nrow { margin-bottom: 4px; }
  #menu.has-names .cbtn { width: 38px; }
  #menu.has-names .cbtn canvas { width: 34px; height: 34px; }
  #menu.has-online #start,
  #menu.has-names #start { margin-top: 4px; }
  #menu.has-online .orow { margin-bottom: 8px; }
  #menu.has-online #onlineHelp { margin-bottom: 8px; line-height: 1.35; }
  #menu.has-online #modeHelp { margin-bottom: 8px; line-height: 1.35; }
  /* The help line above already says the name is what other people see. */
  #menu.has-online #nameHint { display: none; }
  #menu.has-online .codebtn { padding: 6px 10px; }
  #menu.has-online .codebtn-code { font-size: clamp(20px, 6.4vw, 26px); }
  #menu.has-online .lrow { padding: 5px 8px; margin-bottom: 4px; font-size: 12px; }
  #menu.has-online #lobbyList { margin-bottom: 4px; }
  #menu.has-online #netMsg { margin: 0 0 2px; }
}

/* pwa:start */
/* The one-time "saved for offline" toast. Above .sheet (50) and #chipPick (60), because it
   fires on the launch that installs the service worker and the menu is still up then. */
#offline {
  position: fixed;
  z-index: 65;
  left: 0;
  right: 0;
  bottom: calc(var(--safe-bottom) + 18px);
  text-align: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity .3s ease;
}

#offline.on { opacity: 1; }

#offline .card {
  display: inline-block;
  max-width: 86vw;
  padding: 10px 20px;
  border-radius: 14px;
  background: var(--panel);
  border: 2px solid var(--edge2);
  box-shadow: 0 12px 44px var(--shadow);
}

#offline b {
  display: block;
  font: 800 clamp(15px, 4.4vw, 19px) var(--font);
  letter-spacing: .18em;
  color: var(--gold);
}

#offline span {
  display: block;
  margin-top: 3px;
  font: 700 clamp(11px, 3vw, 13px) var(--font);
  letter-spacing: .1em;
  color: var(--ink2);
}
/* pwa:end */

/* ---------------------------------------------------------------------------
   Landscape: two rails and a full-height board.

   The stacks become columns. The HUD is the left rail, the control stack the right one,
   and the board takes everything between them from the status strip down to the bottom
   edge. Every size in here is clamped against vh rather than vw: in landscape the height
   is the scarce axis, and a vw clamp on an 844px-wide phone runs at full desktop size
   exactly where there is least room for it.
   --------------------------------------------------------------------------- */
@media (orientation: landscape) {
  :root { --tap: 40px; }

  /* home-link:start */
  .home-link { left: var(--rail-in); }
  /* home-link:end */

  /* Left rail. */
  #hud {
    left: var(--rail-in);
    top: calc(var(--safe-top) + 26px);
    width: var(--rail-w);
    max-width: var(--rail-w);
  }

  #turnLine { font: 800 clamp(11px, 2vh, 16px)/1.35 var(--font); min-height: 1.35em; }

  /* The counts wrap under the name. A rail is short of width and has height to spare,
     which is the opposite of the row this is in portrait. */
  .rrow {
    flex-wrap: wrap;
    row-gap: 0;
    font: 700 clamp(9px, 1.5vh, 12px)/1.4 var(--font);
  }

  .rname { max-width: none; }
  .rcount { flex-basis: 100%; padding-left: 14px; }
  #tally { font: 700 clamp(9px, 1.4vh, 11px)/1.4 var(--font); margin-top: 5px; }

  /* The timer joins the left rail under the roster, so the strip along the top belongs to
     the status line alone — 26px of a 390px-tall screen is most of a tile. Its top comes
     from script: the roster above it is two lines per seat and there is no flow box here
     to hang it off. */
  #timerWrap {
    left: var(--rail-in);
    top: var(--rail-timer-top);
    bottom: auto;
    transform: none;
    width: var(--rail-w);
    height: clamp(15px, 2.5vh, 20px);
  }

  #timerNum { font-size: clamp(9px, 1.5vh, 11px); }

  /* Right rail: the two pills at its head, then the controls. */
  .mini {
    width: clamp(30px, 5.4vh, 42px);
    height: clamp(30px, 5.4vh, 42px);
    font-size: clamp(12px, 2.1vh, 17px);
  }

  #mute { right: var(--rail-in); top: calc(var(--safe-top) + 8px); }
  #styleBtn { right: calc(var(--rail-in) + clamp(30px, 5.4vh, 42px) + 6px); top: calc(var(--safe-top) + 8px); }

  /* Drops out of the rail head, over the controls it makes pointless while it is open. */
  #style {
    right: var(--rail-in);
    top: calc(var(--safe-top) + 16px + clamp(30px, 5.4vh, 42px));
    width: min(320px, 60vw);
    max-height: calc(100vh - var(--safe-top) - 24px - clamp(30px, 5.4vh, 42px));
  }

  #menuBtn {
    right: var(--rail-in);
    top: calc(var(--safe-top) + 8px + clamp(36px, 6.4vh, 50px));
    font-size: clamp(8px, 1.4vh, 11px);
  }

  /* Its WIDTH is what Game.layout measures to size the board, so it is stated here and
     not left to the content: a rail that changed width with the longest player name
     would resize the board every time the roster was rebuilt. */
  #ctr {
    left: auto;
    right: var(--rail-in);
    top: 0;
    bottom: 0;
    width: var(--rail-w);
    flex-direction: column;
    justify-content: center;
    gap: clamp(6px, 1.4vh, 12px);
    padding: calc(var(--safe-top) + clamp(72px, 13vh, 104px)) 0 calc(var(--safe-bottom) + 8px);
  }

  /* The keyboard line is the first thing to go when the rail is short, and it wraps
     rather than truncates when there is room for it — a rail is a column, and four short
     lines read there as well as one long one does under a stacked control bar. */
  #hint {
    display: none;
    width: 100%;
    text-align: center;
    line-height: 1.55;
    margin-bottom: 2px;
  }

  #tools { flex-direction: column; gap: clamp(5px, 1.2vh, 10px); width: 100%; }

  .btn {
    width: 100%;
    min-width: 0;
    min-height: clamp(32px, 6.2vh, 46px);
    font-size: clamp(11px, 1.9vh, 15px);
    border-radius: 14px;
  }

  .btn.big {
    width: 100%;
    min-width: 0;
    min-height: clamp(46px, 10.5vh, 84px);
    border-radius: 18px;
  }

  :root { --die: clamp(32px, 6.6vh, 50px); }
  .bicn { font-size: clamp(14px, 2.9vh, 21px); }

  /* Both sit in the strip above the board, so neither may run under a rail. */
  /* Wide enough to clear the home link and the two pills, and NOT tied to the rails:
     the rails hug the board, so on a narrow board that padding squeezed the strip to
     about twenty characters and the prompt wrapped. */
  #status {
    font-size: clamp(12px, 2.5vh, 20px);
    padding: 0 clamp(46px, 9vh, 76px);
  }

  /* The reserved height has to follow the padding: it is border-box, and Game.layout
     measures it to size the strip that the board starts below. */
  #popup {
    font-size: clamp(10px, 1.8vh, 14px);
    max-width: min(56vw, 420px);
    padding: 5px 14px;
    height: calc(1.3em + 14px);
  }

  /* Into the left rail, under the timer. The rail is a column with nothing below the
     roster, so the guide costs the board no height at all here. */
  #guide {
    left: var(--rail-in);
    right: auto;
    top: calc(var(--rail-timer-top) + clamp(26px, 4.4vh, 34px));
    bottom: auto;
    min-height: 0;
    width: var(--rail-w);
    padding: 8px 8px 10px;
    gap: 4px;
    border-top: none;
    border: 2px solid var(--gold);
    border-radius: 14px;
    text-align: left;
    align-items: flex-start;
  }

  #guide.snk { border-color: #7fd07a; }
  #guideHead { font-size: clamp(10px, 1.7vh, 13px); gap: 5px; }
  #guideIcon { font-size: clamp(13px, 2.2vh, 18px); }
  #guideStep { font-size: clamp(8px, 1.3vh, 10px); letter-spacing: .12em; }
  #guideBody { font-size: clamp(9px, 1.5vh, 12px); line-height: 1.4; }

  #guideCancel {
    align-self: stretch;
    padding: 0 8px;
    min-height: clamp(28px, 5vh, 38px);
    font-size: clamp(9px, 1.4vh, 11px);
  }

  /* Beside the rail rather than above it: in a column layout the buttons run the whole
     height, so there is no "above the band" to sit in. It slides out of the rail. */
  #cardPanel {
    left: auto;
    right: calc(var(--rail-in) + var(--rail-w) + 12px);
    top: 50%;
    bottom: auto;
    transform: translateY(-50%) translateX(8px);
    transform-origin: right center;
    width: min(320px, 42vw);
    gap: 6px;
  }

  #cardPanel.up { transform: translateY(-50%) translateX(0); }

  /* The sheets. Stacked, the menu is nine rows deep, and on a 390px-tall screen in
     pass-n-play the START button lands 105px below the fold — behind a scroll the body's
     touch-action would first have to be talked out of eating. Two columns instead, and
     the second one exists only when there are names to put in it. */
  .sheet { justify-content: center; padding: 10px 16px; }

  /* The tracks are CONTENT-WIDE and the grid is centred, not two 1fr halves of the
     viewport. Every row inside the card is capped at 340px, so a flexible track just
     parks a 340px control in the middle of a 500px column: on a 1024-wide screen the
     names column landed most of a screen away from the settings it belongs to, and
     switching to pass-n-play threw the settings out of centre to pay for it. The min of
     0 is what lets the pair shrink together on a 568px phone instead of overflowing. */
  #menu {
    display: grid;
    grid-template-columns: minmax(0, 340px);
    align-content: center;
    justify-content: center;
    justify-items: center;
    column-gap: 22px;
  }

  #menu.has-names, #menu.has-online { grid-template-columns: repeat(2, minmax(0, 340px)); }

  #menu h1 {
    grid-area: 1 / 1 / 2 / -1;
    margin: 0 0 clamp(6px, 2vh, 16px);
    font-size: clamp(15px, 4.2vh, 26px);
  }

  #menu > .mlabel, #menu > .mrow, #menu > #modeHelp { grid-column: 1; }
  #modeHelp {
    font-size: clamp(9px, 1.5vh, 12px);
    margin: -1px 0 clamp(5px, 1.4vh, 12px);
  }
  /* Row 3, not row 2: the first name field lines up with the first row of BUTTONS, which
     is what the eye pairs it with. Starting it a row earlier put it level with the GAME
     MODE caption instead and left every field sitting a half-row proud of the controls
     beside it. The end row is only a ceiling — align-self is start — but it has to clear
     the whole left column, which the mode line made one row taller. */
  #menu.has-names > #names { grid-area: 3 / 2 / 9 / 3; align-self: start; }
  /* The room takes the second column for the same reason the names do. Stacked in one
     column it is five more rows on a card that already fills a 390px-tall screen, and the
     sheet would scroll — which on the one screen where the whole card should be visible at
     once is the worst place to hide the JOIN button. */
  #menu.has-online > #online { grid-area: 2 / 2 / 10 / 3; align-self: start; }
  /* START runs the WIDTH of both columns. Left at the 340px cap every other row wears, a
     button spanning two columns is centred across the gap and lines up with neither of
     them — it reads as a third column of one. */
  #menu > #start, #menu > #shopBtn { grid-column: 1 / -1; width: 100%; max-width: none; }

  /* A wide screen gets more boards to a row and the music in two columns. */
  #shopBox { max-width: 760px; }
  #shopGrid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
  #shopGrid.list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .opay { font-size: clamp(20px, 5.6vh, 32px); margin: -4px 0 4px; }
  /* A phone laid flat has 320px for the whole card. */
  #dailyCard { padding: clamp(8px, 2vh, 16px) 14px clamp(8px, 2vh, 14px); }
  #dailyDay { font-size: clamp(20px, 6vh, 36px); }
  #dailyStrip { margin: clamp(6px, 1.8vh, 14px) 0 clamp(6px, 1.6vh, 12px); }
  #dailyAmt { font-size: clamp(22px, 7vh, 40px); }
  #dailyGo { margin-top: clamp(6px, 1.6vh, 12px); }
  #dailyNext { margin-top: clamp(4px, 1.2vh, 10px); }
  .oline { font-size: clamp(9px, 1.7vh, 12px); margin-bottom: clamp(6px, 1.6vh, 12px); }

  /* vh, not vw: on an 844-wide phone a vw clamp runs at full desktop size on the one axis
     that has nothing to spare. Same rule as everything else in this tier. */
  #onlineHelp { font-size: clamp(9px, 1.5vh, 12px); margin-bottom: clamp(5px, 1.4vh, 12px); }
  #nameHint { font-size: clamp(8px, 1.2vh, 10px); margin: -3px 0 clamp(4px, 1.2vh, 9px); }
  .codebtn-code { font-size: clamp(17px, 3.6vh, 26px); }
  .codebtn { padding: clamp(5px, 1.2vh, 9px) 10px; }

  .mlabel { margin-bottom: 4px; font-size: clamp(9px, 1.5vh, 12px); }
  .mrow { margin-bottom: clamp(5px, 1.4vh, 12px); gap: 7px; }
  .mopt { padding: 6px; min-height: clamp(30px, 5.6vh, 44px); font-size: clamp(11px, 1.9vh, 15px); }
  .mname { padding: 7px 11px; margin-bottom: 6px; font-size: clamp(11px, 1.9vh, 14px); }
  .nrow { margin-bottom: 6px; }
  .cbtn { width: 38px; }
  .cbtn canvas { width: 30px; height: 30px; }

  .cta {
    padding: clamp(8px, 1.8vh, 14px);
    margin-top: clamp(4px, 1.2vh, 10px);
    min-height: clamp(34px, 6.4vh, 44px);
    font-size: clamp(12px, 2.2vh, 18px);
  }

  .ghost { margin-top: clamp(5px, 1.4vh, 10px); }

  #overTitle { font-size: clamp(18px, 6.4vh, 40px); }
  #overSub { margin-bottom: clamp(8px, 2.4vh, 20px); font-size: clamp(11px, 1.9vh, 15px); }
}

/* A landscape rail tall enough to spare the room gets the keyboard line back. It is the
   only thing on screen that is pure text, so it is the first thing the short tiers drop
   and the last thing they earn back. */
@media (orientation: landscape) and (min-height: 620px) {
  #hint { display: block; }
}

/* A phone laid flat is the tightest case there is: two rails, a status strip and ten
   rows of board inside 390px of height. Narrow the rails and drop the tally — the one
   line on screen that no turn ever depends on. */
@media (orientation: landscape) and (max-height: 430px) {
  :root { --rail-w: clamp(100px, 13vw, 132px); }
  #tally { display: none; }
  /* The rail is 100px wide and 320px tall here; the guide keeps its heading and its step
     and drops the sentence, which is the only part that would wrap to six lines. */
  #guideBody { display: none; }
}

/* Last in the file on purpose: it has to outrank the landscape tier's own transforms, and
   both are one class deep. The panel still arrives — it just stops travelling to get here. */
@media (prefers-reduced-motion: reduce) {
  #cardPanel,
  #cardPanel.up { transition: opacity .12s linear; transform: translateX(-50%); }
  /* The colour still says it; the title just stops moving. */
  #overTitle.win,
  #overTitle.lose { animation: none; }

  @media (orientation: landscape) {
    #cardPanel,
    #cardPanel.up { transform: translateY(-50%); }
  }
}

/* The first-time tips: a pointing hand, a pulse on what to tap, and a bubble that says why.
   The whole layer is pointer-events: none, so every tap lands on the real button or tile
   underneath. Only GOT IT takes a tap. */
#tut { position: fixed; inset: 0; z-index: 30; pointer-events: none; }
#tut.gone { display: none; }

.tut-ring {
  position: absolute; width: 54px; height: 54px; margin: -27px 0 0 -27px; border-radius: 50%;
  border: 3px solid var(--gold); box-shadow: 0 0 14px rgba(242, 194, 49, .7);
  animation: tutRing 1.1s ease-out infinite;
}
@keyframes tutRing { from { transform: scale(.45); opacity: 1; } to { transform: scale(1.35); opacity: 0; } }

.tut-hand { position: absolute; width: clamp(40px, 11vw, 54px); height: auto; filter: drop-shadow(0 3px 2px rgba(0,0,0,.55)); }
/* The fingertip is the anchor: the hand is placed so the tip sits on the target, and the bob
   moves it off the target and back, which reads as tapping. */
.tut-hand.down { animation: tutTapDown .9s ease-in-out infinite; }
.tut-hand.up { animation: tutTapUp .9s ease-in-out infinite; }
@keyframes tutTapDown { 0%, 100% { translate: 0 -16px; } 45%, 60% { translate: 0 0; } }
@keyframes tutTapUp { 0%, 100% { translate: 0 16px; } 45%, 60% { translate: 0 0; } }

.tut-bubble {
  position: absolute; width: min(290px, calc(100vw - 24px)); padding: 10px 12px 10px;
  background: var(--panel); border: 2px solid var(--gold); border-radius: 14px;
  box-shadow: 0 4px 0 var(--shadow), 0 10px 24px rgba(0,0,0,.5); color: var(--ink);
  font: 700 clamp(11px, 3.2vw, 13px)/1.45 var(--font);
  animation: tutPop .22s ease-out;
}
.tut-bubble b { display: block; color: var(--gold); font-size: 1.12em; letter-spacing: .06em; margin-bottom: 3px; }
.tut-bubble.snk { border-color: #5fd977; }
.tut-bubble.snk b { color: #93cf6b; }
.tut-bubble.card { border-color: #7bb8ff; }
.tut-bubble.card b { color: #9cc9ff; }
.tut-bubble button {
  pointer-events: auto; float: right; margin: 6px 0 0 10px; padding: 5px 10px; border-radius: 9px;
  border: 2px solid var(--edge2); background: var(--panel2); color: var(--ink2);
  font: 800 11px/1 var(--font); letter-spacing: .08em; cursor: pointer;
}
.tut-bubble.done { border-color: var(--gold); }
@keyframes tutPop { from { transform: scale(.85); opacity: 0; } to { transform: scale(1); opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .tut-hand, .tut-ring, .tut-bubble { animation: none; }
}
