/*
 * The app shell, in CSS.
 *
 * Loaded after tokens.css and ui.css and defines no colour, duration or size
 * of its own. Same rule as the table: a value that is not in the tokens goes
 * into the tokens first.
 *
 * The grammar here is not the table's. The table is a stage; these screens are
 * a terminal. design/DIRECTION.md section B2 records where that comes from and
 * what the player has already internalised from the tools they use daily:
 *
 *   rows, not cards            a card wastes vertical space and this user scans
 *   numbers are the interface  tabular, right aligned, k and M for magnitude
 *   green and red mean P&L     and nothing else in the interface may use them
 *   one saturated accent       volt, for action only
 *   two radii                  small on dense things, large only on sheets
 *   text labels in navigation   zero learning curve, and no icon to decode
 *   quick action inside the row  so joining never costs a navigation
 *
 * The last one on that list is also the reason the feed is the home screen:
 * a player who can see hands resolving and sit down from the same row never
 * has to be sold anything.
 */

/* ------------------------------------------------------------------ shell */

/*
 * Three rows: header, scrolling outlet, tab bar.
 *
 * `100dvh` and not `100vh`. The dynamic unit tracks the collapsing browser
 * chrome, which the static one does not, and a fixed tab bar sized against
 * `100vh` sits under the address bar on every mobile browser at some point in
 * the scroll. Inside Telegram neither is right, so the shell prefers the
 * client's own measurement when it is there. See DIRECTION section B1.
 */
.app {
  --shell-header: 52px;
  --shell-tabs: 56px;
  /*
   * The header's own height plus whatever sits above it.
   *
   * `--tg-safe-top` is the notch plus Telegram's own chrome, summed by
   * `telegram.ts`, and the header pays for it with `padding-block-start`. That
   * padding used to be spent inside a grid row fixed at 52 px, under an `.app`
   * that clips, so on a notched iPhone inside Telegram the top of the header
   * was simply cut off and the balance went with it. The inset belongs to the
   * row, not to the padding alone. On the open web the variable is unset and
   * this is 52 px exactly, which is what it always was.
   */
  --shell-header-row: calc(var(--shell-header) + var(--tg-safe-top, 0px));

  display: grid;
  /* Header, the scrolling outlet, the one big control, the tab bar. The
     control has a row of its own rather than floating over the outlet,
     because a button that covers the last row of a list is a button that
     hides the thing somebody scrolled to. */
  grid-template-rows: var(--shell-header-row) 1fr auto auto;
  /*
   * Each band names its own row.
   *
   * They were placed by document order, which is fine until one of them is
   * hidden: `display: none` takes a child out of the grid, every child after
   * it shifts up a row, and the outlet lands in the header's 52 px. Measured
   * exactly that way, as a 16 px arena at 844x390, the first time the header
   * was hidden for a hand.
   */
}
.app > .header { grid-row: 1; }
/*
 * The header's row collapses with it.
 *
 * Hiding the child is not enough: the track is declared at 52 px and a grid
 * keeps an empty track. On a phone held sideways that is thirteen per cent of
 * the screen reserved for nothing, which is most of why the table felt cramped
 * in that orientation.
 */
:root[data-hand="live"] .app { grid-template-rows: 0 1fr auto auto; }
.app > .outlet { grid-row: 2; }
.app > .play-cta { grid-row: 3; }
.app > .tabs { grid-row: 4; }
.app {
  block-size: 100dvh;
  block-size: var(--tg-viewport-stable-height, 100dvh);
  max-block-size: 100dvh;
  max-block-size: var(--tg-viewport-stable-height, 100dvh);
  overflow: hidden;
  background: var(--ground);
}

/* ----------------------------------------------------------------- header */

/*
 * Two things, and the balance is not one of them.
 *
 * The clock that opens tables, and the way to connect an account. Every one of
 * the twenty two apps measured in design/REFERENCES-MOBILE-POKER.md puts the
 * table's identity in the top strip and not one of them puts the player's
 * money there. The balance moved to a box in the tab bar, which is where the
 * owner asked for it and where the category already agrees it belongs.
 *
 * Time on device and net result left with it, to the profile, one tap from
 * every screen. The Behavioural Insights Team audit in DIRECTION section A4
 * asks for both in the header or one tap away, and it is the only item on that
 * list this product has ever had to move rather than invert.
 */
.header {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-inline: var(--space-gutter);
  /* Telegram's own header sits above ours inside the Mini App. */
  padding-block-start: var(--tg-safe-top, 0px);
  background: var(--ground);
  /* No border. Surfaces separate by lightness here, and the header sits on
     the same ground as the scroll behind it: what separates them is that the
     content moves and this does not. A hairline under a header is the
     generated-page tell from DIRECTION C3. */
}

.header > .mark {
  /*
   * It is a link home, so it is a touch target, so it is 44 px tall whatever
   * the type size is. The harness caught this at 25 px: the text was the right
   * size and the box around it was not, which is exactly the class of defect
   * that survives every review because nothing about it looks wrong.
   */
  display: inline-flex;
  align-items: center;
  min-block-size: var(--space-touch);
  font-family: var(--font-display);
  font-weight: var(--font-weight-display);
  font-stretch: var(--font-stretch-wordmark);
  letter-spacing: var(--font-tracking-display);
  font-size: var(--step-1);
  color: var(--ink);
  text-decoration: none;
  /* The one place volt appears in the header, and only as a dot: the mark is
     identity, not an action, and volt means action everywhere else. */
}

.header > .mark::after {
  content: "";
  display: inline-block;
  inline-size: 5px;
  block-size: 5px;
  margin-inline-start: 3px;
  border-radius: 50%;
  background: var(--volt);
  vertical-align: 3px;
}

.header > .spacer { flex: 1; }

/* Two stacked figures: the number on top, its name under it. The name is the
   small one because after the first session nobody reads it again. */
.stat {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  line-height: 1.1;
}

.stat > .v {
  font-size: var(--step-0);
  font-weight: var(--font-weight-display);
  color: var(--ink);
}

.stat > .k {
  font-size: 10px;
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  color: var(--ink-faint);
  font-family: var(--font-display);
  font-weight: var(--font-weight-label);
}

/*
 * Net result colour. The whole reason `store.ts` computes `net` instead of
 * accepting a verdict.
 *
 * `push` is deliberately not green. A hand that returns exactly what it took
 * is not a win, and colouring it as one is the Losses Disguised as Wins
 * pattern in its cheapest form.
 */
.stat[data-kind="win"] > .v { color: var(--win); }
.stat[data-kind="loss"] > .v { color: var(--danger); }
.stat[data-kind="push"] > .v { color: var(--ink-muted); }

/*
 * The protocol clock, and the two places it may never be.
 *
 * DIRECTION section A4 records the Behavioural Insights Team audit of twenty
 * five at risk design features. Two of them are urgent re-engagement prompts
 * immediately after a bet and countdown timers that amplify them. A clock
 * counting down beside the number a player has just won or lost is both at
 * once, so it leaves the document for the whole of a live hand and for the
 * whole of the result.
 *
 * `data-hand` is written by screens/play/table-screen.ts and `data-result` by
 * screens/play/game-screen.ts. The rule lives here, in one place, rather than
 * in whichever screen remembers it.
 */
:root[data-hand="live"] .header > .stat[data-clock="protocol"],
/*
 * The connect panel goes for the length of a hand.
 *
 * The button itself stays and becomes the chat, which is the owner's own
 * answer: reuse the control rather than delete it and leave a hole. The panel
 * behind it is the thing that has no business over a table.
 */
:root[data-hand="live"] .header > .connect-note,
:root[data-result="open"] .header > .stat[data-clock="protocol"] {
  display: none;
}

/*
 * What the connect button says while there is nothing to connect.
 *
 * Out of flow, so opening it never moves the grid row under a finger that is
 * still on the button. A control that reflows the app when pressed is the one
 * defect people describe as the interface fighting them.
 */
.connect-note {
  position: absolute;
  inset-block-start: 100%;
  inset-inline: var(--space-gutter);
  margin: 0;
  padding: var(--space-md);
  border-radius: var(--space-radius-small);
  background: var(--surface-raised);
  color: var(--ink);
  font-size: var(--step-n2);
  line-height: 1.45;
  box-shadow: 0 18px 40px rgba(0, 0, 0, .5);
}

.connect-note[hidden] { display: none; }

/* ----------------------------------------------------------------- outlet */

.outlet {
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  /* Room under the last row so it clears the tab bar when scrolled to the
     end. The palm band is where the hand rests and nothing actionable lives
     there. */
  padding-block-end: var(--space-xl);
}

.screen {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  padding-inline: var(--space-gutter);
  padding-block-start: var(--space-md);
}

/* --------------------------------------------------------------- tab bar */

/*
 * Three tabs and a box, and the box is in the middle.
 *
 * Three columns rather than a flat list of four cells, because a flat list of
 * four cannot centre any of them. The left cell holds two tabs, the middle
 * cell is the chips box, the right cell holds one. That is what puts the box
 * at the centre of the bar instead of a quarter of the way across it.
 *
 * Text labels and no icons. An icon for "feed" and an icon for "you" are both
 * a guess the player has to decode, and the terminals this player already uses
 * label their navigation.
 */
.tabs {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  background: var(--surface-arena);
  padding-block-start: var(--space-xs);
  /*
   * Two safe areas, added together, with the browser's own as the fallback.
   *
   * `--tg-safe-bottom` is published by `telegram.ts` and is already the sum of
   * the hardware inset and Telegram's own chrome. On the open web it is unset
   * and `env(safe-area-inset-bottom)` takes over. Using only one of the two
   * inside Telegram puts this bar under something, and which something depends
   * on the device — which is why that bug reads as intermittent.
   */
  padding-block-end: calc(var(--space-xs) + var(--tg-safe-bottom, var(--space-safe-bottom)));
}

