:root {
  color-scheme: dark;
  --ink: #e8eef0;
  --dim: rgba(232, 238, 240, 0.6);
  /* The notch's share of the top padding, CAPPED — and the cap is
     the fix for a real bug rather than a taste. In the CrazyGames
     iOS app the game runs in a webview that spans the whole screen,
     so env(safe-area-inset-top) reports the device's full 59px
     while the app draws its own toolbar over exactly that space:
     the page paid for the notch twice. Measured in the app on a
     402x874 phone, 75px of empty screen above the title, on a game
     whose entire chrome is meant to be 50.
     In a portal iframe the inset is 0 and this changes nothing. */
  --notch: min(env(safe-area-inset-top, 0px), 24px);
}
* { box-sizing: border-box; }
html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: #10171a;
  color: var(--ink);
  font: 15px/1.45 -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo",
    "Segoe UI", Roboto, system-ui, sans-serif;
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  /* iOS shows the text-selection magnifier on a long press unless
     the callout is switched off — and the cue is held, not tapped,
     so a careful aim was summoning a loupe over the table. Nothing
     on this page is selectable text; there is no callout worth
     having anywhere in it. */
  -webkit-touch-callout: none;
}
/* Landscape puts the chrome in a narrow column beside the table and
   portrait stacks it above — the same fix StoneFlick needed when a
   wide window squashed the board into a band. */
#app {
  display: grid;
  height: 100%;
  grid-template-rows: auto 1fr;
  gap: 8px;
  /* Air above the title. 12px read as tight because the header is
     now a dense three-line block instead of three loose ones —
     tightening the rhythm inside it made the space around it the
     thing that was missing. Scales with the window so it stays a
     proportion rather than a number, and it is ON TOP of whatever
     the notch still needs — see --notch. */
  padding: calc(var(--notch) + clamp(18px, 2.8vh, 34px))
    env(safe-area-inset-right) env(safe-area-inset-bottom)
    env(safe-area-inset-left);
}
/* The header lines up with the TABLE, not with the window.
   --table-left / --table-right are written by main.js from the
   layout every time it changes, so the title starts where the wood
   starts and the buttons end where the wood ends. Before this the
   text sat in the window's corner with 250px of black between it
   and the thing it was describing, which is what "nothing is
   aligned to anything" looks like. */
/* A grid, not a wrapping flex row. Wrapping put the hint BESIDE the
   title on a 2000px screen and under it on a 1160px one — the same
   markup laying itself out two different ways depending on how much
   room there happened to be, which is the definition of a layout
   nobody designed. Three named rows always do the same thing. */
header {
  display: grid;
  grid-template-columns: minmax(0, auto) auto 1fr auto auto auto auto;
  grid-template-areas:
    "name shots . undo preview restart menu"
    "hint hint hint hint hint hint hint"
    "status status status status status status status";
  align-items: baseline;
  column-gap: 10px;
  row-gap: 1px;
  /* Aligned to the TABLE's edges, but never so far in that the row
     cannot hold what is in it. On a portrait tablet the table is
     narrow and this padding was 204px a side, leaving a 360px box
     for a title, a clock, Preview and a menu that need 370 — and
     the title, the only flexible column, paid the 9px as "Play
     with AI" -> "Play with...". The floor keeps a 420px content
     box wherever the window has room for one; on a desktop the
     table's own inset is smaller than that and the alignment is
     untouched. */
  padding-left: min(var(--table-left, 6px), max(0px, (100vw - 420px) / 2));
  padding-right: min(var(--table-right, 6px), max(0px, (100vw - 420px) / 2));
}
#stage-name { grid-area: name; }
#shots { grid-area: shots; }
#preview { grid-area: preview; }
#undo { grid-area: undo; }
#restart-now { grid-area: restart; }
#menu-open { grid-area: menu; }
#hint { grid-area: hint; }
#status { grid-area: status; }
#stage-name { font-weight: 650; font-size: 17px; letter-spacing: -0.01em; }
#shots { color: var(--dim); font-variant-numeric: tabular-nums; font-size: 13px; }
/* The title row must stay ONE line. When the preview badge appears
   it steals ~120px, and without this the row broke into "1. First /
   Point" beside "Try 3 / Par 1" - four lines of chrome above a
   stage that has one thing to say. The name truncates instead. */
#stage-name, #shots, #preview, #undo { white-space: nowrap; }
#stage-name { overflow: hidden; text-overflow: ellipsis; }
#preview, #undo { font-size: 12px; padding: 0 13px; }
/* Both are full-width rows under the title. The tight row-gap is
   the point: three lines 50px apart read as three unrelated things
   floating in a corner, and at 2px they read as one block with a
   heading. */
