/* Base elements, the ground, and the stage everything is composed to. */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;

  /*
   * NOTHING IS SCROLLED TO AND LEFT UNDER THE FOOT.
   *
   * `.table-foot` is `position: sticky; bottom: 0`, so the line telling you what
   * to do holds the bottom edge and whatever scrolls past goes *under* it. That
   * is the whole point of it and it stays.
   *
   * But "scroll this into view" means "into the VIEWPORT", and no browser knows
   * a stuck element is sitting over the place it just chose. So the thing
   * scrolled to lands at the bottom edge, underneath the foot, and every
   * mechanism built on that lands there with it: `scrollIntoView`, sequential
   * focus navigation off a keyboard, `:target` from a fragment link. **A
   * keyboard user tabbing down a lobby has the control they just focused
   * covered by the sentence telling them to press it.**
   *
   * Measured on WebKit at 750x342 — a real iPhone 13 held sideways — in the
   * Sixth Takes lobby: "add a bot" occupied y=291..339 of a 342px window and
   * `elementFromPoint` at its centre returned `P.prompt`, the foot's own
   * sentence, at y=296..334. The tap went to the paragraph. Third instance of
   * this one fault, which is why it is fixed as a class here rather than patched
   * a third time where it showed.
   *
   * `scroll-padding-bottom` is the property that exists for exactly this: it
   * tells the scroller that the last N pixels are spoken for. The shell used to
   * measure the foot and publish `--shell-foot` for it.
   *
   * ## THE FOOT IS GONE, AND THE PADDING IS STILL RIGHT
   *
   * There is no `.table-foot` any more — the prompt moved inside the game's own
   * screen — so `--shell-foot` is never published and the old value here
   * resolved to its 0px fallback. Nothing covers the bottom edge now, so the
   * fault above cannot recur. This became a flat gutter instead, for a
   * different reason that the change itself uncovered:
   *
   * **At zero, `scrollIntoView` lands things FLUSH, and flush is a sub-pixel
   * coin toss.** Measured in the in-room shelf at 1280×720 with this at 0:
   * nine of thirty-seven cards landed at `top = -0.4` and `bottom = 720.4`.
   * They are on screen to the eye and off it to arithmetic, so
   * `scrollIntoViewIfNeeded` followed by "is it inside the viewport" answered
   * no for a quarter of the shelf. It is also simply how it looks: a card
   * touching the window's edge reads as cut off whether or not it is.
   *
   * So the gutter is a real number and no longer borrowed from a measurement.
   * A scrolled-to thing arrives with a margin around it, which is where a
   * reader expects to find it.
   *
   * On `html` because the page model means the document is the scroller: the
   * owner ruled the cabinet out — "no page should have div scrollbars" — so
   * there is one scroll container and this is it.
   */
  scroll-padding-top: calc(var(--s3) + var(--stage-head, 0px));
  /*
   * The gutter, plus whatever the game's own strip is currently covering.
   *
   * `--stage-foot` is `.stage-status`'s measured height, published by
   * `shell/room.js` and zero when there is no strip. It is `--shell-foot`'s
   * successor and the difference is who owns the shadow: the site used to cast
   * it over every game, and now a game casts it over itself.
   */
  scroll-padding-bottom: calc(var(--s3) + var(--stage-foot, 0px));
}

/*
 * The UA sheet's `[hidden] { display: none }` is a single attribute selector and
 * loses to any class rule that sets `display`. That is how the connection pill
 * and the timer rail ended up rendering on the landing page — "OFFLINE" on a
 * page with nothing to connect to, and two stacked rules under the header that
 * encoded nothing. One specificity bug, three visible faults.
 */
[hidden] {
  display: none !important;
}

/*
 * The ground: saturated enamel, faintly speckled, never a page.
 *
 * `100dvh` rather than `min-height`, because the stage below measures itself
 * against it. The old ground grew with its content, which is precisely how
 * every screen ended up as a short stack floating in a tall frame.
 */