.tab-group {
  display: flex;
  min-inline-size: 0;
}

.tab-group > .tab { flex: 1; }

/*
 * The chips box. The word outside it, the number inside it.
 *
 * Separated from the bar by lightness and not by an edge. DIRECTION section C3
 * lists a 1 px border on every surface as the generated-design tell, and this
 * is a raised surface on an arena surface, which is the same separation the
 * rest of the app already uses.
 *
 * It is not a tab and it is not a button. It reports, and a target here that
 * navigated somewhere would be the one mis-tap a thumb reaching for the tab
 * either side of it would make.
 */
.chips {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding-inline: var(--space-md);
}

.chips-k {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.chips-box {
  min-inline-size: 96px;
  padding-block: 5px;
  padding-inline: var(--space-md);
  border-radius: var(--space-radius-small);
  background: var(--surface-raised);
  text-align: center;
}

.chips-v {
  font-size: var(--step-1);
  font-weight: var(--font-weight-display);
  line-height: 1.1;
  color: var(--ink);
}

.tab {
  appearance: none;
  border: 0;
  background: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-block-size: var(--space-touch);
  padding: 0;
  font-family: var(--font-display);
  font-size: var(--step-n2);
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  color: var(--ink-faint);
  text-decoration: none;
  cursor: pointer;
  /* Pressed dozens of times a session. Emil Kowalski's frequency rule: high
     frequency elements get no animation, because the delay is felt exactly
     where the user is watching. Colour only, and instantly. */
}

.tab[aria-current="page"] { color: var(--ink); }

/* The active marker is a bar above the label, not a filled pill. A pill needs
   a radius and a surface and both would put a second card grammar into the
   one place that has to stay quiet. */
.tab > .pip {
  inline-size: 16px;
  block-size: 2px;
  border-radius: 1px;
  background: transparent;
}

.tab[aria-current="page"] > .pip { background: var(--volt); }

/*
 * And they leave while a hand is live.
 *
 * Not one of the twenty two poker apps measured in
 * design/REFERENCES-MOBILE-POKER.md shows a global tab bar with cards on the
 * table. Ours occupied the bottom 6.2 per cent of a 390x844 screen during the
 * hand and pushed the action bar off the edge it belongs on, which costs the
 * thumb arc and invites the one mis-tap that leaves the hand.
 *
 * `data-hand` is written by `screens/play/table-screen.ts` from the phase the
 * hand broadcasts, so the bar goes when the cards come out and is back the
 * moment the hand settles, which is also when the result overlay offers its
 * own explicit way out. The table is a mode, and the way out of a mode is a
 * control, never a tab that is always there.
 */
:root[data-hand="live"] .header,
:root[data-hand="live"] .tabs,
:root[data-hand="live"] .play-cta {
  display: none;
}

/* ------------------------------------------------------------ the control */

/*
 * PLAY. One control, the colour of the mark, the widest object in the app.
 *
 * It was a tab and the owner asked for it to stop being one. A tab says "a
 * place among places" and this is not one of four places, it is the thing the
 * product is for. design/brand/BRAND.md calls volt the colour of action, and
 * this is the action, so this is where the colour is spent.
 *
 * Small radius and not large. DIRECTION section C3 asks for two radii, small
 * on dense elements and large only on sheets and modals, and the single
 * identical radius everywhere is the tell. A wide flat block at 10 px reads as
 * a key rather than as a pill.
 */
.play-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  min-block-size: 56px;
  margin-inline: var(--space-gutter);
  margin-block-end: var(--space-sm);
  border-radius: var(--space-radius-small);
  background: var(--volt);
  color: var(--volt-ink);
  text-decoration: none;
  transition: transform var(--motion-press-in) var(--motion-ease-out),
              filter var(--motion-state-swap) var(--motion-ease-out);
}

/*
 * The same one light, on the one big control.
 *
 * A lit top edge, a dark base, a shadow on the ground, and all three gone on
 * the press. It is the same rule the action bar uses, because two vocabularies
 * of depth in one product is worse than none.
 */
.play-cta {
  box-shadow:
    inset 0 1px 0 color-mix(in oklab, white 55%, transparent),
    inset 0 -3px 0 color-mix(in oklab, black 30%, transparent),
    0 3px 6px -1px color-mix(in oklab, black 45%, transparent);
}
.play-cta:active {
  translate: 0 2px;
  box-shadow:
    inset 0 1px 0 color-mix(in oklab, white 30%, transparent),
    inset 0 -1px 0 color-mix(in oklab, black 22%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  .play-cta:active { translate: none; }
}

/*
 * A tile that has been chosen lifts, and one being pressed sits down.
 *
 * The seat counts and the stakes are the two answers on the screen, so the
 * chosen one is the only object that should be above the surface. Micro and
 * measured: one pixel of lift and a shadow, which is enough to read as
 * selected without competing with the button that confirms them.
 */
.chooser > .quick > .btn,
.chooser > .tiers > .btn {
  transition:
    translate var(--motion-press-in) var(--motion-ease-out),
    box-shadow var(--motion-state-swap) var(--motion-ease-out),
    background var(--motion-state-swap) var(--motion-ease-out);
}
.chooser > .quick > .btn[aria-pressed="true"],
.chooser > .tiers > .btn[aria-pressed="true"] {
  translate: 0 -1px;
}
.chooser > .quick > .btn:active,
.chooser > .tiers > .btn:active {
  translate: 0 1px;
}
@media (prefers-reduced-motion: reduce) {
  .chooser > .quick > .btn,
  .chooser > .tiers > .btn { transition: background var(--motion-state-swap) linear; }
  .chooser > .quick > .btn[aria-pressed="true"],
  .chooser > .tiers > .btn[aria-pressed="true"],
  .chooser > .quick > .btn:active,
  .chooser > .tiers > .btn:active { translate: none; }
}

.play-cta[hidden] { display: none; }

/*
 * There is no dim state.
 *
 * It had one, volt at a third of its strength, for the frames before both
 * questions were answered. The owner asked for the button to stay volt from
 * the first frame of the chooser to the last, and the way to honour that
 * without lying is for it to always have an answer to confirm: the chooser
 * starts with both questions answered rather than with none.
 *
 * The rule is kept in the stylesheet as a rule rather than as an absence, so
 * that a future `aria-disabled` on this element is a visible decision.
 */
.play-cta[aria-disabled] { display: none; }

.play-cta-word {
  font-family: var(--font-display);
  font-weight: var(--font-weight-display);
  font-stretch: var(--font-stretch-display);
  letter-spacing: var(--font-tracking-display);
  font-size: var(--step-2);
  line-height: 1;
  text-transform: uppercase;
}

/* Three visually distinct states with a real press transform. "Buttons that
   snap instead of easing" is on the generated-design tell list. */
.play-cta:active {
  transform: translateY(var(--space-press-depth));
  filter: brightness(.92);
}

@media (hover: hover) {
  .play-cta:hover { filter: brightness(1.06); }
}

/* --------------------------------------------------------------- sections */

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
}

.section-head > h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--step-n2);
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  color: var(--ink-faint);
}

.section-head > .note {
  font-size: var(--step-n2);
  color: var(--ink-faint);
}

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

/*
 * The unit of every list in the app.
 *
 * A row is a complete record: identity, what happened, the number, and the
 * action if there is one. It is not a card. Cards would give each of these an
 * edge, a radius and a shadow, which says "separate object" four times for
 * something the eye is meant to run down in one movement.
 */
.rows {
  display: flex;
  flex-direction: column;
  border-radius: var(--space-radius-small);
  overflow: hidden;
  background: var(--surface-pod);
}

.row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-block-size: 56px;
  padding-inline: var(--space-md);
  padding-block: var(--space-sm);
  text-align: start;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  width: 100%;
}

/* Separation by lightness, and only between rows inside one block. A 1px grey
   line on every element is on the AI-slop tell list in DIRECTION C3. */
.row + .row {
  box-shadow: inset 0 1px 0 var(--line);
}

/* A row can be an anchor now, because a board row opens a profile. An
   underlined name and an underlined figure in the same row is two decorations
   on one record, and the row already says it is tappable by pressing. */
/* `hidden` has to beat `display: flex`, because the attribute's own rule in
   the user agent stylesheet loses to any class that sets display. Every other
   block in this file that hides itself says the same thing, and a list that
   ignores its own hidden attribute is a picker that opens already open. */
.rows[hidden] { display: none; }

.row { text-decoration: none; }

.row.tappable { cursor: pointer; }

.row.tappable:active {
  background: var(--surface-pressed);
}

/* Identity tile. Square with the small radius, initials in the identity face.
   Not a circle: circles read as social, squares read as records, and this is
   a ledger. */
.avatar {
  flex: none;
  inline-size: 32px;
  block-size: 32px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  background: var(--surface-raised);
  color: var(--ink-muted);
  font-family: var(--font-display);
  font-size: var(--step-n2);
  font-weight: var(--font-weight-display);
}