#hint { color: var(--dim); font-size: 13px; max-width: 78ch; }
#status {
  font-size: 12px;
  color: rgba(140, 220, 175, 0.85);
  letter-spacing: 0.01em;
  /* The row holds its height while the line is empty, and the line
     IS empty for the whole of a shot. Without this the header lost
     16px the moment the balls started rolling and got it back when
     they stopped, so the table grew and shrank around every stroke
     — which reads as the screen jolting, not as a layout. */
  /* line-height and min-height must be the SAME multiple, or the
     empty row is a fraction of a pixel shorter than the full one
     and the table still creeps. */
  line-height: 1.35;
  min-height: 1.35em;
}
/* The bead counter. A wooden rack with a steel rod through it, the
   way the one on a billiard-hall wall is built — the rod is what
   makes a line of circles read as beads that slide rather than as
   dots that light up. */
/* Sized to the beads, not to the row. A rack that runs the width of
   the header with five beads at one end is a rack with a rod going
   nowhere; a real one is as long as the count it holds. */
.scoreboard { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; max-width: 100%; }
/* A thirty-point rack is thirty beads, and thirty beads at their
   natural size is wider than a phone. The row is capped at the
   width it has and the beads shrink to fit rather than running off
   the screen — which they did, taking the count with them and
   squeezing the title into "Play with..." on the way, because the
   header is one grid and an item that cannot shrink is paid for by
   its neighbours. */