html,
body {
  height: 100%;
}

/*
 * ------------------------------------------------------- what is text here
 *
 * The owner, on a real iPhone: elements are "accidentally selected". Long-press
 * anything on a board and iOS starts a text selection, raises the callout menu
 * over the thing you were pressing, and — the part that matters most — CANCELS
 * the pointer stream, which is why a one-finger drag on the belt could not pan.
 * One default, two of the three complaints.
 *
 * So the rule is inverted from the browser's: **this is an interface, not a
 * document.** Nothing is selectable unless it is prose or a fact somebody would
 * want to lift, and those are named below rather than left to chance.
 *
 * `-webkit-tap-highlight-color: transparent` goes with it. Every pressable
 * object here already answers a press — `.btn:active` sinks into its own dent,
 * the rimmed field controls translate a pixel — and the grey box iOS paints
 * over the top of that is both redundant and square on objects that are not.
 */

/*
 * Prose is prose. Anything a person might quote out of a screen keeps the
 * browser's behaviour: the how-to-play copy, a game's blurb on the shelf, the
 * log of what happened at the table.
 */
p,
h1,
h2,
h3,
li,
dt,
dd,
code,
pre,
blockquote {
  user-select: text;
  -webkit-user-select: text;
}

/*
 * And the facts worth lifting. The room code above all: the whole product is
 * six characters read out or pasted into a chat window, and a room code you
 * cannot select is a room code you have to retype. It gets the callout menu
 * back too, so iOS still offers Copy on a long press — this is the one place on
 * the site where that menu is the right answer.
 *
 * The share link beside it, for the same reason. (There is a Copy button as
 * well; a button that needs clipboard permission is not a substitute for being
 * able to select six characters.)
 *
 * Inputs are not an exception being granted, they are a thing that breaks
 * without it: `user-select: none` on a text field takes the caret with it.
 */
.code,
.share-link,
input,
textarea {
  user-select: text;
  -webkit-user-select: text;
  -webkit-touch-callout: default;
}

/*
 * Controls are objects, never text — including their own labels, which are
 * often a `<p>` or a `<span>` that the prose rule above would otherwise have
 * re-enabled. Same for every SVG: a drag across the board crosses hundreds of
 * them, and a `<text>` node inside a hex is a number painted on a tile, not a
 * word.
 */
button,
button *,
[role="button"],
[role="button"] *,
.btn,
.btn *,
label,
svg,
svg * {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

body {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  margin: 0;
  /*
   * THE PAGE BEHIND THE TABLE, AND IT IS A SHADE INTO THE GROUND.
   *
   * It was `--table` — the same value as the table itself — so the only thing
   * separating the main container from the page was its rim and its shadow.
   * Measured off the supplied design, that is not what it does there: the page
   * behind is #f1e5d4 and the container is #fdf6eb, so the container is the
   * BRIGHTER of the two and reads as a lit surface laid on a darker one.
   *
   * `--table-deep` rather than a hex, because the relation has to hold in every
   * palette and both lights: by daylight it is `--light-ground-deep`, in the
   * evening `--deep-2`, and both are authored darker than the ground they are
   * named after. So the container is brighter than its page in all twenty-six
   * rooms, and there is nothing per-palette to keep in step.
   *
   * This is NOT the move that was tried and rejected above `.table` — that one
   * darkened the TABLE, which put it at the value of a card back and made a
   * card read as a hole rather than an object. The table is untouched; what
   * moved is the surface underneath it.
   */
  background-color: var(--table-deep);
  background-image: var(--grain);
  background-size: var(--grain-size);
  background-position: var(--grain-pos);
  background-attachment: fixed;
  color: var(--on-table);
  font-family: var(--font-body);
  font-size: var(--t-body);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: var(--track-display);
  line-height: 1;
  margin: 0;
  color: var(--on-table-strong);
  font-size: var(--t-display);
}

p {
  margin: 0;
  text-wrap: pretty;
}

a {
  color: var(--stamp);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--stamp) 40%, transparent);
}