.row > .body {
  flex: 1;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.row .who {
  font-size: var(--step-n1);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.row .sub {
  font-size: var(--step-n2);
  color: var(--ink-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The numeric column. Right aligned and tabular so the digits line up down the
   list, which is the entire reason a terminal is readable at speed. */
.row > .figures {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  font-variant-numeric: tabular-nums;
}

.row .amount {
  font-size: var(--step-n1);
  color: var(--ink-muted);
}

.row .when {
  font-size: var(--step-n2);
  color: var(--ink-faint);
}

/* ------------------------------------------------------------- multiplier */

/*
 * The hero of the feed.
 *
 * `12.4x` travels further than `4 337` — it is comparable between players at
 * different stakes, it fits in a screenshot, and it is the number people
 * actually say out loud. Green because a multiplier above 1 is profit and
 * profit is the one thing green is allowed to mean here.
 */
.mult {
  font-family: var(--font-display);
  font-weight: var(--font-weight-display);
  font-stretch: var(--font-stretch-display);
  letter-spacing: var(--font-tracking-display);
  font-variant-numeric: tabular-nums;
  font-size: var(--step-1);
  line-height: 1;
  color: var(--win);
}

/* Past ten times the buy in the number earns the accent ring. It is rare on
   purpose: roughly one row in thirty, so it still means something when the
   eye lands on it. */
.mult.big {
  color: var(--volt);
}

/*
 * The same figure, counting something that is not money.
 *
 * The board can be ordered by chips won or by hands won, and only the first of
 * those is profit. Green means P&L in this product and nothing else, so a
 * column of hands takes the plain ink and keeps the type, which is what stops
 * the column jumping when the order changes.
 */
.tally {
  font-family: var(--font-display);
  font-weight: var(--font-weight-display);
  font-stretch: var(--font-stretch-display);
  letter-spacing: var(--font-tracking-display);
  font-variant-numeric: tabular-nums;
  font-size: var(--step-1);
  line-height: 1;
  color: var(--ink);
}

/* ---------------------------------------------------------------- chooser */

/*
 * One decision, at the size of one decision.
 *
 * This screen asks a single question and offers three answers, and the first
 * two attempts both tried to solve its emptiness by moving the buttons: pinned
 * to the top they sat furthest from the thumb, centred they were marooned with
 * half a phone of nothing above and below, and spread they made two holes
 * instead of one.
 *
 * None of that was the problem. The problem was three small controls on a
 * screen with nothing else to do. So the controls take the height. Each is a
 * row the width of the phone, the three of them share what is left after the
 * heading, and every one is an enormous target sitting in the thumb arc. The
 * emptiness is gone because it was never space, it was undersized answers.
 *
 * design/REFERENCES-MOBILE-POKER.md is the argument: every app in the set puts
 * the decision where the hand is and makes it the largest thing on the screen.
 */
.chooser,
.searching {
  min-block-size: 100%;
  padding-block-end: var(--space-xl);
}

/*
 * The two questions sit in the thumb's half of the screen.
 *
 * They are compact now, and compact content pinned to the top of a tall screen
 * leaves a hole between the last answer and the confirm button. The owner had
 * already named the same shape at the bottom of the table: a gap that large
 * reads as something missing rather than as room.
 *
 * Centred, the pair lands where a thumb already is, which is the argument
 * design/REFERENCES-MOBILE-POKER.md makes about every app in the set. The
 * heading stays at the top of the group rather than floating, because the
 * group moves as one.
 */
.chooser { justify-content: center; }

/*
 * The wait, centred, with the seats it is filling shown as seats.
 *
 * It was a counter, a bar and a Cancel pinned to the top of the screen, with
 * eight hundred pixels of black under them, and the owner read it as a screen
 * that had nothing to do with anything. He was right: it is the one moment
 * between choosing a table and playing at it, and it was showing less than the
 * chooser it came from.
 *
 * Centred like the chooser, so the two screens are one movement rather than
 * two layouts, and the ring of seats says what the number says in a way a
 * player can see filling.
 */
.searching { justify-content: center; }
.searching > .fill {
  display: grid;
  justify-items: center;
  gap: var(--space-md);
  padding: var(--space-lg) var(--space-md);
  border-radius: var(--space-radius-large);
  background: var(--surface);
  box-shadow:
    inset 0 1px 0 color-mix(in oklab, white 6%, transparent),
    0 8px 24px -12px color-mix(in oklab, black 60%, transparent);
}
/* The seats, as seats. A row of pips that fill, which is the same fact as the
   number above them and the only one a player can read at a glance. */
.searching .pips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
}
.searching .pip {
  inline-size: 10px;
  block-size: 10px;
  border-radius: 3px;
  background: var(--surface-pressed);
  box-shadow: inset 0 0 0 1px var(--line);
  transition: background var(--motion-state-swap) var(--motion-ease-out);
}
.searching .pip[data-on="1"] {
  background: var(--volt);
  box-shadow: none;
}

/*
 * Three seat counts in a row, not a column that fills the screen.
 *
 * The column is what this was when the seat count was the only question on the
 * screen: three tiles, each taking a third of the height, because there was
 * nothing else to give the height to. Both questions share the screen now, and
 * a column of three left the stake grid below the fold on a 390 px phone and
 * made the first question look four times as important as the second.
 *
 * A row of three, the same shape the stake grid below it uses, so the two
 * questions read as two questions rather than as a screen and its footnote.
 */
.chooser > .quick {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-sm);
}
.chooser > .quick > .btn { min-block-size: var(--space-touch); }

.chooser > .quick > .btn {
  /* Grid rather than flex so the pair centres on both axes at once. A flex row
     with `align-items: baseline` pins the text to the top of a tall box, which
     is what it is for and the wrong thing here. */
  display: grid;
  grid-auto-flow: column;
  place-content: center;
  align-items: baseline;
  gap: var(--space-sm);
}

/* The figure carries the row, so it is set at the display step rather than the
   one a dense control uses. A large surface with small type on it reads as a
   container waiting for content. */
.chooser > .quick > .btn > .n {
  font-size: var(--step-4);
}

.chooser > .tiers > .btn > .n {
  font-size: var(--step-2);
}

/* The six stakes share the same height, three across and two down, so the
   second question is answered at the same size as the first. */
/*
 * The six tiers keep their own height instead of taking the slack.
 *
 * `flex: 1` with `grid-auto-rows: 1fr` made the grid absorb whatever was left
 * on the screen, which was fine while it was the last thing on it. With the
 * seat counts compacted into one row above, the six tiles inherited all of
 * that slack and became 210 px tall boxes holding one number each, which is
 * the same emptiness the density ruler in the plan was written against.
 *
 * They size to their content, and the space that is left is left. A screen
 * with room on it is not a screen with a problem.
 */
.chooser > .tiers {
  grid-auto-rows: minmax(76px, auto);
}

/* ------------------------------------------------------------------ tiers */

/*
 * The six stakes, under the three seat counts.
 *
 * `STAKE_TIERS` from packages/protocol has six entries, so the grid is three
 * across and two down and the last row is always full. A grid with a hole in
 * it reads as something that failed to load.
 *
 * The figure is a step smaller than the seat count above it. `25 000` at the
 * seat count's size is 63 px wide in a 104 px column at 360, which is the
 * width the harness measures and the one that would clip.
 */
.tiers {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-sm);
}

.tiers > .btn {
  flex-direction: column;
  gap: 2px;
  min-block-size: 62px;
  padding-inline: var(--space-sm);
  background: var(--surface-pod);
}

.tiers > .btn:active { background: var(--surface-pressed); }

/*
 * The stake that is chosen, in volt.
 *
 * The seat count had a mark and the stake had none, so the screen answered one
 * of its two questions and left the other looking untouched. The owner found
 * it the way a player does: "não tem nenhuma informação visual nisso".
 *
 * The number goes volt and the tile keeps a volt keyline. The seat count above
 * uses a raised surface and a volt bar under it, and this is deliberately the
 * same vocabulary rather than the same rule: a stake is a figure and the thing
 * worth marking is the figure, not the box around it.
 */
.tiers > .btn[aria-pressed="true"] {
  background: var(--surface-raised);
  box-shadow: inset 0 0 0 1px var(--volt);
}
.tiers > .btn[aria-pressed="true"] .n { color: var(--volt); }
.tiers > .btn[aria-pressed="true"] .cap { color: var(--ink); }

.tiers .n {
  font-size: var(--step-0);
  font-weight: var(--font-weight-display);
  color: var(--ink);
  text-transform: none;
}

.tiers .cap {
  font-size: 10px;
  color: var(--ink-faint);
}

/* --------------------------------------------------------------- the wait */

/*
 * Seats arriving, then the clock.
 *
 * One object, not three cards in a row. DIRECTION section C3 puts three
 * feature cards of identical height at the top of the generated-design tell
 * list, and a waiting screen is the easiest place in an app to reach for them.
 */
.fill {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  padding-block: var(--space-xl);
  padding-inline: var(--space-lg);
  border-radius: var(--space-radius-small);
  background: var(--surface-pod);
}

.fill-count {
  font-family: var(--font-display);
  font-size: var(--step-4);
  font-weight: var(--font-weight-display);
  font-stretch: var(--font-stretch-display);
  letter-spacing: var(--font-tracking-display);
  line-height: 1;
  color: var(--ink);
}

.fill-bar {
  inline-size: 100%;
  block-size: 6px;
  border-radius: 3px;
  background: var(--surface-raised);
  overflow: hidden;
}

/* The one accent, on the one thing that is filling. It eases rather than
   stepping, because a seat arriving is a thing happening and not a frame. */
.fill-done {
  display: block;
  inline-size: 0;
  block-size: 100%;
  background: var(--volt);
  transition: inline-size 420ms var(--motion-ease-out);
}

.fill-line {
  margin: 0;
  font-size: var(--step-n1);
  color: var(--ink-muted);
  text-align: center;
}

.fill-clock {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.fill-clock[hidden] { display: none; }

.fill-clock > .v {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: var(--font-weight-display);
  font-variant-numeric: var(--font-numeric);
  line-height: 1.1;
  color: var(--ink);
}

.fill-clock > .k {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ----------------------------------------------------------------- awards */

/*
 * A step on the ladder, in the row grammar the rest of the app already uses.
 *
 * The bar is inside the row rather than beside it, so the ladder reads down
 * the screen as one list and not as four cards. ADR 0012 is explicit that a
 * badge lives on a surface a player visits to look at themselves, and a screen
 * that shouts is a screen they visit once.
 */
.row.award .meter {
  inline-size: 100%;
  block-size: 4px;
  margin-block-start: 6px;
  border-radius: 2px;
  background: var(--surface-raised);
  overflow: hidden;
}

.meter-done {
  display: block;
  inline-size: 0;
  block-size: 100%;
  background: var(--volt);
  transition: inline-size 420ms var(--motion-ease-out);
}

.meter-count {
  font-size: var(--step-n2);
  font-variant-numeric: var(--font-numeric);
  color: var(--ink-faint);
}

/* Earned takes the accent, and only earned. A ladder where every rung is lit
   is a ladder that says nothing about where you are on it. */
.meter-count.earned { color: var(--volt); }

/* The two steps that land on a real change in the product carry the accent in
   their monogram. ADR 0008's fee ladder has a step at a hundred hands and at
   five hundred, and ADR 0012 pins the tiers to it wherever it has one. */
.row.award[data-pinned] > .avatar { color: var(--volt); }

/* What cannot be counted yet says so, in the quietest ink on the screen. It is
   not a failure and it is not a zero: the showdown computes the rank and
   discards it, and until it stops doing that a flush cannot be counted. */
.meter-count.waiting {
  font-size: var(--step-n2);
  color: var(--ink-faint);
}

/* The tier reached, beside the award's name rather than inside the figure
   column. The column carries the date, and a name and a date are two different
   kinds of thing to put in one place. */
.tier-mark {
  margin-inline-start: var(--space-sm);
  font-family: var(--font-display);
  font-size: var(--step-n2);
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  color: var(--volt);
}

.tier-mark[hidden] { display: none; }

/* ------------------------------------------------------- the paste box */

/*
 * ADR 0015's development route, and it looks like one.
 *
 * No polish beyond the grammar the rest of the app already has. It is not a
 * feature of the product, it is a way to look at one before it ships, and a
 * screen that dressed itself up would be a screen somebody links to.
 */
.paste {
  inline-size: 100%;
  max-inline-size: 100%;
  padding: var(--space-md);
  border: 0;
  border-radius: var(--space-radius-small);
  background: var(--surface-pod);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: var(--step-n2);
  line-height: 1.5;
  resize: vertical;
}

.paste:focus-visible {
  outline: 2px solid var(--info);
  outline-offset: 2px;
}

.paste-acts {
  display: flex;
  gap: var(--space-sm);
}

.paste-acts > .btn { flex: 1; }

/* A refusal is read, so it takes readable ink rather than the danger colour on
   a faint grey. Red here would be the only red on a screen with no money on
   it. */
.row .sub.bad { color: var(--ink); }
.row .sub.good { color: var(--win); }

/* -------------------------------------------------------------- documents */

.doc {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.doc-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: var(--font-weight-display);
  color: var(--ink);
}

/* `text-wrap: pretty` keeps the last line of a paragraph from being a single
   word. Where it is unsupported the text is merely normal. */
.doc-line {
  margin: 0;
  font-size: var(--step-n1);
  line-height: 1.55;
  color: var(--ink-muted);
  text-wrap: pretty;
}

/* The quiet line under a block. Smaller than the content and never beside it,
   so it reads as a note about the thing rather than as part of it. */
.fine {
  margin: 0;
  font-size: var(--step-n2);
  line-height: 1.5;
  color: var(--ink-faint);
  text-wrap: pretty;
}

/* ---------------------------------------------------------------- actions */

/*
 * Buttons.
 *
 * Three visually distinct states, with a real press transform. "Buttons that
 * snap instead of easing" and "hover states that do nothing" are both on the
 * generated-design tell list, and the press is the one animation a
 * high-frequency control is allowed: it is synchronous with the finger, so it
 * reads as the object giving rather than as the interface thinking.
 */
.btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  min-block-size: var(--space-touch);
  padding-inline: var(--space-lg);
  border: 0;
  border-radius: var(--space-radius-small);
  background: var(--surface-raised);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: var(--step-n1);
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  cursor: pointer;
  transition: transform var(--motion-press-in) var(--motion-ease-out),
              background var(--motion-state-swap) var(--motion-ease-out);
}

.btn:active {
  transform: translateY(var(--space-press-depth));
  background: var(--surface-pressed);
}

.btn.primary {
  background: var(--volt);
  color: var(--volt-ink);
}

.btn.primary:active { background: var(--volt); }

.btn:disabled {
  background: var(--surface-pod);
  color: var(--ink-faint);
  cursor: default;
  transform: none;
}

/*
 * The inline join, inside a row. Narrower and quieter than a full button, and
 * never the widest thing in the row: the row is a record first and an offer
 * second.
 *
 * It is quieter in width and type, never in height. It was 30 px tall until
 * the harness measured it, which reads as tidy and is a 30 px target for the
 * one control in this app where a mis-tap commits money. Narrow is a design
 * choice; short is a defect.
 */
.btn.inline {
  padding-inline: var(--space-md);
  font-size: var(--step-n2);
  /*
   * Quiet, not accented.
   *
   * The lobby was built with these in volt and the screenshot settled the
   * argument: seven saturated blocks down one screen is not an accent, it is a
   * background, and the eye stops being able to find anything. DIRECTION is
   * explicit that the boldness gets spent in one place. In a list of records
   * the record is the content and the join is the affordance, so the join
   * takes the raised surface and the accent stays available for the one screen
   * where there is a single thing to do.
   */
  background: var(--surface-raised);
  color: var(--ink);
}

.btn.inline:active { background: var(--surface-pressed); }

@media (hover: hover) {
  .btn.inline:hover { background: var(--surface-pressed); }
}

/* ------------------------------------------------------------ quick join */

/*
 * Three formats, one tap, at the top of the home screen.
 *
 * The default is the smallest buy in, not the largest. Betting interfaces
 * habitually pre-select above the minimum, which the BIT audit lists as a dark
 * pattern; defaulting to the floor costs nothing and is visible.
 */
.quick {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-sm);
}

.quick > .btn {
  flex-direction: column;
  gap: 2px;
  min-block-size: 62px;
  padding-inline: var(--space-sm);
  background: var(--surface-pod);
}

.quick > .btn:active { background: var(--surface-pressed); }

/*
 * The selected filter.
 *
 * A filled volt block was the first attempt and the screenshot rejected it:
 * the selected filter became the loudest object on the lobby, louder than the
 * seven joins under it, and a filter is state rather than action. It now says
 * the same thing the tab bar says, in the same vocabulary — raised surface,
 * full strength ink, a volt bar — so the accent still marks where you are
 * without claiming to be the thing to press.
 *
 * The selector is `aria-pressed` rather than a class, because that attribute
 * is what a screen reader needs anyway, and styling from it means the two can
 * never disagree.
 */
.quick > .btn[aria-pressed="true"] {
  background: var(--surface-raised);
  box-shadow: inset 0 -2px 0 var(--volt);
}

.quick > .btn[aria-pressed="true"] .n { color: var(--ink); }

.quick .n {
  font-size: var(--step-2);
  font-weight: var(--font-weight-display);
  font-stretch: var(--font-stretch-display);
  letter-spacing: var(--font-tracking-display);
  color: var(--ink);
  text-transform: none;
}

.quick .cap {
  font-size: 10px;
  color: var(--ink-faint);
}

/* ----------------------------------------------------------------- banner */

/*
 * The first object on the home screen, and the only art in the product.
 *
 * A sheet, so it takes the large radius. Everything dense in this app takes
 * the small one, and the two radii are the rule DIRECTION section C3 sets
 * against the sixteen-pixels-everywhere signature.
 *
 * The ground is a surface and not a gradient. A gradient as atmosphere is the
 * first item on that same tell list, and the material in this panel belongs to
 * one object, which is the fan of card backs drawn into the canvas.
 *
 * Its height comes from its own text, with a floor. An aspect ratio here would
 * decide the height from the width, and on the 360 px phone that is the floor
 * of this product it would cut the headline off rather than admit it did not
 * fit.
 */
.banner {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  min-block-size: 176px;
  max-inline-size: 100%;
  border-radius: var(--space-radius-large);
  background: var(--surface-arena);
}

/* Behind the text, and out of the accessibility tree. Everything a reader
   needs is in the DOM above it. */
.banner-art {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
}

/*
 * The text keeps the left and never crosses the art.
 *
 * `--banner-art-left` is published by `ui/banner.ts` from the same plan that
 * draws the fan, so the column is told where the cards begin instead of
 * guessing at a percentage that is right on one phone and wrong on the next.
 * A headline over an engraving is unreadable and no contrast gate can see it,
 * because the ink is measured against the panel and the panel is flat.
 */
.banner-body {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-xs);
  padding: var(--space-lg);
  /* Zero at the end, because the column already stops where the art starts.
     Sixteen pixels there would be sixteen fewer for the headline at the width
     where the headline is tightest. */
  padding-inline-end: 0;
  max-inline-size: calc(var(--banner-art-left, 60%) - var(--space-sm));
}