.scoreboard-row {
  position: relative;
  width: fit-content;
  max-width: 100%;
  box-sizing: border-box;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px 8px;
  border-radius: 5px;
  background: linear-gradient(180deg, #2a211d, #191310);
  box-shadow: inset 0 1px 0 rgba(226, 205, 164, 0.14), 0 1px 2px rgba(0, 0, 0, 0.5);
}
.scoreboard-rod {
  position: absolute;
  left: 5px;
  right: 5px;
  height: 2px;
  border-radius: 1px;
  background: linear-gradient(180deg, #9aa2a6, #4e5559);
}
/* Two groups on one rod with a gap between them: what is still to be
   cleared on the left, what has been pushed across on the right.
   The gap is what makes it read as beads that MOVED rather than as
   lamps that changed colour. */
.scoreboard-track { position: relative; display: flex; align-items: center; gap: inherit; flex: 1; min-width: 0; }
.scoreboard-side { display: flex; gap: 2px; min-width: 0; }
.scoreboard-gap { flex: 1 1 auto; min-width: 8px; }
.scoreboard-bead {
  position: relative;
  flex: 0 1 auto;
  width: 9px;
  min-width: 4px;
  height: 13px;
  border-radius: 3px;
  /* Off-white bone, and the same walnut behind it as the rails. */
  /* The ones still to be cleared are the bright bone ones: they are
     the work left, and the work left is what the player is looking
     for. Pushed across, a bead goes dim — done is quiet. */
  background: linear-gradient(180deg, #f2ede0, #b9b2a0);
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.45);
}
/* Every second group of ten in brass instead of bone. A rack is
   READ, not counted: one band and three beads is a glance, thirteen
   beads in a row is arithmetic. Same value, same brass as the rest
   of the furniture, so the band reads as a marking on the rack
   rather than as a second kind of score. */
.scoreboard-bead.ten {
  background: linear-gradient(180deg, #e8c489, #a97f43);
}
.scoreboard-bead.done {
  background: linear-gradient(180deg, #5b5548, #3a352d);
  box-shadow: none;
}
.scoreboard-bead.ten.done {
  background: linear-gradient(180deg, #5c4c34, #3a3122);
  box-shadow: none;
}
/* Whose table it is. A rack that is merely a score cannot say that,
   and in a game of alternating turns it is the thing the player most
   needs to know without being told in words. */
.scoreboard-row.live { box-shadow: inset 0 0 0 1px rgba(233, 198, 132, 0.55), 0 1px 2px rgba(0, 0, 0, 0.5); }
.scoreboard-count {
  flex: 0 0 auto;
  margin-left: 7px;
  min-width: 1.4em;
  text-align: right;
  color: rgba(233, 198, 132, 0.85);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
@media (max-width: 520px) {
  .scoreboard-bead { width: 7px; height: 11px; }
  .scoreboard-row { padding: 2px 6px; gap: 1.5px; }
}
/* Centred in what is left under the header, not stacked from the
   top: the card is short and the board is tall, so top-aligned put
   every word on the head rail. */
#setup-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 4px 18px 22px;
  max-width: 420px;
  margin: 0 auto;
  width: 100%;
  min-height: 0;
}
#setup-blurb { color: var(--dim); font-size: 13px; text-align: center; margin: 0; }
#setup-record { color: rgba(233, 198, 132, 0.9); font-size: 13px; margin: 0; min-height: 1.3em; }
.setup-field { width: 100%; display: flex; flex-direction: column; gap: 7px; align-items: center; }
/* An author display rule outranks the UA [hidden] default, so the
   practice screen would keep showing the opponent field without this. */
.setup-field[hidden] { display: none; }
.setup-label { color: var(--dim); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; }
/* A segmented control rather than a dropdown: five numbers is a row,
   and a row shows what the choices ARE without being opened. */
.seg { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; }
.seg button { min-width: 52px; min-height: 40px; }
.seg button.on {
  background: rgba(211, 162, 68, 0.18);
  border-color: rgba(211, 162, 68, 0.75);
  color: #f0d9a2;
}
#setup-start { min-width: 200px; margin-top: 4px; }
#runover strong { font-size: 17px; }
#runover-time {
  font-size: 40px;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  color: #f0d9a2;
}
#runover span { color: var(--dim); font-size: 13px; }
#runover[hidden] { display: none; }
.spacer { display: none; }
/* The header is dev scaffolding — stage navigation will become a
   stage-select screen. It still must not eat the table on a phone:
   at 390px the four buttons wrapped to a second row and took 170px
   of height off a viewport where height is the constrained axis. */
@media (max-width: 520px) {
  #app { padding: calc(var(--notch) + 20px) 0 0; gap: 6px; }
  header { column-gap: 8px; }
  #stage-name { font-size: 15px; }
  #shots, #hint { font-size: 12px; }
  /* The header is padded to the TABLE's edges, and on a phone the
     table is narrow, so that padding is what the title row has to
     live inside. It was first found on short phones — at 375x667 a
     54px box for a 64px word, so "Stage 1" rendered as "Stag." —
     and fixed only there, which was too narrow a fix: at 390x844
     the box is 278px against 294px of title, clock, Preview and
     menu, and "Play with AI" came out as "Play with...". On a
     phone the alignment is worth less than the words, so below this
     width the header uses the window's own edges. */
  header { padding-left: 14px; padding-right: 14px; }
  #status { font-size: 12px; }
  button { padding: 3px 7px; font-size: 12px; }
  /* The row the sentence used to be in. It carries the bead rack
     in the timed modes and nothing at all in the campaign, so it
     takes a line only when there is something in it. */
  #hint, #status { grid-area: hint; line-height: 1.35; }
  #hint:empty { display: none; }
  header.free #status { grid-area: status; min-height: 1.35em; }
}
/* ON A PHONE THE HEADER IS ONE ROW.
   Everything above this point spent two or three lines on text —
   a title, a hint, a status — and on the axis a phone is tightest
   on. Measured at 390x844: 98px of chrome, 12% of the window, for
   a sentence nobody reads twice; the table under it was 292px wide
   with a 6.4px ball.
   The teaching does not disappear, it moves: the hint is shown on
   the table itself when the stage opens (the banner pill, which is
   where every other event in this game already speaks) and lives
   in the menu for the rest of the stage. The status line — "tap
   the bar", "let go to shoot" — is dropped on phones: the first-run
   card teaches those three sentences once, and repeating them
   forever costs a line of table on every frame. */
@media (max-width: 520px), (max-height: 500px) {
  /* #hint is the row the bead rack lives in, so it stays — it
     just has no sentence in it any more. The status line goes:
     the first-run card teaches those three sentences once, and
     repeating them forever cost a line of table on every frame. */
  #status { display: none !important; }
  header { row-gap: 0; }
  /* The 20px above the title was breathing room for a three-row
     header. One row does not need it, and the notch has its own
     inset. */
  #app { padding: calc(var(--notch) + 8px) 0 0; gap: 4px; }
}
/* A SHORT PHONE HELD UPRIGHT pays for the hint twice: 98px of a
   568px screen is chrome, and the table below it is fitted to what
   is left and then halved again by its own 1:2 shape. Two lines of
   hint is 21 of those pixels. Tall phones keep both lines — the
   longest of the hundred hints is two lines at this size — and the
   short ones clamp to one, because on those the second line costs
   more table than it carries meaning. */
@media (max-width: 520px) and (max-height: 700px) and (orientation: portrait) {
  #app { padding: calc(var(--notch) + 10px) 0 0; }
}
/* A LANDSCAPE PHONE IS NOT A DESKTOP.
   The compaction above is keyed on width, and a phone on its side
   is 844px wide — so none of it applied, and the game shipped a
   desktop header onto a 390px-tall screen: 112 of those pixels,
   29% of the window, spent on a title, a two-line hint and a
   status line. The table is laid out inside what is left and it is
   height-limited, so every pixel of chrome costs TWO of table
   width. Measured at 844x390: a 359px table with 240px of black
   either side of it.
   Height is the axis that is short here, so this block is keyed on
   height, and it does what the phone block does — the hint and the
   status share one line instead of taking three between them. */
@media (max-height: 500px) {
  #app { padding: calc(var(--notch) + 6px) 0 0; gap: 4px; }
  header { padding-left: 14px; padding-right: 14px; row-gap: 0; }
  #stage-name { font-size: 15px; }
  #shots, #hint, #status { font-size: 12px; }
  #hint, #status { grid-area: hint; line-height: 1.3; }
  #hint:empty { display: none; }
  header.free #status { grid-area: status; min-height: 1.3em; }
}
button {
  appearance: none;
  border: 1px solid rgba(232, 238, 240, 0.22);
  background: rgba(232, 238, 240, 0.06);
  color: var(--ink);
  border-radius: 8px;
  padding: 6px 12px;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
button:active { background: rgba(232, 238, 240, 0.16); }
#board { position: relative; min-height: 0; }
/* The properties are repeated on the canvas rather than left to
   inherit: it is the element the finger is actually on, and Safari
   has shipped versions where the callout came back if the rule was
   only on body. */
canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
}
#banner {
  position: absolute;
  left: 50%;
  top: 14px;
  transform: translateX(-50%) translateY(-8px);
  padding: 8px 18px;
  border-radius: 999px;
  background: rgba(16, 23, 26, 0.86);
  border: 1px solid rgba(232, 238, 240, 0.18);
  font-weight: 600;
  opacity: 0;
  transition: opacity 0.18s ease, transform 0.18s ease;
  pointer-events: none;
  /* Not nowrap: the longest banner ("Preview ready - aim and it
     draws the path") is wider than a 390px phone, and a pill that
     runs off both edges of the screen is worse than a two-line
     one. */
  max-width: min(86%, 340px);
  text-align: center;
  line-height: 1.3;
}
#banner.show { opacity: 1; transform: translateX(-50%) translateY(0); }

/* Overlays sit on the board, not above it: the table stays visible
   behind everything, because a player who can see the game is a
   player who has already started reading it. */
#firstrun, #cleared, #menu, #stuck, #runover {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 20px 26px;
  border-radius: 14px;
  background: rgba(10, 20, 16, 0.82);
  border: 1px solid rgba(232, 238, 240, 0.16);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
  text-align: center;
  max-width: min(92%, 420px);
}
/* At the TOP of the board, not the middle or the bottom. The middle
   is where the balls are and the bottom is where the cue and the
   controls are — a card that covers the thing it is describing is
   worse than no card. The head of the table is empty in every stage
   because that is where the player shoots FROM. */
#firstrun {
  pointer-events: none;
  transition: opacity 0.25s ease;
  top: 5%;
  bottom: auto;
  transform: translate(-50%, 0);
  gap: 3px;
  padding: 11px 18px 13px;
  /* Wider and squarer than it was, because it now carries three
     numbered steps. The width is what keeps each step on ONE line:
     a step that wraps reads as two steps, and this card's whole job
     is to say there are exactly three.
     width: max-content is not decoration. An absolutely positioned
     box pinned with left: 50% shrinks to fit the space from that
     edge to the right of its container — HALF the board — so the
     max-width below was never reached and every step wrapped at
     190px however wide the phone was. */
  width: max-content;
  max-width: min(94%, 376px);
  background: rgba(10, 20, 16, 0.8);
}
#firstrun.gone { opacity: 0; }
#firstrun strong { font-size: 15px; font-weight: 650; line-height: 1.25; }
#firstrun span { color: var(--dim); font-size: 12.5px; }
#howto {
  margin: 7px 0 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
  display: flex;
  flex-direction: column;
  gap: 4px;
  text-align: left;
  font-size: 12.5px;
  color: rgba(140, 220, 175, 0.92);
}
/* Flex rather than a text-indent, so a step that does wrap on a
   narrow phone hangs under its own text instead of sliding back
   under the number. */