@media (hover: hover) {
  a:hover {
    color: var(--stamp-bright);
    border-bottom-color: var(--stamp-bright);
  }
}

/*
 * A pressed link, for a finger. `:hover` never fires on touch, and the grey box
 * iOS used to paint over a tapped link is gone with the tap highlight — so
 * without this a link is the one thing on the site that answers a thumb with
 * nothing at all. Buttons already sink into their own dent (`.btn:active`).
 */
a:active {
  color: var(--stamp-bright);
  border-bottom-color: var(--stamp-bright);
  opacity: 0.75;
}

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

input,
button {
  font: inherit;
  color: inherit;
}

/*
 * One focus treatment, in stock rather than in the stamp: the ring appears
 * wherever the keyboard goes, and `--stamp` has to keep meaning "this person is
 * being singled out". A ring that borrowed it would say that on every tab stop.
 */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

:where(button, a, input):focus:not(:focus-visible) {
  outline: none;
}

/* ---------------------------------------------------------------- the rim */

/*
 * The signature, in one place.
 *
 * A bright fill inside a heavy dark edge with a line of glaze catching the
 * light just inside it — the way a chipped enamel mug is. Every object in the
 * interface takes this; what varies is only the fill and the radius.
 *
 * It is also doing structural work. The design this replaced separated objects
 * from the ground by luminance, which forced the daylight theme to keep a
 * mid-tone ground it could never brighten. A rim separates an object from
 * anything, so the two themes are free to be a real inversion.
 */
.enamelled {
  border: var(--rim-w) solid var(--rim);
  border-radius: var(--r-panel);
  box-shadow: var(--glaze-line);
}

/* Small objects — a chip, a badge, a seat — take a lighter edge. At 2px a
   16px-tall chip is more rim than chip. */
.enamelled--fine {
  border-width: var(--rim-hair);
  border-radius: var(--r-chip);
}

/*
 * A GAME'S OWN SURFACE, LAID ON THE ROOM'S TABLE.
 *
 * The same three properties one size up, and it exists because a baize is the
 * one object in this product that gets *no help at all* from its fill.
 *
 * ## What went wrong without it
 *
 * A game that wants a table paints its stage from the `--deep*` family — the
 * saturated shelf this design keeps its grounds on — and the evening theme
 * spends all four of those on structural layers: `--table: --deep`,
 * `--table-deep: --deep-2`, `--panel: --deep-lift`, `--rule: --deep-pale`. So
 * after dark a baize is painted the colour of some layer of the room it is
 * lying in. MEASURED across all fourteen palettes on 2026-08-26: `--deep` is
 * **1.00:1** against the room's table — Secret Codes' board had no edge at
 * all — and the best of the four, `--deep-pale`, reaches 2.13. By daylight the
 * room is cream, none of them collides, and the worst is 8.64:1, which is how
 * this survived three sessions of being looked at.
 *
 * **There is no token that answers it**, and that is the useful half of the
 * measurement: a fill cannot separate a dark object from a dark ground.
 *
 * ## Which is what the paragraph above `.enamelled` already said
 *
 * *"A rim separates an object from anything, so the two themes are free to be a
 * real inversion."* The house has had the answer since the rim was written; the
 * three games with a baize simply were not taking it. Secret Codes rimmed
 * itself in `--deep-lift` — a lighter member of the ground's own family, which
 * is by construction close to the ground — Word Tiles had no border at all, and
 * none of the three cast anything.
 *
 * ## The three properties, and why all three
 *
 * `--rim` is the heavy dark edge; the glaze is the hairline of light caught
 * just inside it, and it is the half that does the work after dark, because
 * near-white on near-black is a step no ground can flatten; `--lift-sm` is the
 * cloth being ON the table rather than inlaid into it. `.table` — the room's
 * own surface, the largest object of this kind there is — takes exactly these
 * three, and it reads in both lights on a 1.27:1 fill. That is the existence
 * proof, and it is the reason the fix is a class rather than a colour.
 *
 * `--glaze-line-deep` and not `--glaze-line`: same line, same job, and a
 * stronger alpha because the fill under it is the darkest in the product. The
 * measured table is beside that token in `tokens.css`; the short version is
 * that at 20% Mosaic's edge reads 2.57:1 against a floor of 3.
 *
 * `--r-field` and not `--r-panel`: a stage is a field, and the three games that
 * had a radius had already chosen that one.
 */