.banner-eyebrow {
  font-family: var(--font-display);
  font-size: var(--step-n2);
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  color: var(--volt);
}

/* The display step and not the largest one. An oversized hero over vague
   aspirational copy is the layout tell in DIRECTION section C3, and the line
   under it is the claim CANON section 1.1 requires rather than a slogan. */
.banner-title {
  margin: 0;
  font-size: var(--step-1);
  line-height: 1.15;
  color: var(--ink);
  text-wrap: balance;
}

.banner-line {
  margin: 0;
  font-size: var(--step-n2);
  line-height: 1.45;
  color: var(--ink-muted);
  text-wrap: pretty;
}

/*
 * The way in, and it is not volt.
 *
 * The one big control in the shell is the accent and is on screen at the same
 * time as this panel. A second saturated block above it is two shouts, so this
 * one takes the accent as a hairline and leaves the block to the control that
 * opens a table.
 */
.banner-cta {
  margin-block-start: var(--space-xs);
  box-shadow: inset 0 0 0 1px var(--volt-line);
  text-decoration: none;
}

/* ------------------------------------------------------------ the segment */

/*
 * Two cuts on one board: which table, and what ahead means.
 *
 * Text and not icons, the same decision the tab bar makes. Three seats, six
 * seats and twelve seats are the product's own vocabulary and an icon for any
 * of them would be a guess the player has to decode.
 *
 * The pressed state is a lighter surface and the accent on the label. A filled
 * volt pill here would put a second saturated block on the home screen, and
 * the accent is spent on the control that opens a table.
 */