#howto li {
  counter-increment: step;
  display: flex;
  align-items: baseline;
  gap: 7px;
}
#howto li::before {
  content: counter(step);
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1px solid rgba(140, 220, 175, 0.45);
  color: rgba(140, 220, 175, 0.75);
  font-size: 10px;
  line-height: 15px;
  text-align: center;
  vertical-align: 1px;
}
#tip {
  position: absolute;
  top: 8px;
  right: 10px;
  z-index: 3;
  max-width: min(74%, 292px);
  padding: 9px 12px;
  border-radius: 11px;
  text-align: left;
  font-size: 12px;
  line-height: 1.45;
  color: var(--dim);
  background: rgba(10, 20, 16, 0.93);
  border: 1px solid rgba(232, 238, 240, 0.18);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.5);
  pointer-events: none;
  transition: opacity 0.25s ease;
}
#tip.gone { opacity: 0; }
#tip[hidden] { display: none; }
/* The menu sits ABOVE the other cards and brings its own ground.
   All four of these are absolutely positioned in the same place
   with no z-index between them, so opening the menu over a cleared
   card or the ad offer painted the two into each other: "Next
   stage" bled through under "Close", the stars through "Restart
   this stage". Two dialogs at once is a bug whichever one wins, so
   the menu covers the board rather than sharing it. */