.baize {
  border: var(--rim-w) solid var(--rim);
  border-radius: var(--r-field);
  box-shadow: var(--glaze-line-deep), var(--lift-sm);
}

/*
 * THE SAME EDGE, ON A SURFACE THAT CANNOT SPEND THE PIXELS.
 *
 * A border costs four pixels of inner width and four of inner height, and for
 * three of the four games that took `.baize` that is nothing. For two others it
 * is the reason they have gone without an edge, and both wrote the price down
 * rather than paying it:
 *
 *   Word Tiles — `scrabble.css`: *"No border for the same reason: two more
 *     pixels is two more pixels."* Its board already loses 6 of 49 squares to a
 *     sticky write row pinned over them, and `--s5` of table padding took that
 *     to 13. Height on that stage is measured in squares nobody can press.
 *   Pool — `.pl-stage` is one property short of a baize and the property it is
 *     short of is the one that does the work. Its canvas is `inset: 0`, so a
 *     border shrinks the felt, and that felt's geometry is the acceptance gate
 *     for the whole rebuild, measured in `pool-lab.spec.ts` at two phone
 *     viewports. Four pixels is probably nothing, and "probably" is not how
 *     those numbers were arrived at.
 *
 * `tests/e2e/a-game-has-a-table.spec.ts` carried both on `EDGE_OWED`, and its
 * note beside Word Tiles named the answer without building it: **a rim that
 * costs no height is the shape of the answer.** This is that rim.
 *
 * ## Why a pseudo-element and not the two obvious things
 *
 * `outline` with a negative offset draws inside the box for free and is the
 * first thing to reach for. It cannot carry the glaze, and the glaze is the
 * half that does the work after dark — a dark ring on a dark ground is the
 * fault, not the fix.
 *
 * An inset `box-shadow` on the element itself carries both and is painted
 * BETWEEN the background and the children. Pool's canvas is the whole element,
 * so on the one surface this exists for, the edge would be behind the felt. A
 * pseudo-element is generated last and positioned, so it paints over every
 * child, which is what an edge has to do to a board drawn out to it.
 *
 * ## What it costs instead, since nothing is free
 *
 * Content flush to the boundary is overdrawn by 2px rather than pushed off it.
 * On a table that is what a table edge does; on a surface whose last 2px carry
 * something a player must read, `.baize` and its border is still the right
 * class. `pointer-events: none` keeps the ring out of every hit test, which
 * matters most on exactly the two stages here: one is a canvas that arbitrates
 * every finger, and the other already has six squares answering
 * `elementFromPoint` with the wrong element.
 *
 * The lift stays on the element, because a shadow cast by a pseudo-element
 * inset to the box is a shadow cast from inside the cloth.
 */
.baize-flush {
  position: relative;
  border-radius: var(--r-field);
  box-shadow: var(--lift-sm);
}

.baize-flush::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  /*
   * The rim is FIRST and therefore on top: it covers the outer 2px of both
   * glaze shadows, which is what leaves the hairline sitting just inside it
   * rather than under it. Reordering these paints a 3px band of light with a
   * dark line lost somewhere behind it.
   */
  box-shadow: inset 0 0 0 var(--rim-w) var(--rim), var(--glaze-line-deep-flush);
}