/* The room the feed keeps for the board while the board is on its way. An
   empty flex item still costs a gap, and a gap under the last block reads as
   a list that ended early. */
.board-slot:empty { display: none; }

.seg-pair {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.seg {
  flex: 1 1 auto;
  display: flex;
  min-inline-size: 0;
  gap: 2px;
  padding: 2px;
  border-radius: var(--space-radius-small);
  background: var(--surface-pod);
}

.seg-btn {
  flex: 1;
  appearance: none;
  border: 0;
  /* The 44 px floor, on a control that looks like a filter and is one. The
     wrapper's own two pixels of padding put the group at 48. */
  min-block-size: var(--space-touch);
  padding-inline: var(--space-sm);
  border-radius: 8px;
  background: none;
  color: var(--ink-faint);
  font-family: var(--font-display);
  font-size: var(--step-n2);
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--motion-state-swap) var(--motion-ease-out);
}

.seg-btn:active { background: var(--surface-pressed); }

.seg-btn[aria-pressed="true"] {
  background: var(--surface-raised);
  color: var(--ink);
}

/* The position on the board, in the tile the rest of the app uses for a face.
   A rank is the one place a number belongs in that square. */
.avatar.num {
  font-variant-numeric: var(--font-numeric);
  color: var(--ink-muted);
}

/* -------------------------------------------------------------- live dot */

/*
 * The only looping animation in the shell.
 *
 * It earns its place because "is anything happening right now" is the question
 * the home screen exists to answer, and a static label claiming LIVE answers
 * it less convincingly than a thing that moves. Everything else on this screen
 * is still.
 */
.live {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--step-n2);
  color: var(--ink-faint);
}

.live::before {
  content: "";
  inline-size: 6px;
  block-size: 6px;
  border-radius: 50%;
  background: var(--win);
  animation: pulse 2s var(--motion-ease-in-out) infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}

/* ----------------------------------------------------------- sample note */

/*
 * Says the feed is not real, on the feed.
 *
 * The caution colour and a rule down the leading edge, the same device the
 * consent gate's preview notice uses, so the two read as the same kind of
 * statement. Not volt: volt means there is something to press.
 */
.sample-note {
  margin: 0;
  padding: var(--space-md);
  border-inline-start: 2px solid var(--risk);
  border-radius: 0 var(--space-radius-small) var(--space-radius-small) 0;
  background: var(--surface-pod);
  font-size: var(--step-n1);
  line-height: 1.45;
  color: var(--ink);
  /* The one paragraph in the product that must never end on a single word.
     It is the sentence that says the figures above it are invented, and a
     stray "yet." on its own line reads as a caption that lost its picture. */
  text-wrap: pretty;
}

/* ------------------------------------------------------------------ empty */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  padding-block: var(--space-xxl);
  color: var(--ink-faint);
  font-size: var(--step-n1);
  text-align: center;
}

/* --------------------------------------------------------------- a11y */

/* Focus has to be visible and it has to be visible on a near black ground,
   which a default outline is not. */
:where(.row, .tab, .btn, .play-cta):focus-visible {
  outline: 2px solid var(--volt);
  outline-offset: -2px;
}

/*
 * Reduced motion removes movement and keeps colour and opacity, which is the
 * distinction the accessibility guidance actually draws: the setting is about
 * vestibular response to motion, not about feedback disappearing.
 *
 * Two selectors, and both are needed.
 *
 * The attribute is the authoritative one. `settings.ts` writes
 * `data-motion` on the root, and it starts from the operating system's own
 * preference, so a person who has already told their OS never has to tell this
 * product as well — but it can also be turned on here by someone whose OS
 * setting is off, which the media query alone can never honour.
 *
 * The media query covers the frames before the script runs. Without it, a page
 * on a slow phone animates its way in and only then goes still.
 *
 * An audit found the attribute half of this missing entirely: the toggle in the
 * profile wrote `data-motion="reduced"` onto the root and no rule in the
 * repository selected it, so the control moved and nothing happened. That is
 * worse than not offering it. The two blocks below must stay in step.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
  }

  .btn:active { transform: none; }
}

[data-motion="reduced"] *,
[data-motion="reduced"] *::before,
[data-motion="reduced"] *::after {
  animation-duration: 1ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 1ms !important;
}

[data-motion="reduced"] .btn:active { transform: none; }

/* The two looping animations are the ones this setting is really about: they
   are the only things on screen that move without anybody touching them. */
[data-motion="reduced"] .live::before,
[data-motion="reduced"] .dealing-logo {
  animation: none;
  opacity: 1;
}

/* Hover only where there is a real pointer. On touch, `:hover` sticks after a
   tap and leaves a row looking selected for the rest of the session. */
@media (hover: hover) {
  .row.tappable:hover { background: var(--surface-raised); }
  .tab:hover { color: var(--ink-muted); }
  .tab[aria-current="page"]:hover { color: var(--ink); }
}

/* ------------------------------------------------------------------- gate */

/*
 * The consent gate: a playing card, dealt onto the screen.
 *
 * The card is light. Everything else in this product is dark by decision, and
 * this is not an exception to that rule — it is the rule applied. A card in
 * this product is a light object with dark ink on it, and the gate is a card.
 * A dark rectangle here would be a modal wearing a card's proportion.
 *
 * The controls sit under the card rather than on it. At 360 px a 5:7 card is
 * 320 by 448, and two tick boxes, a hint and two buttons take about 250 of that
 * — which would leave seven lines of legal text visible at a time. Below the
 * card they cost nothing and land where a thumb already is.
 */
/*
 * The gate scrolls rather than clipping.
 *
 * `overflow: hidden` on a fixed full screen grid means that whatever does not
 * fit is simply gone, and on a phone held sideways what did not fit was the
 * tick boxes and the button: 390 px of height against a card of terms, so
 * nobody could agree and nobody could decline. Measured at 844x390 and
 * 800x360, and it had been true for as long as the gate has existed.
 *
 * A screen that cannot be agreed to and cannot be refused is the one screen in
 * this product where being unreachable is a legal problem and not only a
 * layout one.
 *
 * `place-items: center` becomes `justify-items: center` so the card sits at the
 * top of a viewport too short to centre it in, rather than being centred and
 * clipped at both ends.
 */
.gate {
  position: fixed;
  inset: 0;
  z-index: var(--layer-modal);
  display: grid;
  justify-items: center;
  align-content: safe center;
  padding: var(--space-lg);
  padding-block-end: calc(var(--space-lg) + var(--tg-safe-bottom, var(--space-safe-bottom)));
  background: var(--ground);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.gate-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
  inline-size: 100%;
  max-inline-size: 420px;
}

/*
 * The card.
 *
 * Height first and the width derived from it, which is the only way to hold a
 * proportion while fitting a viewport: `block-size` takes the smaller of the
 * height available and the height the width allows, and `aspect-ratio` follows.
 * Setting the width instead and clamping the height silently breaks the ratio,
 * which on a card is the whole point of the object.
 *
 * 62dvh, checked at all three widths. 360 by 800 is the tightest case: the
 * width allows 448 and 62dvh allows 496, so the width wins and the card is 320
 * by 448 with about 200 of controls under it. On taller screens the height cap
 * binds instead and the card grows with the viewport.
 */
.gate-card {
  block-size: min(62dvh, calc((100vw - 40px) * 1.4));
  aspect-ratio: 5 / 7;
  display: flex;
  flex-direction: column;
  padding: var(--space-lg);
  border-radius: 18px;
  background: var(--card-face);
  color: var(--suit-spades);
  box-shadow:
    inset 0 0 0 1px var(--card-edge),
    0 24px 60px rgba(0, 0, 0, .55);

  /* The deal. Starts off the bottom, turned, and settles. */
  transform: translateY(42px) rotate(-7deg) scale(.94);
  opacity: 0;
  transition:
    transform 620ms var(--motion-ease-out),
    opacity 320ms var(--motion-ease-out);
}

.gate-card.dealt {
  transform: none;
  opacity: 1;
}

.gate-card-head {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-block-end: var(--space-md);
}

.gate-logo {
  inline-size: 26px;
  block-size: 26px;
  flex: none;
  border-radius: 8px;
}