#menu {
  z-index: 6;
  /* Opaque, and with the rest of the board dimmed behind it. The
     other cards are translucent because there is only ever one of
     them and the table behind is worth seeing; the menu is the one
     card that opens ON TOP of another. At 0.82 alpha the cleared
     card read straight through it — "Next stage" under "Home",
     the stars under "Restart this stage" — which is two dialogs at
     once whichever one you meant to press. */
  background: #101c18;
  box-shadow: 0 0 0 100vmax rgba(6, 12, 10, 0.74), 0 18px 50px rgba(0, 0, 0, 0.5);
}
#firstrun[hidden], #cleared[hidden], #menu[hidden], #stuck[hidden], #runover[hidden] { display: none; }
/* Hiding the header on the front screens cannot be `display: none`
   on the header itself: that removes it as a GRID ITEM, #board
   slides up into the `auto` row and the table gets laid out in a
   195px strip with the 1fr row left empty below it. (Measured, once,
   the hard way.) The row template moves with the state instead. */
#app.front { grid-template-rows: 1fr; }
#app.front > header { display: none; }
#stuck strong { font-size: 16px; }
#stuck span { color: var(--dim); font-size: 13px; max-width: 300px; }
#stuck-why { color: rgba(255, 170, 150, 0.9); font-size: 13px; }
#preview.ready {
  background: rgba(150, 235, 255, 0.18);
  border-color: rgba(150, 235, 255, 0.6);
  color: #bdf0ff;
}
#cleared-stars { font-size: 30px; letter-spacing: 4px; color: #ffd479; }
#cleared-kind { color: var(--dim); font-size: 13px; margin-bottom: 4px; }
#menu button { min-width: 190px; }
/* One accent per screen. Rematch stays, it just does not get the
   accent - two accents in one place is "which do I press". */
/* Brass, not green, and the reason is that brass already has a job
   on this screen: the wordmark, the star count and the rail sights
   are all this colour, so the one button that matters joins the
   family instead of introducing a second one. It is also the
   complement of the cloth, which is the highest-contrast pairing
   available for the thing that has to be tapped.
   The green it replaced was picked against a GREEN table, where it
   was a lighter tint of the same family. On the blue-teal default
   it became an unrelated hue with nothing else on the page
   answering to it. Brass survives all five tables; nothing else
   tried does.
   Green stays on .tile.here and .card.on, which mean "you are
   here" rather than "press this". The two never share a screen. */
button.primary {
  background: #d3a244;
  border-color: #eec97a;
  color: #1b1305;
  font-weight: 650;
  font-size: 15px;
  padding: 10px 22px;
}
button.quiet { border-color: transparent; background: none; color: var(--dim); }
/* 44x38, not 26x22. The menu is the only control in the in-game
   header and it was under half the size a thumb is measured
   against, which is a miss every few taps and reads as small even
   when it does land. The ::after grows the TARGET past the box, so
   the row does not have to grow with it. */
#menu-open {
  font-size: 20px;
  line-height: 1;
  padding: 0;
  width: 44px;
  min-height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 11px;
  position: relative;
}
#menu-open::after { content: ""; position: absolute; inset: -6px; }
/* Same box as the menu button, so the two read as one pair rather
   than as a button and a smaller button. */
#restart-now {
  padding: 0;
  width: 40px;
  min-height: 38px;
  align-items: center;
  justify-content: center;
  border-radius: 11px;
  position: relative;
  color: var(--ink);
}
#restart-now::after { content: ""; position: absolute; inset: -6px; }
#restart-now svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.1;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Everything else in that row matches it, or a 38px menu button
   sits beside two 26px ones and the row looks broken. */
header button { align-self: center; min-height: 38px; }
/* :not([hidden]) is load-bearing. A bare "#undo { display: flex }"
   OUTRANKS the global [hidden] { display: none } — id beats
   attribute — so both buttons appeared in the campaign, where
   neither belongs, and squeezed the stage name down to "S.". */
/* :not([hidden]) for the same reason as the two above: an id
   selector setting display outranks [hidden] { display: none }, and
   the button would then be on screen in the timed modes, where
   restarting means something else. */
#preview:not([hidden]), #undo:not([hidden]), #restart-now:not([hidden]) {
  display: inline-flex;
  align-items: center;
}

/* --- the front screens -------------------------------------------
   The choice this markup makes, and the one worth writing down: the
   home screen has NO PANEL. It is a scrim and some type laid
   straight over the live table, which is still rendering and still
   running its attract stroke behind the words.

   That is a deliberate answer to a measured loss. Daily Five put a
   title, a tagline and three buttons on an opaque first screen and
   44% of visitors (36.8% on mobile) never started a game — the
   retro's read was that the screen was a page ABOUT the game rather
   than the game. A gate is what this project wants, so it gets a
   gate; what it does not have to be is opaque. A player who has not
   read a word has already seen a billiard table with a cue moving
   on it, and the primary button is one tap from the cloth.

   Everything here is `position: absolute` inside #board and named
   in the display rule below, which is the invariant test/copy
   checks: an overlay that is not positioned renders underneath a
   full-size canvas and is invisible while claiming to be shown. */
#home, #stagemap, #tables, #setup {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 16px;
  overflow: hidden;
}
#home[hidden], #stagemap[hidden], #tables[hidden], #setup[hidden] { display: none; }
/* Dark at the edges, clear through the middle: the type gets its
   contrast and the cloth keeps its colour. A flat 70% wash over the
   whole thing would read as a modal, which is the thing we are
   avoiding. */