/*
 * There was a `.field` region primitive here — a full area of one material,
 * meant to be the thing screens compose from. It is gone for two reasons.
 *
 * It collided: `.field` was already the class for a form's label-plus-input,
 * so every form field silently inherited a region's rim and background, which
 * is what drew a heavy rounded box straight through the "Your name" label on
 * the join screen.
 *
 * And it was never used. Every region in the interface — the invitation, the
 * lobby, the low edge of a round, the panel on the join screen — wants its own
 * padding, flow and anchoring, so each is styled where it lives. A shared class
 * that nothing shared was one indirection for no reuse.
 *
 * What the regions *do* share is `--r-field` and the rim, and those are tokens.
 */

/* ------------------------------------------------------- printed labels */

/*
 * Data is mono, uppercase and tracked out. It marks every machine fact — room
 * code, clock, counts — and every field label, which is what makes the
 * interface read as a component that had to survive translation.
 *
 * ## …AND "EVERY FIELD LABEL" IS THE HALF THAT WAS WRONG
 *
 * `docs/design-verdict.md` §1 counted what that clause actually produced. In
 * the lobby, in reading order:
 *
 *     READ THIS OUT ON THE CALL · COPY LINK · SEE IT PLAYED · WORD TILES ·
 *     HOW LONG IS A ROUND · THIRTY SECONDS · FORTY-FIVE SECONDS · A MINUTE ·
 *     HOW MANY ROUNDS · SOMEBODY SHORT? ASK ONE OF THESE · DEAL WORD TILES
 *
 * Eleven mono-caps labels on one screen, and only one of them — none of them —
 * is a machine fact. The measurement, and it is a measurement rather than a
 * taste: *"Uppercase removes word-shape, which is what fast readers actually
 * read; mono removes the proportional spacing that makes a phrase look like a
 * phrase. So our labels are literally slower to read and emotionally flat. We
 * adopted mono-caps as 'data' styling and then let it escape into every label
 * in the product."*
 *
 * The rule that came out of it, and it is the house rule now: **mono is for
 * machine facts — a room code, a clock, a score. Never for an instruction, a
 * heading or a button.** `.data` keeps its dress and loses that clause; a label
 * takes `.said` below, and a control is fixed in `.btn`.
 */
.data {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--t-data);
  letter-spacing: var(--track-data);
  text-transform: uppercase;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

/*
 * ...AND ANYTHING A PERSON WOULD SAY OUT LOUD.
 *
 * The other half of the rule above, as a class rather than as a note in a
 * review. "Read this out on the call" and "Who is in" are sentences addressed
 * to a colleague, so they are set the way this product sets a sentence:
 * proportional, sentence case, the body face. Nothing here is a machine fact
 * and nothing here is tracked out.
 *
 * A step under the body copy in size and above it in weight, so a label still
 * reads as a label — the job `.data` was doing correctly and in the wrong
 * voice. `--dim`, like `.data`, because the thing under a label is what the
 * label is for.
 *
 * DELIBERATELY NOT A `.data--` MODIFIER. It is not data, and a modifier that
 * turned off mono, tracking and uppercase would be three declarations spent
 * undoing three, in a class whose name would still be lying about what it is.
 */
.said {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--t-small);
  letter-spacing: normal;
  text-transform: none;
  color: var(--dim);
}


.data--sm {
  font-size: var(--t-data-sm);
  letter-spacing: var(--track-label);
}

.data--loud {
  font-weight: 600;
  color: var(--on-table-strong);
}

/* A label printed *on* glaze rather than on the ground. */
.data--print {
  color: rgb(8 22 29 / 0.72);
}

.muted {
  color: var(--dim);
}

.measure {
  max-width: var(--measure);
}