.gate-mark {
  font-family: var(--font-display);
  font-weight: var(--font-weight-display);
  font-stretch: var(--font-stretch-wordmark);
  letter-spacing: var(--font-tracking-display);
  font-size: var(--step-0);
  color: var(--suit-spades);
}

.gate-version {
  margin-inline-start: auto;
  font-family: var(--font-mono);
  font-size: 10px;
  /* Measured against the card face rather than guessed: this is the faintest
     thing on the card and it still clears 4.5:1. */
  color: #5C6470;
}

.gate-title {
  margin: 0 0 var(--space-sm);
  font-family: var(--font-display);
  font-weight: var(--font-weight-display);
  font-stretch: var(--font-stretch-display);
  letter-spacing: var(--font-tracking-display);
  font-size: var(--step-2);
  line-height: 1.05;
  color: var(--suit-spades);
  /* Headings balance so a two word title never leaves one word alone. */
  text-wrap: balance;
}

/*
 * The terms. The only scrolling region on the screen.
 *
 * `text-wrap: pretty` is what keeps the last line of a paragraph from being a
 * single word. It is a one line fix for the thing that makes a block of text
 * look unconsidered, and where it is unsupported the text is merely normal.
 */
.gate-terms {
  flex: 1;
  min-block-size: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* Room under the last line so the end of the text is never flush with the
     card's edge, which is what makes a scroll region look cut off. */
  padding-block-end: var(--space-md);
  /* A soft edge at the bottom while there is more to read. It is removed the
     moment the end is reached, which is also when the read box unlocks, so the
     two say the same thing. */
  mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent);

  /*
   * The scrollbar, in the card's own ink.
   *
   * A desktop browser draws its default bar here, and the default is a grey
   * system chrome sitting inside a printed card, flush to the card's edge,
   * visible for the whole session. It was the first thing the owner named on
   * desktop and he was right twice over: it is not ours, and it is not placed.
   *
   * So it is thin, it is the card's own ink rather than the app's, and the
   * track is transparent so the bar floats in the margin that already exists
   * instead of drawing a second edge next to the card's. It is not hidden,
   * because this is the one region on the screen the reader has to reach the
   * end of, and a scroll region with no bar is a scroll region nobody knows
   * is scrollable.
   */
  scrollbar-width: thin;
  scrollbar-color: rgba(20, 22, 27, .28) transparent;
}

.gate-terms::-webkit-scrollbar {
  inline-size: 6px;
}

.gate-terms::-webkit-scrollbar-track {
  background: transparent;
}

.gate-terms::-webkit-scrollbar-thumb {
  background: rgba(20, 22, 27, .28);
  border-radius: 999px;
}

.gate-terms::-webkit-scrollbar-thumb:hover {
  background: rgba(20, 22, 27, .42);
}

.gate-card.read .gate-terms {
  mask-image: none;
}

.term {
  margin-block-end: var(--space-lg);
}

.term:last-child {
  margin-block-end: 0;
}

.term-title {
  margin: 0 0 3px;
  font-family: var(--font-display);
  font-size: var(--step-n1);
  font-weight: var(--font-weight-label);
  color: var(--suit-spades);
  text-wrap: balance;
}

.term-body {
  margin: 0;
  font-size: var(--step-n1);
  line-height: 1.5;
  color: #2C333D;
  text-wrap: pretty;
}

.term-body.faint {
  margin-block-start: var(--space-sm);
  color: #5C6470;
}

.term-list {
  margin: 0;
  padding-inline-start: var(--space-lg);
  font-size: var(--step-n1);
  line-height: 1.55;
  color: #2C333D;
}

/*
 * The preview notice, while the build is not playing for real.
 *
 * A rule down the leading edge in the caution colour, the same device the feed
 * uses for its sample-data note, so the two read as the same kind of statement.
 */
.gate-preview {
  margin: 0 0 var(--space-lg);
  padding: var(--space-sm) var(--space-md);
  border-inline-start: 2px solid var(--risk);
  border-radius: 0 8px 8px 0;
  background: rgba(20, 22, 27, .06);
  font-size: var(--step-n2);
  line-height: 1.5;
  color: #2C333D;
  text-wrap: pretty;
}

/* --------------------------------------------------------- gate controls */

.gate-controls {
  inline-size: 100%;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/*
 * One affirmation. The whole row is the target, which is what `<label for>`
 * buys, so the 22 px tick box does not make the tap 22 px.
 */
.gate-check {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  min-block-size: var(--space-touch);
  padding-block: 5px;
  cursor: pointer;
}

/* The real input, kept in the accessibility tree and out of sight. Not
   `display: none`, which would take it out of the tab order and off the screen
   reader, and then the tick would be a picture of a checkbox. */
.gate-box {
  position: absolute;
  opacity: 0;
  inline-size: 1px;
  block-size: 1px;
  margin: 0;
  pointer-events: none;
}

.gate-tick {
  flex: none;
  inline-size: 22px;
  block-size: 22px;
  margin-block-start: 1px;
  border-radius: 6px;
  background: var(--surface-pressed);
  box-shadow: inset 0 0 0 1px var(--line-strong);
  position: relative;
  transition: background var(--motion-state-swap) var(--motion-ease-out);
}

.gate-tick::after {
  content: "";
  position: absolute;
  inset-block-start: 4px;
  inset-inline-start: 7px;
  inline-size: 6px;
  block-size: 11px;
  border: 2px solid var(--volt-ink);
  border-block-start: 0;
  border-inline-start: 0;
  transform: rotate(42deg) scale(0);
  transition: transform var(--motion-state-swap) var(--motion-ease-out);
}

.gate-box:checked + .gate-tick {
  background: var(--volt);
  box-shadow: none;
}

.gate-box:checked + .gate-tick::after {
  transform: rotate(42deg) scale(1);
}

.gate-box:focus-visible + .gate-tick {
  outline: 2px solid var(--volt);
  outline-offset: 2px;
}

.gate-text {
  font-size: var(--step-n2);
  line-height: 1.4;
  color: var(--ink);
  text-wrap: pretty;
}

/* Locked until the terms have been read to the end. Dimmed rather than hidden,
   so the person can see what is waiting and why. */
.gate-check.locked { cursor: default; }
.gate-check.locked .gate-text { color: var(--ink-faint); }
.gate-check.locked .gate-tick { opacity: .45; }

.gate-hint {
  margin: 2px 0 4px;
  min-block-size: 1.15em;
  font-size: var(--step-n2);
  color: var(--ink-faint);
}

.gate-enter,
.gate-leave {
  inline-size: 100%;
}

/*
 * Declining is a full width button in the same stack as accepting, one tap, no
 * confirmation step. Quieter in colour because it is not the default, identical
 * in reach because making it harder to leave than to enter is the pattern this
 * product is positioned against.
 */
.gate-leave {
  background: none;
  color: var(--ink-muted);
}

.gate-leave:active { background: var(--surface-pod); }

.gate-card.declined {
  justify-content: center;
}

.gate-declined-body {
  margin: 0;
  font-size: var(--step-n1);
  line-height: 1.5;
  color: #2C333D;
  text-wrap: pretty;
}

/* --------------------------------------------------------------- the ten */

/*
 * The only rank that is two characters.
 *
 * ui.css sets the index at 38cqw, which is right for one glyph and roughly
 * double what it should be for two: a 10 at that size runs into the pip below
 * it and off the card at the opponent size. Condensed and tracked in, on that
 * rank alone.
 *
 * Bricolage carries a real width axis, so this narrows the letterforms rather
 * than squashing them — the difference between a condensed 10 and a 10 that
 * looks like a rendering fault.
 */
.card[data-r="10"] > .rank {
  font-size: 30cqw;
  font-stretch: 68%;
  letter-spacing: -.06em;
}

@container card (max-width: 59px) {
  .card[data-r="10"] > .rank { font-size: 33cqw; }
}

@container card (max-width: 40px) {
  .card[data-r="10"] > .rank { font-size: 36cqw; letter-spacing: -.09em; }
}

/* ------------------------------------------------------- result colouring */

/*
 * `.mult` is the result figure wherever a result appears: a feed multiplier, a
 * hand's net, an all-time net. The three modifiers are the only way a number
 * in this product gets a colour, and they come from `classify` in store.ts,
 * which computes from two amounts and accepts no verdict.
 *
 * `push` is grey. A hand that returned exactly what it took is not a win, and
 * this is the line where that would otherwise be quietly crossed.
 */
.mult.win { color: var(--win); }
.mult.loss { color: var(--danger); }
.mult.push { color: var(--ink-muted); }

/* ---------------------------------------------------------------- profile */

.profile {
  display: flex;
  /* Top and not centre. The body carries a name, a date and up to five badges
     now, and a 56 px tile centred against three lines sits opposite the gap
     between them rather than opposite the name it belongs to. */
  align-items: flex-start;
  gap: var(--space-md);
}

.avatar.big {
  inline-size: 56px;
  block-size: 56px;
  border-radius: var(--space-radius-small);
  font-size: var(--step-1);
  color: var(--ink);
}

.profile-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-inline-size: 0;
}

.profile-name {
  font-size: var(--step-2);
  color: var(--ink);
}

.profile-body > .sub {
  font-size: var(--step-n2);
  color: var(--ink-faint);
}

/* ----------------------------------------------------------------- badges */

/*
 * Up to five, under the name, and this is the only place they appear.
 *
 * All five look the same. No tier colour, no shine, no count of how many
 * players have one: a badge here is a sentence about how somebody plays, and
 * the moment one of them outranks the others the strip becomes a score, which
 * is a different product and a worse one.
 *
 * They wrap rather than scroll. Five short names fit two lines on the smallest
 * phone, and a strip that scrolls sideways hides the fifth from the person
 * whose profile it is.
 */
.badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-block-start: 6px;
}

.badge {
  padding-inline: var(--space-sm);
  padding-block: 3px;
  border-radius: 8px;
  background: var(--surface-raised);
  color: var(--ink-muted);
  font-family: var(--font-display);
  font-size: var(--step-n2);
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  white-space: nowrap;
}

/* An empty strip says what would be in it, rather than collapsing and leaving
   a gap the person cannot name. */
.badges-none {
  font-size: var(--step-n2);
  color: var(--ink-faint);
}

/* ------------------------------------------------------------------ stats */

/*
 * Three across, because six figures in two rows of three is the shape that
 * fits a 360 px screen without either wrapping a label or shrinking a number
 * below the point where it can be read at arm's length.
 *
 * The 1px gap over a line coloured background is what draws the separators.
 * It is one declaration instead of a border on every cell, and it cannot
 * produce the doubled edge that per-cell borders produce at the seams.
 */
.stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  border-radius: var(--space-radius-small);
  overflow: hidden;
  background: var(--line);
}

/*
 * Two across, for a group of four.
 *
 * Four figures in a three column grid leaves one cell empty and one figure
 * alone on a row, which is the "column count the items do not fill" failure:
 * the empty cell reads as something that failed to load. The column count
 * follows the item count instead of being fixed.
 */
.stats.pairs {
  grid-template-columns: repeat(2, 1fr);
}

.stats > .cell {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-md);
  background: var(--surface-pod);
}

.cell > .cv {
  font-family: var(--font-display);
  font-weight: var(--font-weight-display);
  font-stretch: var(--font-stretch-display);
  letter-spacing: var(--font-tracking-display);
  font-variant-numeric: tabular-nums;
  font-size: var(--step-1);
  color: var(--ink);
}

/*
 * The net result, in the profile's session block.
 *
 * The same three way rule the header carried, moved with the figure. `push` is
 * deliberately not green: a hand that returns exactly what it took is not a
 * win, and colouring it as one is Losses Disguised as Wins in its cheapest
 * form.
 */
.cell[data-kind="win"] > .cv { color: var(--win); }
.cell[data-kind="loss"] > .cv { color: var(--danger); }
.cell[data-kind="push"] > .cv { color: var(--ink-muted); }

.cell > .ck {
  font-size: 10px;
  letter-spacing: var(--font-tracking-label);
  text-transform: uppercase;
  color: var(--ink-faint);
  font-family: var(--font-display);
  font-weight: var(--font-weight-label);
}

/* ------------------------------------------------------------- row extras */

.row .sub.mono {
  font-family: var(--font-mono);
  font-size: 11px;
}

.row > .chev {
  flex: none;
  color: var(--ink-faint);
  font-size: var(--step-1);
  line-height: 1;
}

/* A switch, drawn rather than an <input type=checkbox> with an appearance
   reset, because the reset differs per engine and the drawn one does not. */
.row > .switch {
  flex: none;
  inline-size: 40px;
  block-size: 24px;
  border-radius: 12px;
  background: var(--surface-pressed);
  position: relative;
  transition: background var(--motion-state-swap) var(--motion-ease-out);
}

.row > .switch::after {
  content: "";
  position: absolute;
  inset-block-start: 3px;
  inset-inline-start: 3px;
  inline-size: 18px;
  block-size: 18px;
  border-radius: 50%;
  background: var(--ink-faint);
  transition: transform var(--motion-state-swap) var(--motion-ease-out),
              background var(--motion-state-swap) var(--motion-ease-out);
}

.row[aria-pressed="true"] > .switch { background: var(--volt-dim); }

.row[aria-pressed="true"] > .switch::after {
  transform: translateX(16px);
  background: var(--volt);
}

/* ------------------------------------------------------------------- chat */

/*
 * A sheet inside the hand, opened by a control, closed by the same one.
 *
 * It was a tab with the whole screen to itself and no table behind it. ADR
 * 0012 moved it into the hand, which is where the reference study says it
 * belongs: not one of the twenty two apps measured gives standing space to
 * anything but the table while cards are out, and the fifteen per cent of dead
 * arena that study found is being closed, not filled with a panel.
 *
 * The large radius is correct here and only here. DIRECTION section C3 asks
 * for two radii, small on dense elements and large on sheets, and this is the
 * one sheet in the table.
 *
 * The log scrolls, the composer does not. Anything else and the input rides
 * up out of reach the moment the software keyboard opens.
 */
.hand-chat {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 3;
  max-block-size: 58%;
  border-start-start-radius: var(--space-radius-large);
  border-start-end-radius: var(--space-radius-large);
  background: var(--surface-arena);
  /* The arena surface is one step off the table's ground, which is enough
     separation in a list and not enough for a sheet lying on top of a table.
     The shadow is what makes it an object above rather than a region of. */
  box-shadow: 0 -20px 44px rgba(0, 0, 0, .55);
  display: grid;
  /*
   * `minmax(0, 1fr)` and not `1fr`.
   *
   * With no column named, the implicit column is `auto`, which is sized to its
   * contents and is free to grow past the container. The screenshot harness
   * measured the log and the composer at 364.2 px inside a 360 px shell: four
   * pixels of horizontal overflow, invisible in a screenshot because the
   * scrollbars are hidden, and a page that slides sideways under the thumb on
   * a real phone. The `0` floor is what lets the track shrink to the shell.
   */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
  min-block-size: 0;
}

.hand-chat[hidden] { display: none; }

/*
 * The control that opens it.
 *
 * Top right of the stage, clear of the action bar in the bottom sixth. The
 * reference study is explicit that the commit action owns the thumb arc, and a
 * chat button next to it is the one mis-tap that costs money.
 */
/*
 * The chat, in the corner the connect button is leaving.
 *
 * Two things were in the top right of a live hand: the app's connect button,
 * which asks the player to open an account while they are holding cards, and
 * this, which floated under it and overlapped it on a phone. The owner named
 * both: the chat collides with the table, and a connect button on a table
 * makes no sense.
 *
 * So the whole app header goes for the length of a hand, the way the tab bar
 * already does, and the chat takes the corner. The table's own status band is
 * the top row now and it carries the two numbers a hand is about, so nothing
 * is lost by the header leaving and a hand gains the height.
 */
/*
 * The result, and the ten seconds it owns. ADR 0018.
 *
 * Seven to read, three to announce the next deal. The line fills across the
 * button that takes it, and it is a transform on a child rather than a
 * background position, so the browser can run it off the main thread and it
 * costs nothing while the table is still settling behind it.
 *
 * It says when and never how much, which is the rule that separates a rhythm
 * from the re-engagement prompt DIRECTION section A4 names.
 */
.next-hand { position: relative; overflow: hidden; }
.next-line {
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  block-size: 3px;
  inline-size: 100%;
  background: var(--volt-ink);
  opacity: .55;
  transform: scaleX(var(--fill, 0));
  transform-origin: left center;
  transition: transform 3s linear;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .next-line { transition: none; }
}

/*
 * The player card. A small square over the felt, and nothing more.
 *
 * The owner's own limits: opens and closes on one tap, does not take much of
 * the screen, and never takes you off the table. So it is sized to its content
 * with a hard ceiling, it is absolutely placed against the stage by script
 * because only the page knows where a seat ended up, and the hand keeps
 * running behind it.
 *
 * The same one light every other surface here uses, and the large radius,
 * because it is a sheet rather than a dense control.
 */
.seat-card {
  position: absolute;
  z-index: var(--layer-modal);
  inline-size: min(232px, calc(100vw - 32px));
  display: grid;
  gap: var(--space-sm);
  padding: var(--space-sm);
  border-radius: var(--space-radius-large);
  background: var(--surface-raised);
  box-shadow:
    inset 0 1px 0 color-mix(in oklab, white 8%, transparent),
    0 16px 40px -12px color-mix(in oklab, black 70%, transparent);
  animation: seat-card-in var(--motion-state-swap) var(--motion-ease-out) both;
}
.seat-card[hidden] { display: none; }
@keyframes seat-card-in {
  from { opacity: 0; scale: .96; }
  to { opacity: 1; scale: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .seat-card { animation: none; }
}

.sc-head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--space-sm);
}
.sc-head > .avatar {
  inline-size: 40px;
  block-size: 40px;
  border-radius: var(--space-radius-small);
  display: grid;
  place-items: center;
  background: var(--surface-pod);
  font-family: var(--font-display);
  font-size: var(--step-n1);
  color: var(--ink-muted);
}
.sc-who { display: grid; gap: 2px; min-inline-size: 0; }
.sc-name {
  font-family: var(--font-display);
  font-weight: var(--font-weight-display);
  font-size: var(--step-0);
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sc-since {
  font-family: var(--font-sans);
  font-size: var(--step-n2);
  color: var(--ink-muted);
}

/* The badges, and no more of them than fit on one line each. A wall of every
   badge earned says nothing, which is the same reason a profile shows five. */
.sc-badges { display: flex; flex-wrap: wrap; gap: 4px; }
.sc-badge {
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--surface-pod);
  box-shadow: inset 0 0 0 1px var(--line);
  font-family: var(--font-sans);
  font-size: var(--step-n2);
  color: var(--ink-muted);
  max-inline-size: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sc-badges:empty { display: none; }

.sc-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 4px;
}
.sc-stats:empty { display: none; }
.sc-stat {
  display: grid;
  justify-items: center;
  gap: 1px;
  padding: 6px 2px;
  border-radius: var(--space-radius-small);
  background: var(--surface-pod);
}
.sc-v {
  font-family: var(--font-display);
  font-variant-numeric: var(--font-numeric);
  font-weight: var(--font-weight-display);
  font-size: var(--step-n1);
  color: var(--ink);
}
.sc-k {
  font-family: var(--font-sans);
  font-size: var(--step-n2);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-faint);
}