#home::before, #stagemap::before, #tables::before, #setup::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}
/* Home lets the table through: it is the poster, and the product is
   the green. */
#home::before {
  background:
    radial-gradient(120% 78% at 50% 42%, rgba(8, 14, 12, 0.14) 0%, rgba(6, 11, 10, 0.68) 74%, rgba(4, 8, 7, 0.92) 100%),
    linear-gradient(180deg, rgba(6, 11, 10, 0.86) 0%, rgba(6, 11, 10, 0.06) 26%, rgba(6, 11, 10, 0.1) 52%, rgba(5, 9, 8, 0.96) 100%);
}
/* The stage map does not: it is a page of data, and cloth texture
   running under seven tiles of small type is noise competing with
   the only thing the screen is for. Same table, much further back. */
#stagemap::before, #tables::before { background: rgba(6, 12, 10, 0.88); }
/* The setup card sits ON the table, and the type was landing on
   the rail, where a light wood grain ate whole words. Same wash
   as home: dark where the words are, cloth still visible. */
#setup::before {
  background: radial-gradient(120% 70% at 50% 50%, rgba(6, 11, 10, 0.62) 0%, rgba(5, 9, 8, 0.88) 70%, rgba(4, 8, 7, 0.94) 100%);
}
#home > *, #stagemap > *, #tables > *, #setup > * { position: relative; }

/* The wordmark. Serif, spaced, brass — a billiard hall sign, not a
   UI label. The rule under it carries a ◆, which is the rail sight
   marker the table itself is painted with; the logo and the object
   are then made of the same vocabulary, which is most of what a
   mark has to do. Georgia is on every platform we ship to, so this
   costs no font request in a game whose whole delivery is a ZIP. */
#home h1 {
  margin: 0;
  font-family: Georgia, "Times New Roman", serif;
  font-weight: 400;
  font-size: clamp(21px, 6.2vmin, 40px);
  line-height: 1.06;
  letter-spacing: 0.19em;
  text-indent: 0.19em;
  text-transform: uppercase;
  color: #e2c07d;
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.7);
}
#home h1 span { display: block; white-space: nowrap; }
.sight {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: min(72%, 260px);
  margin: 9px auto 8px;
  color: rgba(226, 192, 125, 0.55);
  font-size: 9px;
}
.sight::before, .sight::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(226, 192, 125, 0.5), transparent);
}
#home p {
  margin: 0;
  max-width: 30ch;
  color: var(--dim);
  font-size: clamp(13px, 3.5vmin, 15px);
}
/* The type sits above centre and the buttons below it, with the
   middle band left alone — that band is where the table's own balls
   are, and the one thing this screen is selling is that they are
   there. */
.top { margin-top: clamp(14px, 6vh, 54px); }
.grow { flex: 1; min-height: 12px; }
#home-actions {
  display: flex;
  flex-direction: column;
  gap: 9px;
  width: min(100%, 320px);
  margin-bottom: clamp(8px, 4vh, 26px);
}
#home-actions .row { display: flex; gap: 9px; }
#home-actions .row > button { flex: 1; }
#home-actions button { padding: 11px 14px; font-size: 14px; }
/* Including the quiet ones. The phone stylesheet trims every button
   to 3px of padding to buy header height, and it caught these two —
   Tables and the sound toggle came out 27px tall on a 390px phone,
   well under the 44px a thumb needs, and Tables is a whole screen
   of unlockable content presented as a link. The header is where
   the height pressure is; the home screen has room. */
#home-actions button { min-height: 44px; }
#home-actions button.primary { font-size: 16px; padding: 14px 18px; }
#home-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  color: var(--dim);
  font-size: 12px;
  margin-bottom: max(6px, env(safe-area-inset-bottom));
}
#home-foot button { padding: 8px 12px; font-size: 12px; min-height: 44px; }
.stars-total { color: #ffd479; letter-spacing: 0.04em; }

/* --- stage map ---------------------------------------------------
   A grid, because the question it answers is "how much of this is
   there and how much have I done" and a grid answers that in one
   look where a list does not. Two columns on a phone, four when
   there is room; the tiles keep a fixed aspect so the grid reads as
   a board rather than as a stack of cards. */
/* Three columns, and the third is empty on purpose: it is the same
   width as the back button, so the middle column is centred on the
   SCREEN rather than on what is left over beside a button. A title
   "centred" in a flex row with one control on one side is off by
   half that control every time. */
#stagemap header, #tables header {
  display: grid;
  /* The in-game `header` rule names six areas, and named areas
     EXTEND the explicit grid: without this reset these two screens
     inherited three extra 0px columns and the middle one stopped
     being the middle. */
  grid-template-areas: none;
  grid-template-columns: 44px minmax(0, 1fr) 44px;
  align-items: center;
  gap: 12px;
  width: min(100%, 620px);
  padding: 2px 2px 10px;
}
.htitle {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 12px;
  min-width: 0;
}
/* Letter-spacing is added AFTER the last letter too, so a centred
   line of tracked-out type sits half that spacing to the left of
   where it looks like it should. Cancel it on whichever element
   ends the row. */