/*
 * SAID, NOT SHOWN.
 *
 * The clip that takes a phrase out of the picture and leaves it in the
 * accessible tree, for the cases where the ARRANGEMENT of the screen carries a
 * fact that the words alone do not — the host's star in the roster is the
 * standing example (`shell/chrome.js`), and the room's live regions are the
 * other.
 *
 * It is the right primitive and it is also a trap, which is what the block
 * below exists to get out of: applied to something FOCUSABLE it produces a
 * control a keyboard can reach and an eye cannot follow. The product's own skip
 * link was that for its whole life.
 */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/*
 * THE SKIP LINK: THE ONE THING IN THE DOCUMENT THAT MUST APPEAR WHEN IT IS
 * FOCUSED.
 *
 * Everything else in this interface is hidden or shown by what is happening in
 * the room. This is hidden by there being nothing to skip and shown by somebody
 * looking for it, and the person looking for it is looking at the screen — so
 * `.visually-hidden` is exactly half a skip link, and the half it was missing
 * is the half a sighted keyboard user needs. `index.html` records the three
 * separate faults this had.
 *
 * `:focus` and not `:focus-visible`. Everywhere else the narrower selector is
 * right, because a ring around a button somebody just clicked is noise. Here
 * the element is invisible at rest, so the moment it holds focus at all it has
 * to be on screen — `:focus-visible` would leave a programmatic `.focus()` on a
 * 1px box, which is the fault being fixed rather than a smaller version of it.
 *
 * It sits ON the table's top-left corner rather than above it, so it lands
 * where the eye already is after the address bar, and it is an ordinary
 * enamelled object because it is an ordinary control that is usually elsewhere.
 * `z-index` beats the reconnecting pill (200), which is otherwise the highest
 * thing in the product: a game may hold the whole viewport and this must still
 * be the first thing Tab reaches and the first thing it draws.
 */
.skip-link {
  position: fixed;
  z-index: 300;
  top: calc(var(--s2) + var(--safe-t));
  left: calc(var(--s2) + var(--safe-l));
  /* Off the glass rather than clipped to a pixel: it keeps its real box, so
     what appears on focus is the object at its true size rather than something
     that has to be rebuilt by a second rule. */
  transform: translateY(calc(-100% - var(--s4) - var(--safe-t)));
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--s2) var(--s4);
  background: var(--primary);
  color: var(--on-primary);
  border: var(--rim-w) solid var(--rim);
  border-radius: var(--r-control);
  box-shadow: var(--glaze-line), var(--lift-sm);
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: var(--t-data);
  letter-spacing: var(--track-data);
  text-transform: uppercase;
  text-decoration: none;
  transition: transform var(--fast) var(--ease);
}

.skip-link:focus {
  transform: translateY(0);
}

/* ------------------------------------------------------------- motion */

/* One orchestrated arrival, not scattered micro-motion. */
@keyframes deal {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/*
 * `backwards`, NOT `both` — and the difference is a hover rule that had never
 * once fired.
 *
 * A filling animation does not merely set a property, it OWNS it: a
 * `forwards` fill wins over every ordinary declaration for as long as the
 * element lives, because animations sit above the cascade. The `to` frame
 * above is `transform: none`, so `both` handed `transform` to this animation
 * permanently on all eight `.deal` elements — and `.game-card:hover` asks for
 * `translateY(-2px)`.
 *
 * So the front door's boxes never lifted. Measured on a desk at 1280×800 with
 * the pointer on a card and `:hover` confirmed matching: `transform` reads
 * `matrix(1, 0, 0, 1, 0, 0)`. The `box-shadow` half of the same rule fires
 * perfectly, which is exactly why it survived — the card visibly answers the
 * pointer, so nobody looking at it would say the lift was missing rather than
 * subtle. It took a test reading the matrix to say the movement was zero.
 *
 * `backwards` fills only the delay before the animation starts, which is the
 * half that has a job: eleven boxes staggered up to 640ms apart must not be
 * visible at full opacity while they wait their turn. After the run they go
 * back to their own declared values — and `opacity: 1` / `transform: none` are
 * the defaults, so nothing on any of the eight moves. What changes is that the
 * two properties belong to the stylesheet again.
 */
.deal {
  animation: deal var(--slow) var(--ease) backwards;
}

.deal-1 {
  animation-delay: 50ms;
}
.deal-2 {
  animation-delay: 120ms;
}
.deal-3 {
  animation-delay: 200ms;
}