.chat-toggle {
  position: absolute;
  inset-block-start: var(--space-sm);
  inset-inline-end: var(--space-gutter);
  z-index: var(--layer-hud);
}

/* The same floor one level down. A grid item's automatic minimum size is its
   min-content size, so an item that cannot shrink pushes the track back out. */
.chat-log,
.chat-composer {
  min-inline-size: 0;
}

.chat-log {
  overflow-y: auto;
  overscroll-behavior-y: contain;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding-inline: var(--space-gutter);
  padding-block: var(--space-md);
}

.chat-line {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
}

.chat-line.own { justify-content: flex-end; }

.chat-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-inline-size: 74%;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--space-radius-small);
  background: var(--surface-pod);
}

/* The player's own line takes the accent surface, not the accent itself.
   Volt on a message body would put the action colour on something that is not
   an action, and after twenty lines the screen would be mostly action. */
.chat-line.own .chat-body {
  background: var(--surface-raised);
}

.chat-who {
  font-size: var(--step-n2);
  color: var(--ink-faint);
}

.chat-text {
  font-size: var(--step-n1);
  color: var(--ink);
  overflow-wrap: anywhere;
}

.chat-line > .when {
  flex: none;
  align-self: flex-end;
  font-size: 10px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

.chat-composer {
  display: flex;
  gap: var(--space-sm);
  padding-inline: var(--space-gutter);
  padding-block: var(--space-sm);
  background: var(--ground);
  box-shadow: inset 0 1px 0 var(--line);
}

.chat-input {
  flex: 1;
  min-inline-size: 0;
  min-block-size: var(--space-touch);
  padding-inline: var(--space-md);
  border: 0;
  border-radius: var(--space-radius-small);
  background: var(--surface-pod);
  color: var(--ink);
  font-family: var(--font-sans);
  /* 16px or larger, or iOS Safari zooms the whole page on focus and the
     layout the rest of this file describes stops being the one on screen. */
  font-size: 16px;
}

.chat-input::placeholder { color: var(--ink-faint); }

.chat-input:focus-visible {
  outline: 2px solid var(--volt);
  outline-offset: -2px;
}

/* ------------------------------------------------------------------ stage */

/*
 * The table fills the outlet instead of scrolling inside it. Same pair as the
 * chat tab above, and the reason is the same shape of problem.
 *
 * `.table` in ui.css asks for `block-size: 100%`. A percentage height against
 * a parent whose own height is automatic resolves to `auto`, so the table was
 * as tall as its contents, the arena's `1fr` row received nothing left over,
 * and because every `.slot` in the arena is absolutely positioned there was no
 * intrinsic height underneath to fall back on. Measured in Chrome on the
 * deployed site the arena came out 16 px tall at 390x844 and again at
 * 1440x900: the whole stage, every opponent and the clock, compressed into a
 * strip. The stage never had a rule of its own anywhere in the repository.
 *
 * Giving it a definite box is the whole fix. Nothing else moves.
 */
.outlet:has(> .stage) {
  overflow: hidden;
  padding-block-end: 0;
}

/* Positioned, because the chat sheet and its control are anchored to it. A
   static stage would anchor them to the viewport and they would sit over the
   tab bar on every screen. */
.stage {
  position: relative;
  block-size: 100%;
  /* The table clips its own overflow. Repeating it here keeps a mid deal
     card from lengthening the box the table was measured against. */
  overflow: hidden;
}

/* ---------------------------------------------------------------- dealing */

/*
 * What fills the gap while the game module arrives.
 *
 * Not a spinner. The mark, and the two facts the player just chose: how many
 * seats and what it costs. DIRECTION is explicit that a loading state should
 * be content, and these two lines are the only content that is true yet.
 */
.dealing {
  display: grid;
  place-items: center;
  padding: var(--space-xl);
}

.dealing-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-md);
}

/* The size the img element already declares. Written here too so a stylesheet
   that loads before the image does not reflow the block when it lands. */
.dealing-logo {
  inline-size: 96px;
  block-size: 96px;
}

.dealing-line {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--step-n2);
  font-weight: var(--font-weight-label);
  letter-spacing: var(--font-tracking-label);
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ----------------------------------------------------------------- result */

/*
 * The hand result, as a real object.
 *
 * It shipped with no CSS at all: a block of text in the document flow under a
 * table, which is how a moment the whole hand was played for reads as a stray
 * paragraph. It is a modal, so it looks like one. Over everything, centred,
 * on a scrim, with the table still visible behind it because what the player
 * wants to check first is the board that produced the number.
 *
 * Fixed and not absolute. The tab bar has to be covered: while this is up
 * there are exactly two answers, and a third route out of the screen would
 * leave a hand settled and a table nobody closed.
 */
.result {
  position: fixed;
  inset: 0;
  z-index: var(--layer-modal);
  display: grid;
  place-items: center;
  padding: var(--space-lg);
  padding-block-end: calc(var(--space-lg) + var(--tg-safe-bottom, var(--space-safe-bottom)));
  background: var(--scrim);
  -webkit-backdrop-filter: var(--glass-backdrop);
  backdrop-filter: var(--glass-backdrop);
}

/* The box takes focus when the hand settles so a screen reader lands inside
   the dialog, and it is not itself an action, so it does not draw a ring.
   Both selectors, because which one a programmatic focus matches is an engine
   decision and this must not depend on that. */
.result:focus,
.result:focus-visible {
  outline: none;
}

/* The one glass surface per viewport rule, honoured the same way the choice
   bar honours it: opaque, and not merely less transparent. */
@media (prefers-reduced-transparency: reduce) {
  .result {
    background: var(--glass-fallback);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/*
 * The sheet. Large radius because it is a sheet, which is the only thing the
 * large radius is for, and a luminance step over the ground rather than a
 * border, which is the rule everywhere else in this product.
 */
.result-card {
  inline-size: min(100%, 360px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-xl) var(--space-lg);
  border-radius: var(--space-radius-large);
  background: var(--surface-pod);
  box-shadow: var(--glass-shadow);
  text-align: center;
}

/* `verdict` in words, over the figure. It carries `.label` from ui.css for
   the face and the tracking, so all this owes is the leading. */
.result-kind {
  line-height: 1;
}

/*
 * The figure, at the step the scale reserved for exactly this.
 *
 * `--step-4` is documented in tokens.ts as "a result" and had never been used
 * by anything. The colour comes from `.mult`, which takes win, loss or push
 * from `classify`, and `classify` reads two amounts and never a verdict.
 */
.result-hero {
  font-size: var(--step-4);
  line-height: .95;
}

.result-line {
  font-size: var(--step-n1);
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
  text-wrap: balance;
}

/* Two answers, equal weight in the layout and not in the surface: the accent
   is on the one that continues and the other is quiet. */
.result-acts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
  inline-size: 100%;
  margin-block-start: var(--space-sm);
}

/* ------------------------------------------------------------------ share */

/*
 * The share card, and only on a win. It is absent from most hands, so the
 * slot collapses rather than reserving a hole in the sheet.
 */
.share-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  inline-size: 100%;
}

.share-slot:empty {
  display: none;
}

/*
 * The canvas is drawn at 1200 by 675, because that is what a timeline wants.
 * With no CSS it laid out at its attribute width and pushed 810 px of sheet
 * out of a 390 px phone. The intrinsic ratio holds the height, so one line
 * does it and there is no second number to keep in step with the canvas.
 */
.share-card {
  inline-size: 100%;
  block-size: auto;
  border-radius: var(--space-radius-small);
}

/*
 * Tune mode's panel.
 *
 * It is a tool and not a screen, so it is deliberately plain: it must never be
 * mistaken for part of the product in a screenshot, and every pixel it spends
 * looking designed is a pixel of the table it is covering. It sits bottom
 * right, folds to a single button, and scrolls on its own so a phone can hold
 * it next to a full table.
 *
 * The rules live here rather than in `ui.css` because `ui.css` is the table's
 * own stylesheet and the table does not know this exists.
 */
.tune {
  position: fixed;
  right: 8px;
  bottom: 8px;
  z-index: 999;
  width: min(270px, calc(100vw - 16px));
  max-height: min(70vh, 520px);
  overflow: auto;
  padding: 8px;
  border-radius: 10px;
  border: 1px solid var(--line-strong);
  background: var(--surface-raised);
  font-size: 11px;
  line-height: 1.5;
  box-shadow: 0 8px 30px rgba(0, 0, 0, .55);
}
.tune[data-open="no"] > .tune-body { display: none; }
.tune[data-open="no"] { width: auto; }
.tune-row {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 2px 0;
}
.tune-head { font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }
.tune-label { flex: 1 1 auto; min-width: 64px; }
.tune-num {
  flex: 0 0 auto;
  min-width: 34px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  opacity: .7;
}
.tune-btn {
  flex: 0 0 auto;
  padding: 2px 6px;
  border-radius: 6px;
  border: 1px solid var(--line);
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.tune-btn:active { background: var(--line); }
.tune input[type="range"] { flex: 1 1 90px; min-width: 0; }
.tune-out {
  width: 100%;
  margin-top: 6px;
  font-family: var(--font-mono, monospace);
  font-size: 10px;
}