.htitle h2:last-child { margin-right: -0.16em; }
.htitle .stars-total:last-child { margin-right: -0.04em; }
/* The way out comes FIRST, on the left, with an arrow on it.
   It used to sit at the end of the row after the title and the star
   count, at the same weight as the text beside it, and it read as a
   third piece of metadata rather than as the control that leaves
   the screen. Position is most of what makes a button a button:
   top-left, before the title, is where every reader of every app
   already looks for the way back. */
button.back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  min-height: 40px;
  padding: 0;
  border-radius: 11px;
  color: var(--ink);
  background: rgba(232, 238, 240, 0.09);
  border-color: rgba(232, 238, 240, 0.3);
}
/* Drawn, not typed. A "<" is a comparison operator at whatever
   weight the font feels like and a chevron glyph is a different
   shape in every font stack; two borders on a rotated square is the
   same chevron everywhere, and it scales with the button. */
button.back .chev {
  width: 9px;
  height: 9px;
  border-left: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  margin-left: 3px;
  opacity: 0.85;
}
button.back:hover { background: rgba(232, 238, 240, 0.15); }
button.back:hover .chev { opacity: 1; }
#stagemap h2, #tables h2 {
  margin: 0;
  font-family: Georgia, "Times New Roman", serif;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  font-size: 17px;
  color: #e2c07d;
}
#map-tabs {
  display: flex;
  gap: 6px;
  width: min(100%, 620px);
  padding-bottom: 9px;
}
.mtab {
  appearance: none;
  flex: 1;
  padding: 7px 4px 8px;
  border-radius: 10px;
  border: 1px solid rgba(232, 238, 240, 0.16);
  background: rgba(13, 24, 20, 0.94);
  color: rgba(232, 238, 240, 0.6);
  font: inherit;
  font-size: 13px;
  letter-spacing: 0.04em;
  cursor: pointer;
}
.mtab.on {
  color: var(--ink);
  border-color: #58c493;
  box-shadow: 0 0 0 1px #58c493 inset;
}
/* A locked difficulty stays visible: what the game still has is
   worth seeing, and a tab that vanishes reads as a tab that broke. */
.mtab:disabled { opacity: 0.4; cursor: default; }
#stage-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 9px;
  width: min(100%, 620px);
  overflow-y: auto;
  padding-bottom: max(10px, env(safe-area-inset-bottom));
}
@media (min-width: 620px) { #stage-grid { grid-template-columns: repeat(4, 1fr); } }
.tile {
  appearance: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  text-align: left;
  padding: 10px 12px 11px;
  border-radius: 12px;
  border: 1px solid rgba(232, 238, 240, 0.16);
  background: rgba(13, 24, 20, 0.94);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  /* Two short lines, a number and three stars. It was 84px when a
     tile also carried a stage NAME; with a hundred numbered stages
     the grid's job is to fit on a screen. */
  min-height: 64px;
}
.tile .no {
  font-family: Georgia, "Times New Roman", serif;
  font-size: 24px;
  line-height: 1;
  color: rgba(232, 238, 240, 0.5);
}
.tile .st { font-size: 13px; letter-spacing: 2px; color: #ffd479; margin-top: auto; }
.tile .st.none { color: rgba(232, 238, 240, 0.25); }
/* Cleared tiles carry the cloth's own green rather than a checkmark:
   the grid should read as a table filling up. */
.tile.done { background: rgba(24, 62, 47, 0.78); border-color: rgba(88, 196, 147, 0.35); }
.tile.here { border-color: #58c493; box-shadow: 0 0 0 1px #58c493 inset; }
/* A locked tile is a number and nothing else. Six tiles all saying
   "Locked" is a wall of the word Locked, which is a worse thing to
   show a new player than six quiet numbers. */
.tile.locked {
  opacity: 0.5;
  cursor: default;
  align-items: center;
  justify-content: center;
  background: rgba(10, 18, 15, 0.9);
}
.tile.locked .no { color: rgba(232, 238, 240, 0.34); }
#map-done {
  width: min(100%, 620px);
  padding: 12px 2px 2px;
  color: var(--dim);
  font-size: 13px;
  text-align: left;
}
#map-done[hidden] { display: none; }

/* Tables. One card per look, each showing the actual renderer's
   output rather than a swatch: the whole point of a table theme is
   what it looks like, so a colour chip would be describing the
   reward instead of showing it. */
#table-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  width: min(100%, 620px);
  overflow-y: auto;
  padding-bottom: max(10px, env(safe-area-inset-bottom));
}
@media (min-width: 620px) { #table-grid { grid-template-columns: repeat(2, 1fr); } }
.card {
  appearance: none;
  display: block;
  width: 100%;
  text-align: left;
  padding: 10px 10px 12px;
  border-radius: 12px;
  border: 1px solid rgba(232, 238, 240, 0.16);
  background: rgba(18, 26, 22, 0.55);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}
.card canvas {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
  background: #0b1013;
}
.card .cn { display: flex; align-items: baseline; gap: 8px; margin-top: 9px; }
.card .cn b { font-size: 15px; font-weight: 650; }
.card .cn .tag {
  margin-left: auto;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}
.card p { margin: 4px 0 0; font-size: 12.5px; color: var(--dim); }
.card.on { border-color: #58c493; box-shadow: 0 0 0 1px #58c493 inset; }
.card.on .tag { color: #58c493; }
.card.locked { opacity: 0.55; cursor: default; }
.card.locked canvas { filter: grayscale(0.65) brightness(0.7); }

/* ==== the site's top bar and its Games panel ==============================
 * DESIGN.md "Site navigation" §6. The bar's own rules, the panel's and the
 * eleven game cards' all live in /style.css, which this page now loads
 * FIRST; everything below is the standalone build's share of the work.
 *
 * There is no scroll here (html, body { overflow: hidden }), so the bar
 * cannot scroll away the way it does on a site-shell phone page. It takes a
 * real row instead and the game gets what is left — as a FLEX COLUMN, not as
 * calc(100vh - var(--nav-h)): this page has already lost pixels to 100vh on
 * iOS once, and the notch padding makes that arithmetic wrong twice.
 * ------------------------------------------------------------------------- */
body { display: flex; flex-direction: column; }
/* was height: 100% */
#app { flex: 1 1 auto; height: auto; min-height: 0; }
.site-nav {
  flex: 0 0 auto;
  align-self: stretch;          /* body centres its children here */
  /* The bar pays for the notch now (this page's own capped --notch, not
   * the raw inset — see :root). */
  padding-top: var(--notch);
}
/* Only the three --nav-* values change for a dark page — that is the whole
 * point of exposing them (see /style.css :root). The MARK is deliberately
 * left alone: it is a deep-green tile with two cream shapes on every other
 * page of the site, a 65+ visitor recognises it as the same badge, and
 * re-tinting it per game would make one brand look like three. */
.site-nav {
  --nav-bg: #131c20;
  --nav-ink: var(--ink);
  --nav-border: rgba(232, 238, 240, 0.25);
}
/* The two controls are the site's own .btn, restated for a dark field: the
 * ivory-on-deep-green fill would be a second bright band across the top. */
.site-nav .btn {
  background: rgba(232, 238, 240, 0.08);
  color: var(--ink);
  border-color: rgba(232, 238, 240, 0.45);
  box-shadow: none;
}
.site-nav .btn:hover { background: rgba(232, 238, 240, 0.16); }
.site-nav .btn:active {
  background: rgba(232, 238, 240, 0.24);
  color: var(--ink);
  transform: translateY(1px);
}
.site-nav .nav-brand,
.site-nav .nav-brand:visited { color: var(--ink); }

/* IT HIDES DURING PLAY. Set by src/main.js when a table screen opens and
 * cleared on the way back to home / stage select, because the canvas geometry
 * is measured in JS and a permanent band above a surface you drag on is a
 * mis-tap hazard. Every toggle is followed by this game's own remeasure in
 * the same frame — see main.js. Mid-game the panel is still one tap away
 * from the game's own menu. */
body[data-nav="hidden"] .site-nav { display: none; }

/* The panel is the site's cream sheet, and this file redefines --ink (and
 * --accent) for a dark table — so both go back to /style.css's values for
 * the length of the panel, or its text is near-white on cream. */
.nav-panel {
  --ink: #0f2419;
  --accent: #f2b705;
  /* Not only for the rules that name --ink: the panel head's <h2> and the
   * language heading have no colour of their own and inherit it from body,
   * which on this page is near-white. Without this the word "Games" was
   * invisible on its own cream sheet. */
  color: var(--ink);
}
/* …and #app must not pay for it a second time while the bar is on screen.
 * Scoped to #app so the four other places that spend --notch (the landscape
 * and short-screen queries) keep working from one declaration. */
body:not([data-nav="hidden"]) #app { --notch: 0px; }

/* THE HOME SCREEN MAY SCROLL WHEN IT DOES NOT FIT, rather than clip a
 * control. It is `position: absolute; inset: 0; overflow: hidden` over the
 * table, and on a 390px-tall landscape phone its content was already 14px
 * over before the bar existed: #home-foot (the star total, Tables, Sound)
 * sat just off the bottom edge. Take another 48px for the bar and the whole
 * row is gone, which is a control a player cannot reach — so on a short
 * screen the poster scrolls. Only #home: the other three overlays own their
 * own scrolling lists. */
@media (max-height: 560px) {
  #home { overflow-y: auto; }
}

/* /style.css's mobile block puts 64px of bottom padding on the page body for the
 * phone action bar it owns. There is no action bar here and no scroll to
 * absorb it. */
html, body { padding: 0; }
