:root {
  --bg: #05070c;
  --panel: rgba(5, 7, 12, 0.84);
  --panel-strong: rgba(9, 14, 23, 0.92);
  --panel-border: rgba(255, 255, 255, 0.14);
  --text: #eef3ff;
  --text-dim: #94a0b8;
  --accent: #3bf5c8;
  --blue: #4da6ff;
  /* One gold, not two. This was #ffd166 while the brand's is #ffc83d, so the season pass and the
     year-in-review card were paying out in a different metal from the shop, the wallet and the
     mode cards — close enough that nobody spotted it, far enough that the two clients' coins were
     measurably different colours. Gold means money or reward and there is only one of it. */
  --gold: var(--brand-gold);
  --panel-soft: rgba(255, 255, 255, 0.055);
  --control-surface: rgba(10, 15, 24, 0.74);
  --control-hover: rgba(255, 255, 255, 0.07);
  --accent-ink: #03100d;
  --secondary-ink: #03101b;
  --danger: #ff4d6d;
  --radius-card: 28px;
  --radius-button: 22px;
  --radius-small: 12px;
  --radius-pill: 999px;

  /* Buttons and rows — the dominant consumer of this token by a wide margin. The handful of
     genuinely SMALL things that were relying on 8px are pinned to --radius-small individually
     below, rather than holding the whole scale down to keep them company. */
  --radius: var(--radius-button);

  /* ---- the brand palette --------------------------------------------------------------------
     A theme dresses the ARENA; the brand dresses everything else (brand guide §1). These are the
     app's tokens.css values verbatim, and they are fixed: a player who picks Blush asked for a
     pink arena, not a pink account drawer.

     They were being USED before they were ever DEFINED. `var(--brand-purple, #d83cff)` appeared in
     five rules and `var(--brand-gold, #ffc83d)` in six, each carrying the real value in its own
     fallback, so the page looked right while the token layer the guide describes did not exist
     here at all. That is why the two clients could drift without anything failing: there was no
     one place to compare. `ClientBrandTokenParityTests` now reads both files and holds them equal. */
  --brand-purple:  #d83cff;
  --brand-magenta: #ea42f5;
  --brand-violet:  #7a28ff;
  --brand-green:   #63ff48;
  --brand-gold:    #ffc83d;
  --brand-ink:     #050713;
  --brand-ink-2:   #09051a;

  /* The accent MENUS use, as opposed to the one the arena does. Same split, and the same two
     values, as the app's ThemeStyle: #d83cff measures 5.52:1 on the lobby's dark ground but only
     3.49:1 on a pastel sheet's white, so the light branch moves down the brand's OWN gradient to
     the violet end rather than inventing a second brand. Eight of the fifteen themes are pastel. */
  --ui-accent:     #d83cff;
  --ui-accent-ink: #ffffff;
  --ui-accent-hover: color-mix(in srgb, var(--ui-accent) 82%, white);

  /* Brand text, for surfaces that stay on brand ink whatever the arena is doing. Distinct from
     --text/--text-dim, which follow the theme and go DARK on the eight pastel themes. */
  --brand-text:           #ffffff;
  --brand-text-secondary: #aaa5c5;
  --brand-text-muted:     #77738f;

  /* The brand gradient, wherever a surface needs one (brand guide §1). */
  --brand-gradient: linear-gradient(100deg, var(--brand-violet), var(--ui-accent) 55%, var(--brand-magenta));

  /* ---- the granted crowns -------------------------------------------------------------------
     The two colours a granted crown lights up in, mirrored from the app's tokens.css by name and
     by value — these are the same tokens, and GrantedCrownParityTests reads both files and holds
     them equal for the same reason BrandTokenParityTests does. The values are Accessories.Azure
     and Accessories.Amethyst in Core, which is where the choice is argued: gold and green are
     reserved (brand guide §1), and gold in particular is out because these crowns are worn BESIDE
     the gold winner's crown.

     A glow and nothing else would be colour carrying a state on its own, which nothing here is
     allowed to do. Every surface that lights up also says the word — "Granted" under the builder
     tile — and the glow is the third carrier, never the first. */
  --granted-beta:  #3d9bff;
  --granted-dev:   #b14bff;
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
}

/* The brand accent moved down the brand's own gradient for a pastel ground — a CONTRAST fix, not a
   second brand, and the same one the app makes in ThemeStyle. Surfaces that stay on brand ink (the
   lobby) keep the purple; the drawer and sheets, which DO go white on a light theme, get the
   violet end. `data-theme-tone` is written by app.js and, since this rule exists, by
   public-theme.js before first paint as well — otherwise the pre-paint pass and the client
   disagreed for a frame. */
:root[data-theme-tone="light"] {
  --ui-accent: var(--accent, var(--brand-violet));
  --brand-text: var(--text);
  --brand-text-secondary: var(--text-dim);
  --brand-text-muted: var(--text-dim);
}

/* All interactive chrome follows the same primary outline colour used by the
   Background effects choices. This keeps active tabs and buttons visually tied
   to the selected theme instead of falling back to brand violet. */
:root {
  --ui-accent: var(--accent, var(--brand-purple));
  --ui-accent-hover: color-mix(in srgb, var(--ui-accent) 82%, white);
  --ui-accent-ink: var(--brand-primary, #ffffff);
}

/* The one place the canvas HUD can find out where the notch is. Script cannot read the four
   properties above — getPropertyValue hands back the unresolved env() token, because a custom
   property is substituted where it is used and env() only resolves in a real declaration. A
   padding is a real declaration, so getComputedStyle on this element reports the insets as
   pixels. app.js/safeArea() creates it and measures it; overriding --safe-* on :root moves the
   HUD, which is how it is tested without a notched device. */
.safe-area-probe {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
  visibility: hidden;
  pointer-events: none;
  padding: var(--safe-top) var(--safe-right) var(--safe-bottom) var(--safe-left);
}

* {
  box-sizing: border-box;
}

html,
body {
  width: 100%;
  height: 100%;
  margin: 0;
  overflow: hidden;
  background: #05070c;
  color: var(--text);
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  letter-spacing: 0;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

button,
input {
  font: inherit;
}

button {
  cursor: pointer;
  transition:
    transform 180ms ease,
    border-color 180ms ease,
    background 180ms ease,
    filter 180ms ease,
    box-shadow 180ms ease;
}

button:hover {
  filter: brightness(1.04);
}

/* 60ms, because a press that eases in over 180 has been released before it arrives (brand
   guide §6). The transition above is the idle/hover one; the press overrides its own duration. */
button:active {
  transform: translateY(1px);
  transition-duration: 60ms;
}

button:focus-visible,
input:focus-visible,
summary:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--ui-accent) 70%, white 0%);
  outline-offset: 2px;
}

#game {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  display: block;
  touch-action: none;
}

/* A high-DPI canvas fill touches every physical pixel for every frame of the flash. This layer's
   colour is painted once and only its opacity is animated by the compositor. It remains above the
   game canvas, where the wash has always appeared, without intercepting steering. */
#prize-wash {
  position: fixed;
  z-index: 1;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  will-change: opacity;
}

/* Live Team Deathmatch kills, in the only clear strip between the map and team board. The names
   carry team colour while the verb stays neutral, so "Alpha hunted Beta" can be read at a
   glance without turning the whole sentence into one side's announcement. */
.team-kill-feed {
  position: fixed;
  z-index: 30;
  top: calc(var(--safe-top) + 54px);
  left: 50%;
  display: grid;
  gap: 6px;
  width: min(420px, calc(100vw - var(--safe-left) - var(--safe-right) - 32px));
  transform: translateX(-50%);
  pointer-events: none;
}

/* Your own line. It is the one a player is looking for, and in a Royale wipe it is one of three
   that arrived in the same second. The accent edge is the only difference — the names inside keep
   their own colours, which are what the feed is actually for. */
.team-kill-notice.is-yours {
  border-color: color-mix(in srgb, var(--ui-accent) 60%, transparent);
}

.team-kill-notice {
  justify-self: center;
  max-width: 100%;
  padding: 7px 12px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 999px;
  background: rgba(5, 8, 13, 0.86);
  box-shadow: 0 8px 26px rgba(0, 0, 0, 0.34);
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 800;
  line-height: 1.2;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
  animation: team-kill-in 180ms ease-out both;
}

.team-kill-notice strong { font-weight: 900; }
.team-kill-notice.is-leaving { animation: team-kill-out 260ms ease-in both; }

@keyframes team-kill-in {
  from { opacity: 0; transform: translateY(-8px) scale(0.96); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes team-kill-out {
  from { opacity: 1; transform: translateY(0); }
  to { opacity: 0; transform: translateY(-7px); }
}

@media (prefers-reduced-motion: reduce) {
  .team-kill-notice,
  .team-kill-notice.is-leaving { animation: none; }
}

.menu-shell {
  position: fixed;
  inset: 0;
  display: grid;
  /* "safe" is load-bearing, not decoration. A centred grid item taller than its container
     overflows BOTH edges, and the half that goes off the top cannot be scrolled back to —
     which is exactly what happened on a phone once the rounds drawer listed eight matches:
     the list ran off the screen and nothing would scroll. safe centre falls back to start
     the moment the content stops fitting, so the overflow all goes one way, downwards,
     where the scrollbar can reach it. */
  align-items: safe center;
  justify-items: center;
  gap: 14px;

  /* The shell's own vertical padding, named so the panel inside it can be capped at the space
     that is actually left. It was a literal in five places and the cap was a sixth number
     guessing at them — see .menu-panel's max-height. */
  --shell-top: 58px;
  --shell-bottom: 18px;
  padding:
    calc(var(--shell-top) + var(--safe-top))
    calc(18px + var(--safe-right))
    calc(var(--shell-bottom) + var(--safe-bottom))
    calc(18px + var(--safe-left));
  pointer-events: none;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.menu-shell.is-hidden,
.is-hidden {
  display: none !important;
}

.menu-panel,
.end-panel,
.pause-panel {
  width: min(520px, calc(100vw - 36px - var(--safe-left) - var(--safe-right)));
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent 32%),
    var(--panel);
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: var(--radius);
  box-shadow:
    0 22px 70px rgba(0, 0, 0, 0.48),
    inset 0 1px 0 rgba(255, 255, 255, 0.08);
  backdrop-filter: blur(18px) saturate(1.22);
  -webkit-backdrop-filter: blur(18px) saturate(1.22);
  pointer-events: auto;
}

/* The cap is the shell's own padding, not a number that hopes to match it.

   It was `100vh - 36px`, and the shell's vertical padding is 58+18 at this size and four other
   pairs across the breakpoints — 86+28 on a desktop, 72+16 on a phone. So every panel was allowed
   to be taller than the space it sits in, by up to 78px. Nothing showed it until a party lobby had
   a full party in it — eight people, when a side was eight: measured at 320x568 the panel's box
   ended 10px BELOW the bottom of the
   screen with `body { overflow: hidden }` above it, so the last of its own scrollable content
   could not be reached at all, and at 844x390 it overflowed by 78 and "Start the round" was off
   the screen with nothing that would scroll to it.

   dvh rather than vh for the same class of reason: on a phone browser `100vh` is the viewport with
   the address bar hidden, which is not the viewport a player is looking at while it is showing. */
.menu-panel {
  display: grid;
  gap: 14px;
  padding: 18px;
  max-height: calc(100vh - var(--shell-top, 58px) - var(--shell-bottom, 18px)
    - var(--safe-top) - var(--safe-bottom));
  max-height: calc(100dvh - var(--shell-top, 58px) - var(--shell-bottom, 18px)
    - var(--safe-top) - var(--safe-bottom));
  overflow-y: auto;
  overscroll-behavior: contain;
}

.builder-panel {
  width: min(780px, calc(100vw - 36px - var(--safe-left) - var(--safe-right)));
  background: var(--panel-strong);
}

/* The pinned band: the header and the preview, held at the top of the panel's own scroller.

   Full-bleed — negative margins cancelling the panel's 18px padding — so the grids slide
   underneath it rather than past it in a gutter, and painted in `--panel-strong` rather than
   `--panel` because the panel's own ground is translucent and a translucent band over it composites
   to a different colour. Near-opaque is what a header needs anyway. */
.builder-top {
  position: sticky;
  top: 0;
  z-index: 4;
  display: grid;
  gap: 14px;
  margin: -18px -18px 0;
  padding: 18px 18px 12px;
  background: var(--panel-strong);
}

.close-label {
  display: none;
}

@media (min-width: 900px) {
  .builder-top .close-button {
    width: auto;
    min-width: 74px;
    padding-inline: 12px;
    gap: 6px;
  }

  .builder-top .close-label {
    display: inline;
    font-size: 12px;
    font-weight: 800;
  }
}

.home-head {
  min-height: 42px;
  justify-content: center;
  text-align: center;
}

.home-head h1 {
  width: 100%;
}

.site-brand {
  position: fixed;
  z-index: 3;
  top: calc(16px + var(--safe-top));
  left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: 34px;
  padding: 7px 12px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  background: rgba(5, 7, 12, 0.72);
  color: var(--text);
  font-size: 14px;
  font-weight: 900;
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.34);
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
}

#client-build {
  position: fixed;
  z-index: 2;
  left: calc(8px + var(--safe-left));
  bottom: calc(6px + var(--safe-bottom));
  color: rgba(255, 255, 255, 0.42);
  font: 500 10px/1.2 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: 0.02em;
  white-space: nowrap;
  /* This is diagnostic chrome, never a control. A fixed element with pointer events enabled wins
     elementsFromPoint and can make a real button underneath look inexplicably dead. */
  pointer-events: none;
  user-select: none;
}

/* ...and diagnostic chrome still has to stand somewhere. During play the bottom-leading corner is
   its designed home, under the HUD plate. Over the lobby it has none: the dashboard is centred at
   min(760px, 100vw - 28px), so the gutter only clears this label's ~120px above about 1020px wide,
   and below that "Build z7X-Z7n0Pscy5S14c" prints straight across the bottom row of the lobby. On
   a phone that is what read as a box stuck to the footer — a build id lying on top of a strip the
   fold had already cut in half.
   Nothing diagnostic is lost. The build is written to localStorage and rides every round-metrics
   payload; the corner label is a convenience, and this hides the label rather than the fact. */
@media (max-width: 1020px) {
  body:not(.is-playing):has(#home-panel:not(.is-hidden)) #client-build { display: none; }
}

.inner-window {
  position: fixed;
  z-index: 8;
  inset: 50% auto auto 50%;
  display: grid;
  gap: 22px;
  width: min(560px, calc(100vw - 32px - var(--safe-left) - var(--safe-right)));
  max-height: calc(100vh - 48px - var(--safe-top) - var(--safe-bottom));
  padding: 24px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-card);   /* a sheet is a card: 28, not 16 */
  background: linear-gradient(180deg, rgba(20, 28, 36, 0.98), rgba(14, 20, 26, 0.98));
  box-shadow: 0 28px 90px rgba(0, 0, 0, 0.52);
  transform: translate(-50%, -50%);
  overflow: auto;
  pointer-events: auto;
  animation: innerWindowIn 180ms ease-out;
}

.inner-window.is-hidden {
  display: none !important;
}

.inner-window-subtitle {
  margin-top: 5px;
  color: var(--text-dim);
  font-size: 13px;
}

.help-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.help-grid section {
  min-height: 112px;
  padding: 16px;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  background: rgba(30, 38, 51, 0.5);
}

.help-grid h3 {
  margin: 0 0 7px;
  color: var(--text);
  font-size: 15px;
}

.help-grid p {
  margin: 0;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.45;
}

.join-form {
  display: grid;
  gap: 14px;
}

.join-form .play-button {
  width: 100%;
}

@keyframes innerWindowIn {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 8px)); }
  to { opacity: 1; transform: translate(-50%, -50%); }
}

@media (max-width: 560px) {
  .inner-window {
    width: min(calc(100vw - 24px), 560px);
    max-height: calc(100vh - 32px - var(--safe-top) - var(--safe-bottom));
    padding: 18px;
  }

  .help-grid { grid-template-columns: 1fr; }
}

.brand-light {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 18px var(--accent);
}

.app-menu-toggle {
  position: fixed;
  z-index: 7;
  top: calc(14px + var(--safe-top));
  right: calc(14px + var(--safe-right));
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: var(--radius);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.1), transparent),
    rgba(5, 7, 12, 0.82);
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.36);
  backdrop-filter: blur(16px) saturate(1.2);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
}

.app-menu-toggle span,
.app-menu-toggle::before,
.app-menu-toggle::after {
  content: "";
  width: 19px;
  height: 2px;
  display: block;
  border-radius: 999px;
  background: var(--text);
}

.app-menu-toggle {
  gap: 5px;
}

.app-menu-toggle::before,
.app-menu-toggle::after {
  position: static;
}

.app-menu {
  position: fixed;
  z-index: 9;
  inset: 0;
  display: grid;
  justify-items: end;
  background: rgba(0, 0, 0, 0.42);
  pointer-events: auto;
}

.app-menu-panel {
  width: min(410px, calc(100vw - var(--safe-left)));
  height: 100%;
  display: grid;
  align-content: start;
  gap: 12px;
  padding:
    calc(18px + var(--safe-top))
    calc(16px + var(--safe-right))
    calc(18px + var(--safe-bottom))
    16px;
  border-left: 1px solid rgba(255, 255, 255, 0.16);
  background:
    linear-gradient(180deg, rgba(59, 245, 200, 0.08), transparent 32%),
    rgba(5, 7, 12, 0.94);
  box-shadow: -22px 0 70px rgba(0, 0, 0, 0.5);
  overflow-y: auto;
  overscroll-behavior: contain;
  backdrop-filter: blur(20px) saturate(1.2);
  -webkit-backdrop-filter: blur(20px) saturate(1.2);
}

.app-menu-head h2 {
  font-size: 22px;
}

h1,
h2,
h3,
p {
  margin: 0;
}

h1 {
  font-size: 38px;
  line-height: 1;
  font-weight: 850;
  letter-spacing: 0;
}

h2 {
  font-size: 28px;
  line-height: 1;
  font-weight: 850;
  letter-spacing: 0;
}

h3 {
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1;
  font-weight: 800;
  letter-spacing: 0;
  text-transform: uppercase;
}

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.panel-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

.icon-button {
  position: relative;
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
}

.icon-button::before {
  content: "";
  width: 18px;
  height: 18px;
  display: block;
  background: var(--text);
  mask: var(--icon) center / contain no-repeat;
  -webkit-mask: var(--icon) center / contain no-repeat;
}

#sound-toggle,
#builder-sound-toggle {
  --icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 9h4l5-4v14l-5-4H4zM17 8c1 1 1.5 2.3 1.5 4S18 15 17 16M19.5 5.5C21 7.3 22 9.5 22 12s-1 4.7-2.5 6.5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

#sound-toggle.is-off,
#builder-sound-toggle.is-off {
  opacity: 0.52;
}

#sound-toggle.is-off::after,
#builder-sound-toggle.is-off::after {
  content: "";
  position: absolute;
  width: 24px;
  height: 2px;
  background: var(--danger);
  transform: rotate(-38deg);
}

.close-button {
  --icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6 6l12 12M18 6L6 18' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E");
}

.random-button {
  --icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16 3h5v5M4 20 21 3M21 16v5h-5M15 15l6 6M4 4l5 5' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

.refresh-button {
  width: 34px;
  height: 34px;
  --icon: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 11a8 8 0 0 0-14.6-4.5L4 8M4 4v4h4M4 13a8 8 0 0 0 14.6 4.5L20 16M16 16h4v4' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

.refresh-button::before {
  width: 16px;
  height: 16px;
}

.snake-preview {
  width: 100%;
  height: 132px;
  display: block;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.045);
}

.launch-grid {
  display: grid;
  grid-template-columns: minmax(430px, 1.15fr) minmax(300px, 0.85fr);
  gap: 16px;
  align-items: start;
}

.preview-column,
.play-column {
  display: grid;
  gap: 12px;
  min-width: 0;
  align-content: start;
}

.play-column {
  align-content: start;
  min-height: 188px;
  padding: 14px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius);
  background:
    linear-gradient(135deg, rgba(59, 245, 200, 0.08), rgba(77, 166, 255, 0.06)),
    rgba(255, 255, 255, 0.045);
}

.action-grid,
.builder-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.builder-actions > :only-child {
  grid-column: 1 / -1;
}

.action-grid .plain-button {
  grid-column: 1 / -1;
}

.play-button,
.plain-button,
.end-actions button {
  min-height: 48px;
  border-radius: var(--radius);
  font-weight: 850;
  letter-spacing: 0;
}

.play-button {
  border: 0;
  background: var(--ui-accent);
  color: #03100d;
}

.play-button.secondary {
  background: #4da6ff;
  color: #03101b;
}

.plain-button,
.end-actions button:last-child {
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.1);
  color: var(--text);
}

.play-button:disabled,
.plain-button:disabled {
  cursor: progress;
  filter: saturate(0.62) brightness(0.78);
}

.account-card,
.scoreboard-card,
.audio-card {
  display: grid;
  gap: 10px;
  padding: 12px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.065);
}

.app-menu .account-card {
  gap: 12px;
  padding: 12px;
  border-color: rgba(77, 166, 255, 0.18);
  background:
    linear-gradient(135deg, rgba(59, 245, 200, 0.08), rgba(77, 166, 255, 0.07)),
    rgba(255, 255, 255, 0.045);
}

.account-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
}

.account-copy {
  display: grid;
  gap: 3px;
  min-width: 0;
}

.profile-crown {
  justify-self: start;
  padding: 3px 7px;
  border: 1px solid rgba(245, 190, 65, 0.3);
  border-radius: 999px;
  background: rgba(245, 190, 65, 0.1);
  color: var(--gold);
  font-size: 10px;
  font-weight: 850;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* Which pane you are looking at, and nothing else. `is-hidden` on a drawer section means the
   feature behind it is not available at all — no push key on the server, no navigator.vibrate in
   this browser — and belongs to the code that knows that. Keeping the two on separate classes is
   what stops opening the cog from un-hiding a section whose feature does not exist. */
.drawer-profile-pane.is-view-hidden,
.drawer-settings-pane.is-view-hidden {
  display: none !important;
}

.profile-stat-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}
.profile-season-button { display:flex; width:100%; margin-top:14px; padding:12px 14px; align-items:center; gap:10px; border:1px solid color-mix(in srgb,var(--accent) 38%,transparent); border-radius:14px; background:color-mix(in srgb,var(--accent) 10%,var(--panel)); color:var(--text); text-align:left; cursor:pointer; }
.profile-season-button:hover { border-color:var(--accent); transform:translateY(-1px); }
.profile-season-mark { color:var(--accent); font-size:20px; }
.profile-season-button strong,.profile-season-button small { display:block; }
.profile-season-button small { margin-top:2px; color:var(--text-dim); font-size:11px; }
.profile-season-arrow { margin-left:auto; color:var(--accent); font-size:25px; line-height:1; }

.profile-stat {
  display: grid;
  gap: 4px;
  min-width: 0;
  padding: 13px 8px;
  border: 1px solid var(--panel-border);
  border-radius: 13px;
  background:
    radial-gradient(circle at 50% 0, rgba(20, 184, 166, 0.12), transparent 72%),
    rgba(255, 255, 255, 0.035);
  text-align: center;
}

.profile-stat strong {
  overflow: hidden;
  color: var(--text);
  font-size: 23px;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  text-overflow: ellipsis;
}

.profile-stat span {
  color: var(--muted);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.profile-section-heading,
.profile-win-head,
.profile-win-detail {
  display: flex;
  align-items: center;
}

.profile-section-heading,
.profile-win-head {
  justify-content: space-between;
  gap: 10px;
}

.profile-refresh {
  padding: 3px 0;
  border: 0;
  background: transparent;
  color: var(--ui-accent-hover);
  font-size: 11px;
  font-weight: 800;
  cursor: pointer;
}

.profile-refresh:disabled {
  cursor: wait;
  opacity: 0.55;
}

.profile-win-list {
  display: grid;
  gap: 8px;
}

.profile-win {
  display: grid;
  gap: 8px;
  padding: 12px;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
}

.profile-win-head strong {
  color: var(--text);
  font-size: 13px;
}

.profile-win-head time {
  color: var(--muted);
  font-size: 11px;
}

.profile-win-detail {
  flex-wrap: wrap;
  gap: 6px;
}

.profile-win-detail span {
  padding: 3px 7px;
  border-radius: 999px;
  background: rgba(20, 184, 166, 0.09);
  color: var(--text-dim);
  font-size: 10px;
  font-weight: 750;
}

#account-name {
  font-size: 15px;
  font-weight: 900;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#account-sub {
  color: var(--text-dim);
  font-size: 12px;
  font-weight: 700;
}

.coin-badge {
  min-width: 86px;
  min-height: 48px;
  display: grid;
  align-content: center;
  justify-items: end;
  padding: 6px 10px;
  border: 1px solid rgba(59, 245, 200, 0.2);
  border-radius: var(--radius-small);
  background: rgba(59, 245, 200, 0.11);
}

.coin-badge span {
  color: var(--text-dim);
  font-size: 10px;
  font-weight: 850;
  text-transform: uppercase;
}

.coin-badge strong {
  color: var(--brand-gold);
  font-size: 22px;
  line-height: 1;
  font-weight: 950;
}

/* An account a refund has taken past zero. The badge shows what can be spent, which is nothing,
   and stops colouring it as a balance — a bright zero reads as a purse somebody simply emptied,
   which is the one wrong thing this state could say. The sentence underneath does the explaining;
   this only has to stop the badge claiming to be good news. The app's twin is
   AccountPill.razor.css / SettingsSheet.razor.css. */
.coin-badge.is-owing {
  border-color: rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.04);
}

.coin-badge.is-owing strong { color: var(--text-dim); }

/* Muted rather than a warning colour. Nothing has gone wrong — the player asked for their money
   back and got it — so this explains, it does not alarm. */
.coin-owing {
  margin: 8px 0 0;
  color: var(--text-dim);
  font-size: 11.5px;
  line-height: 1.45;
}

/* What the operator has decided about this account, at the top of the lobby.
 *
 * Plain panel until something is actually stopping play, and red only then. A grace week is a
 * deadline to act on, and painting it in the colour of a lockout would turn a week of warning into
 * a panic — which is the opposite of what the week is for.
 *
 * Deliberately NOT gold, however much a sentence about coins invites it. Gold means money and
 * reward (brand guide §1); a refund debt is neither, which is the same reason the coin pill drops
 * its gold rather than printing a negative in it. The app's lobby holds the same two rules. */
.standing-notice {
  margin: 0 0 12px;
  padding: 12px 13px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--panel-soft);
  color: var(--text);
  font-size: 13px;
  line-height: 1.45;
  text-align: left;
}

.standing-notice.is-blocked {
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}

.standing-notice p {
  margin: 0;
}

.standing-notice-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
}

.standing-notice-actions:empty,
.standing-notice-actions:has(> .is-hidden:only-child) {
  margin-top: 0;
}

/* Both clear 44px, the floor every other target in this lobby is held to. A notice telling somebody
   how to get their account back is the worst possible place for a button that is hard to hit. */
.standing-notice-actions button {
  appearance: none;
  min-height: 44px;
  padding: 0 14px;
  border-radius: 999px;
  border: 1px solid transparent;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}

.standing-notice-go {
  border-color: color-mix(in srgb, var(--blue) 60%, transparent);
  background: color-mix(in srgb, var(--blue) 24%, transparent);
  color: var(--text);
}

.standing-notice-quiet {
  border-color: color-mix(in srgb, var(--text-dim) 40%, transparent);
  background: transparent;
  color: var(--text-dim);
}

.auth-actions {
  display: flex;
  justify-content: start;
  align-items: center;
  gap: 8px;
  max-width: none;
  flex-wrap: wrap;
}

.google-slot {
  min-height: 40px;
}

.auth-button {
  min-height: 40px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.1);
  color: var(--text);
  padding: 0 12px;
  font-size: 13px;
  font-weight: 850;
  white-space: nowrap;
}

.auth-button.primary {
  min-width: 96px;
  border: 0;
  background: var(--accent);
  color: #03100d;
}

/* Follows the theme — see --apple-fill in app.js. Hard-coded white put a white button on a
   near-white panel under every light theme. */
.auth-button.apple {
  background: var(--apple-fill, #000000);
  color: var(--apple-ink, #ffffff);
}

.local-auth {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 8px;
}

.local-auth input {
  min-width: 0;
  min-height: 40px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
  padding: 0 11px;
  outline: none;
}

.local-auth input::placeholder {
  color: rgba(148, 160, 184, 0.82);
}

#auth-email,
#auth-password,
.auth-submit-row {
  grid-column: 1 / -1;
}

.auth-submit-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  align-items: center;
}

.auth-mode-switch {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  padding: 4px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.06);
}

.scoreboard-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.scoreboard-head h2 {
  font-size: 18px;
}

.scoreboard-list {
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.scoreboard-row {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr) auto;
  align-items: center;
  gap: 9px;
  min-height: 34px;
  color: var(--text);
}

.scoreboard-rank {
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(59, 245, 200, 0.16);
  color: var(--ui-accent);
  font-size: 12px;
  font-weight: 900;
}

.scoreboard-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  font-weight: 850;
}

.scoreboard-meta {
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 760;
  text-align: right;
  white-space: nowrap;
}

.scoreboard-empty {
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 700;
}

.home-dock {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}

.app-menu .audio-card {
  min-height: 72px;
  background: rgba(255, 255, 255, 0.045);
}

.audio-card {
  align-content: start;
}

.audio-summary {
  min-height: 34px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  color: var(--text);
  font-size: 18px;
  font-weight: 850;
  cursor: pointer;
  list-style: none;
}

.audio-summary::-webkit-details-marker {
  display: none;
}

.audio-summary::after {
  content: "";
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  background: var(--text-dim);
  mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m6 9 6 6 6-6' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m6 9 6 6 6-6' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}

.audio-card[open] .audio-summary::after {
  transform: rotate(180deg);
}

.audio-card .audio-grid {
  padding-top: 10px;
}

.audio-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 9px;
}

.audio-grid.compact {
  grid-template-columns: 1fr;
}

.audio-row,
.range-row {
  min-height: 42px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  color: var(--text);
  font-size: 13px;
  font-weight: 850;
}

.audio-row input {
  width: 20px;
  height: 20px;
  accent-color: var(--accent);
}

.range-row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 104px minmax(0, 1fr);
}

.range-row input {
  width: 100%;
  accent-color: var(--accent);
}

.source-row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  padding: 4px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.06);
}

/* Three positions rather than two. A modifier rather than a change to .source-row itself,
   because the prize flash picker below it has three buttons in a two-column grid and has shipped
   that way — retuning it is a look decision, not part of this. */
.source-row-three {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.source-button {
  min-height: 34px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 850;
}

.source-button.is-active {
  background: rgba(59, 245, 200, 0.18);
  color: var(--ui-accent);
}

.compact-row {
  display: grid;
  grid-template-columns: 1fr 126px;
  gap: 10px;
  align-items: end;
}

.field {
  display: grid;
  gap: 7px;
}

.field span {
  color: var(--text-dim);
  font-size: 12px;
  font-weight: 800;
  line-height: 1;
  text-transform: uppercase;
}

.field input {
  width: 100%;
  height: 46px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
  padding: 0 13px;
  outline: none;
}

.field input:focus {
  border-color: color-mix(in srgb, var(--ui-accent) 65%, white 0%);
  box-shadow: 0 0 0 3px rgba(59, 245, 200, 0.18);
}

.segmented {
  min-height: 46px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
  padding: 4px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.06);
}

.segment {
  min-height: 36px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--text-dim);
  font-weight: 800;
}

.segment.is-active {
  background: rgba(59, 245, 200, 0.18);
  color: var(--ui-accent);
}

.builder-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
}

/* ---- roster ---- */

.roster-row {
  display: flex;
  align-items: stretch;
  gap: 8px;
  min-width: 0;
}

/* Scrolls rather than wraps. Eight chips is the cap, and a strip that grows a second row on a
   narrow window would push the tabs and the whole builder down as somebody adds snakes. */
.snake-roster {
  display: flex;
  flex: 1 1 auto;
  gap: 8px;
  min-width: 0;
  overflow-x: auto;
  padding-bottom: 2px;
  scrollbar-width: none;
}

.snake-roster::-webkit-scrollbar {
  display: none;
}

.snake-chip {
  display: flex;
  flex: 0 0 auto;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  width: 78px;
  padding: 7px 6px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
}

/* Tinted with the accent rather than lightened: the theme-aware block below repaints every
   control surface for a pastel background, and a chip that said "selected" by being whiter would
   say nothing at all on Blush. */
.snake-chip.is-active {
  border-color: var(--ui-accent);
  background: color-mix(in srgb, var(--ui-accent) 14%, transparent);
}

/* The bands the snake is actually wearing, not a solid swatch: a flag or a painting is what makes
   one chip tellable from another at a glance. */
.chip-body {
  width: 100%;
  height: 11px;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3);
}

.chip-name {
  max-width: 100%;
  overflow: hidden;
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 760;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.snake-chip.is-active .chip-name {
  color: var(--text);
}

/* Dashed, so the one chip that is not a snake does not read as one. */
.snake-chip.add {
  justify-content: center;
  width: 48px;
  border-style: dashed;
  color: var(--text-dim);
  font-size: 22px;
  font-weight: 300;
}

.roster-delete {
  flex: 0 0 auto;
  align-self: center;
  padding: 8px 12px;
  border: 1px solid color-mix(in srgb, var(--danger, #ff4d6d) 45%, transparent);
  border-radius: var(--radius-small);
  background: color-mix(in srgb, var(--danger, #ff4d6d) 10%, transparent);
  color: var(--danger, #ff4d6d);
  font-size: 12px;
  font-weight: 760;
}

.builder-tabs {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 4px;
  padding: 4px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
}

.builder-tab {
  min-width: 0;
  min-height: 44px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 850;
  overflow: hidden;
  text-overflow: ellipsis;
}

.builder-tab.is-active {
  background: var(--ui-accent);
  color: #03100d;
}

.builder-pages {
  min-height: 260px;
}

.builder-page {
  display: grid;
  gap: 14px;
}

.control-block {
  display: grid;
  gap: 9px;
}

.control-block.wide {
  grid-column: 1 / -1;
}

/* The name box over a grid that has outgrown the screen. Finder.razor.css is the twin, and this
   carries its decisions rather than re-deciding them.

   A row, so 22px (brand guide 3) — the same radius every other row in this panel takes, because it
   is the same thing: a field is a row. The BRAND accent for focus, never the theme's: a player who
   picked Blush asked for a pink arena, not a pink search box (brand guide 1). */
.finder {
  display: flex;
  align-items: center;
  gap: 8px;

  background: var(--control-surface);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-button);
  padding: 0 8px 0 14px;

  /* 44pt, because the whole row is the target rather than the 14px glyph on it (brand guide 7) —
     tapping anywhere in this bar puts the caret in the box. */
  min-height: 44px;

  transition: border-color 150ms ease;
}

/* Focus is on the ROW, not on the input inside it: the input has no border of its own, so a focus
   ring drawn on it would be a rectangle floating inside a pill. */
.finder:focus-within {
  border-color: var(--ui-accent);
}

.finder-mark {
  flex: 0 0 auto;
  font-size: 15px;
  line-height: 1;
  color: var(--text-dim);
}

.finder-box {
  flex: 1 1 auto;
  min-width: 0;

  appearance: none;
  -webkit-appearance: none;
  border: none;
  background: transparent;
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  padding: 11px 0;

  /* The page turns selection off for the whole client, which is right for a game surface and wrong
     for a field: without this the caret cannot be moved and nothing in the box can be selected. */
  -webkit-user-select: text;
  user-select: text;
}

.finder-box:focus {
  outline: none;
}

.finder-box::placeholder {
  color: var(--text-dim);
  font-weight: 500;
  /* WebKit dims a placeholder of its own accord on top of whatever colour it is given, which on
     --text-dim over this ground lands under 3:1. Stated so the prompt is readable rather than a
     suggestion of one. */
  opacity: 1;
}

/* 44pt square, so the cross is a target rather than a decoration a thumb has to hunt for. */
.finder-clear {
  flex: 0 0 auto;
  appearance: none;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: none;
  background: transparent;
  color: var(--text-dim);
  font-size: 14px;
  border-radius: var(--radius-pill);
}

/* Drop, darken, light the border (brand guide 6). */
.finder-clear:active {
  transform: scale(0.94);
  background: var(--control-hover);
  color: var(--text);
  border-color: var(--ui-accent);
}

/* Under the box rather than beside it, so the number never competes with the field for width on a
   375px phone. 13px is the floor the scale puts under a sentence (brand guide 2). */
.finder-count {
  margin: 0;
  padding: 0 4px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dim);
}

/* Take away the movement, not the feedback (brand guide 6): the press keeps its colour and the
   focus keeps its border, and neither of them moves to say so. */
@media (prefers-reduced-motion: reduce) {
  .finder { transition: none; }
  .finder-clear:active { transform: none; }
}

.swatches {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(32px, 32px));
  gap: 9px;
  min-height: 34px;
}

.swatch {
  width: 32px;
  height: 32px;
  border: 2px solid transparent;
  border-radius: 50%;
  background: var(--swatch);
  box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.3);
}

.swatch.is-active {
  border-color: #ffffff;
  box-shadow:
    inset 0 0 0 2px rgba(0, 0, 0, 0.3),
    0 0 0 3px rgba(59, 245, 200, 0.24);
}

.option-grid {
  display: grid;
  gap: 8px;
}

.pattern-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.skin-grid,
.theme-grid,
.trail-grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* A wake is a wide, short thing, so its tile is too — a square chip would show a stub of one. */
.trail-canvas {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 2.6;
  border-radius: 8px;
}

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

.option {
  min-height: 40px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
  font-weight: 760;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.option.skin-option {
  display: grid;
  gap: 6px;
  min-height: 82px;
  align-content: center;
  justify-items: center;
  padding: 8px 6px;
}

.option.chip-option {
  min-height: 38px;
  padding-inline: 14px;
  border-radius: 999px;
}

.skin-art {
  position: relative;
  width: 100%;
  aspect-ratio: 1.55;
  display: grid;
  place-items: center;
  border-radius: 6px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.08);
  font-size: 42px;
  line-height: 1;
}

.skin-art.plain {
  background: var(--skin-plain, #3bf5c8);
}

.skin-art.flag {
  background: transparent;
}

.skin-bands {
  width: 100%;
  height: 6px;
  border-radius: 999px;
  background: var(--skin-bands, transparent);
}

.option-label,
.face-name {
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.option.is-active {
  border-color: color-mix(in srgb, var(--ui-accent) 55%, transparent);
  background: rgba(59, 245, 200, 0.16);
  color: var(--ui-accent);
}

/* The cover note and the button that answers it, read as one thing rather than as prose with a
   button near it. First on the Colour tab, because it is what is stopping the rest of the tab from
   doing anything. */
.skin-cover {
  display: grid;
  gap: 10px;
  padding: 12px 14px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-small);
  background: var(--panel-soft);
}

.skin-cover .hint {
  margin: 0;
}

/* Quiet, not a call to action. It is the way out of a state, and a snake in a flag is a perfectly
   good snake — a blue slab here would read as "you have made a mistake". */
.skin-take-off {
  padding: 10px 14px;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  background: var(--control-surface);
  color: var(--text);
  font-size: 13.5px;
  font-weight: 760;
}

.skin-take-off:hover { background: var(--control-hover); }

/* ---- padlocks ----------------------------------------------------------------------------
   A locked chip is dimmed, not disabled. It is still a button — it opens the confirmation — and a
   greyed-out control that nonetheless responds is a worse lie than a dim one that does.

   The art dims and the label does not: the point is that you can see exactly what you would be
   getting, and the level under it is the one thing that has to stay legible. Builder.razor.css
   dims the same halves for the same reason; a padlock that looked different in the two clients
   would read as two different rules. */

/* 0.85, not 0.4. Dimmed to 0.4 the shop was the least legible thing on the screen: the thirty-odd
   hats and eyes a player might spend coins on were the ones they could not see, while the three
   they already own were bright. A padlock is meant to be an invitation, and nobody is tempted by a
   shape they cannot make out. Builder.razor.css lifted the same halves by the same amount. */
.option.is-locked .skin-art,
.option.is-locked .skin-bands,
.option.is-locked .face-canvas {
  opacity: 0.85;
}

.option.is-locked .option-label,
.option.is-locked .face-name {
  color: var(--text-dim);
}

.option-lock {
  max-width: 100%;
  color: var(--text-dim);
  font-size: 10px;
  font-weight: 760;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- where the player stands ----
   Under the preview and above the roster, because it is about the account rather than about the
   snake in front of you — and because the very next thing a player does after seeing a padlock is
   look for how far away it is. The app's builder stacks the same three lines in the same order. */

.standing {
  display: grid;
  gap: 6px;
  padding: 10px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.04);
}

.standing-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.standing-level {
  min-width: 0;
  font-size: 13px;
  font-weight: 760;
  color: var(--text);
  /* One line. "Level 12 · 1250 food to level 13" would otherwise wrap on a narrow window and push
     the bar down a row, so the panel changes height as the number grows. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.standing-coins {
  flex: 0 0 auto;
  font-size: 12px;
  font-weight: 760;
  color: var(--brand-gold);
}

/* Fixed height and a rounded track: a bar visible only when part-full reads as an error at 0% and
   at 100%. */
.standing-bar {
  height: 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.09);
  overflow: hidden;
}

.standing-bar > span {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
  /* Grows rather than jumps when a purchase or a refresh moves it. */
  transition: width 240ms ease;
}

/* The account's copy of the panel sits inside a card that already has a border, so it drops its
   own and keeps the spacing. Same three lines, same order, one less box. */
.account-standing {
  margin-top: 12px;
  padding: 0;
  border: 0;
  background: none;
}

.standing-next {
  font-size: 11.5px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.face-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
}

.face-option {
  width: 78px;
  min-height: 94px;
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 4px;
  padding: 6px 5px;
}

.face-canvas {
  width: 58px;
  height: 58px;
  display: block;
}

.face-name {
  color: var(--text-dim);
  font-size: 10.5px;
  font-weight: 760;
  line-height: 1.1;
}

.face-option.is-active .face-name {
  color: var(--ui-accent);
}

/* ---- the granted crowns ----

   "A glow which goes around the box" — the owner's words. It goes on the CONTAINER rather than on
   the artwork: a glow painted into the snake's own head would have to be an accessory part, and
   AccessoryTests refuses a translucent part outside the Face slot, so two of the three renderers
   could not draw it at all.

   These two class names are the contract with Builder.razor.css — the app writes exactly
   granted-beta / granted-dev / granted-plain onto its own tile, from the same switch in
   Builder.GlowClass that grantedGlowClass mirrors here.

   Restrained on purpose. This is a grid of ninety chips and one of them glowing is enough; a
   pulsing one would be the loudest thing on a screen whose subject is the snake in the middle. */
.face-option.granted-beta,
.face-option.granted-dev,
.face-option.granted-plain {
  border-color: color-mix(in srgb, var(--granted-glow) 55%, transparent);
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--granted-glow) 35%, transparent),
    0 0 18px color-mix(in srgb, var(--granted-glow) 42%, transparent);
}

.face-option.granted-beta  { --granted-glow: var(--granted-beta); }
.face-option.granted-dev   { --granted-glow: var(--granted-dev); }
/* A granted item nobody has picked a colour for yet. The brand accent, so it still reads as
   "given" rather than silently losing its glow — see grantedGlowClass in app.js. */
.face-option.granted-plain { --granted-glow: var(--ui-accent); }

/* The word under the glow. Colour never carries a state on its own here, and "this one was given
   to you" is a state. Builder.razor puts the same word in the same place. */
.face-option .granted {
  display: block;
  margin-top: 2px;
  color: var(--granted-glow);
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* The chip being worn keeps its own ring. Two rings would read as a chip that is both selected and
   not, so the glow softens and the selection wins — being worn is the more urgent fact. */
.face-option.granted-beta.is-active,
.face-option.granted-dev.is-active,
.face-option.granted-plain.is-active {
  box-shadow: 0 0 22px color-mix(in srgb, var(--granted-glow) 50%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  /* Nothing to disable — the glow is static by design, and this rule is here to say that was a
     choice rather than an omission. A breathing crown is exactly the thing somebody with
     vestibular sensitivity has asked the OS to stop. Builder.razor.css says the same. */
  .face-option.granted-beta,
  .face-option.granted-dev,
  .face-option.granted-plain { animation: none; }
}

.accessory-list {
  display: grid;
  gap: 14px;
}

.hint {
  color: var(--text-dim);
  font-size: 12.5px;
  line-height: 1.45;
}

.status {
  min-height: 18px;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 18px;
}

.status.is-bad {
  color: var(--danger);
}

.end-panel {
  position: fixed;
  left: calc(18px + var(--safe-left));
  bottom: calc(18px + var(--safe-bottom));
  display: grid;
  gap: 12px;
  padding: 18px;
  pointer-events: auto;
}

.end-panel.is-death {
  inset: 0;
  z-index: 6;
  place-content: center;
  justify-items: center;
  padding: 28px;
  text-align: center;

  /* This shares a rule with the menu and pause cards, which pins width to 520px and gives them a
     bordered, rounded, shadowed panel. `inset: 0` cannot stretch a box that has an explicit width,
     so without these the death wash rendered as a 520px column with a hard rounded edge down the
     middle of the screen instead of covering it.

     `max-width` has to be reset for the same reason and was the half that got missed: the narrow
     breakpoint also caps these panels at `100vw - 24px - the safe areas`, which is a card inset
     and nonsense for a full-screen wash. It left the red exactly 24px short of the edge on a
     phone — full height, visibly not full width. */
  width: auto;
  max-width: none;
  max-height: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;

  /* Thinner than it was, and barely blurred. The world keeps running behind this now, and the
     point of the wash is to colour the aftermath rather than hide it — at 0.58 with a 3px blur
     you could tell something was moving but not what. */
  background: rgba(92, 8, 18, 0.42);
  backdrop-filter: blur(1px);
  -webkit-backdrop-filter: blur(1px);

  /* Fades in once, then breathes. Slow and shallow on purpose: the wash sits under a screen you
     are reading, so it should register as the game still being alive underneath rather than as a
     flash competing with the title. */
  animation:
    death-wash 520ms ease-out both,
    death-breathe 3.6s ease-in-out 520ms infinite;
}

/* The stun: the same full-screen wash as the death, in the game's own colours rather than the
   danger red.
 
   That is the point of it. The two screens used to differ in palette for no reason a player could
   name — reported as "when the screen shows the revive, it's a different theme" — because one was
   the ordinary panel and the other the red one, and both said you were dead. Now the red IS the
   death: as long as you are still in the round the colours are the round's, and they turn when
   you actually go. It does not breathe either; a pulsing background under a decision on a clock
   is a distraction, where under a score you are reading it is atmosphere. */
.end-panel.is-stunned {
  inset: 0;
  z-index: 6;
  place-content: center;
  justify-items: center;
  padding: 28px;
  text-align: center;

  /* Same overrides as .is-death, and for the same reason: these panels share a rule that pins
     them to a 520px card, and `inset: 0` cannot stretch a box with an explicit width. */
  width: auto;
  max-width: none;
  max-height: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;

  background: rgba(4, 8, 16, 0.52);
  backdrop-filter: blur(1px);
  -webkit-backdrop-filter: blur(1px);
  animation: death-wash 320ms ease-out both;
}

/* The explanation under the clock now, not the thing being announced, so it sits well below the
   size the same words take on the death screen. */
.end-panel.is-stunned h2 {
  color: var(--text);
  font-size: clamp(24px, 5vw, 40px);
  letter-spacing: 0.06em;
  text-shadow: 0 0 28px rgba(0, 0, 0, 0.6);
  animation: death-title 460ms cubic-bezier(.2, 1.4, .4, 1) both;
}

.end-panel.is-stunned p { color: var(--text-dim); font-size: 16px; }

/* Quiet on purpose. It is the alternative to the thing the player came here for, and it has to be
   findable without competing with it — the mistake in the other direction is a give-up button that
   reads as the primary action on a screen selling a way to carry on. */
.give-up-button {
  grid-column: 1 / -1;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.24);
  color: var(--text-dim);
}

/* The clock, which is the largest thing on this screen and the first thing read.
 
   Asked for by name: above the "you are stunned" and bigger than it. It sat under the buttons in
   small print, which is where the eye arrives last, for the number the whole decision is being
   made against.
 
   tabular-nums and a fixed min-width are what stop it rocking sideways as it counts. The digit is
   the element the eye is fixed on and a proportional one moves its own centre every second, which
   at this size reads as a fault rather than as a clock. */
/* Scoped to the panel, and deliberately at three classes' worth of specificity: `.end-panel
   .is-stunned p` below sets the sub-line to 16px, and a bare `.stun-clock` loses to it — which is
   how the clock shipped, briefly, at the size of a caption. */
.end-panel.is-stunned .stun-clock {
  margin: 0 0 -6px;
  min-width: 2ch;
  text-align: center;
  font-size: clamp(56px, 13vw, 88px);
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  text-shadow: 0 0 30px color-mix(in srgb, var(--accent) 40%, transparent);
}



.end-panel.is-death h2 {
  color: #fff1f2;
  font-size: clamp(38px, 7vw, 76px);
  letter-spacing: 0.06em;
  text-shadow: 0 0 28px rgba(255, 64, 82, 0.72);
  animation: death-title 700ms cubic-bezier(.2, 1.4, .4, 1) both;
}

.end-panel.is-death p { color: rgba(255, 225, 228, 0.86); font-size: 16px; }

/* The win: the third state of this panel, and the one that was missing.
 
   A Royale winner used to get `.is-death` — the danger wash, at the one moment in the game worth
   celebrating. It is the same full-screen wash as the other two, because the round is still being
   drawn behind all three and a card would hide the thing the player has just won; what changes is
   the colour, which is the game's own accent rather than the danger red. The change of palette is
   the outcome, exactly as it is between the stun and the death.
 
   It breathes like the death rather than sitting still like the stun. There is no clock here and
   nothing to decide, so movement under the score is atmosphere rather than a distraction — the
   same argument that keeps it off the stunned screen, pointed the other way. */
.end-panel.is-win {
  inset: 0;
  z-index: 6;
  place-content: center;
  justify-items: center;
  padding: 28px;
  text-align: center;

  /* Same overrides as the other two, and for the same reason: these panels share a rule that pins
     them to a 520px card, and `inset: 0` cannot stretch a box with an explicit width. */
  width: auto;
  max-width: none;
  max-height: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;

  background: color-mix(in srgb, var(--accent) 16%, rgba(4, 8, 16, 0.62));
  backdrop-filter: blur(1px);
  -webkit-backdrop-filter: blur(1px);
  animation:
    death-wash 520ms ease-out both,
    win-breathe 3.6s ease-in-out 520ms infinite;
}

/* The placement, at the size the death screen gives its verdict. On a win the placement IS the
   news — `#1` is the whole headline — where on a death it is a consolation in the line
   underneath. That asymmetry is the app's (RoundVerdict.WonHeadline) and it is deliberate. */
.end-panel.is-win h2 {
  color: var(--accent);
  font-size: clamp(38px, 7vw, 76px);
  letter-spacing: 0.06em;
  text-shadow: 0 0 30px color-mix(in srgb, var(--accent) 55%, transparent);
  animation: death-title 700ms cubic-bezier(.2, 1.4, .4, 1) both;
}

.end-panel.is-win p { color: var(--text); font-size: 16px; }

/* Scoped to the panel, and deliberately at two classes' worth of specificity: `.end-panel.is-win
   p` below sets every paragraph on this screen to 16px, and a bare `.end-trophy` loses to it —
   which shipped the trophy at the size of a full stop, exactly as the stun clock once shipped at
   the size of a caption. Caught in a screenshot, which is the only thing that catches it. */
.end-panel.is-win .end-trophy {
  margin: 0;
  font-size: clamp(44px, 9vw, 64px);
  line-height: 1;
  filter: drop-shadow(0 0 22px color-mix(in srgb, #F5C542 60%, transparent));
  animation: trophy-pop 700ms cubic-bezier(0.2, 1.5, 0.4, 1) both;
}

/* What coming first actually was, rather than a second way of saying "#1". */
.end-panel.is-win .end-crown {
  margin: -4px 0 0;
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
}

/* ---- the middle tier -------------------------------------------------------------------------

   A side that came second got `.is-death`: the red wash, the death sting and the music cut, under
   the headline "YOUR TEAM CAME 2ND". That is the same fault the win branch above was written to
   fix, one tier down, with the sound saying the opposite of the words.

   The red is not about a placement. It is about YOUR SNAKE dying, and in Team Deathmatch those are
   different events that need not happen in the same minute: your own death is terminal but not
   final, the round carries on and you watch your side, and a RESULT can arrive while your snake is
   still perfectly alive — once the ring has closed the round is decided on the field, so second,
   third and last can all still have snakes standing when the verdict lands.

   So there are four states on this panel now and `.is-death` is reached only by an individual
   verdict. Both new ones are the same full-screen wash as the other three, because the round is
   still being drawn behind all of them; what changes is the colour, which is how this panel has
   always said what happened. Which class a placement wears is Core's answer, carried in
   js/teams.js as `teamTone` — see RoundVerdict.TeamTone for why the cut is at second. */
.end-panel.is-runner-up,
.end-panel.is-placed {
  inset: 0;
  z-index: 6;
  place-content: center;
  justify-items: center;
  padding: 28px;
  text-align: center;

  /* Same overrides as the other three, and for the same reason: these panels share a rule that
     pins them to a 520px card, and `inset: 0` cannot stretch a box with an explicit width. */
  width: auto;
  max-width: none;
  max-height: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;

  backdrop-filter: blur(1px);
  -webkit-backdrop-filter: blur(1px);

  /* Fades in and then holds. It does not breathe: the death and the win both do, because both are
     announcements, and a placement is a result being read. The stunned screen makes the same call
     from the other direction. */
  animation: death-wash 520ms ease-out both;
}

/* Second of more than two: the near miss, with a silver cast over the dark rather than a colour of
   its own. It is the only losing outcome a player replays in their head, and in a four-sided round
   it is the natural cut — but it is still a loss, so it is marked rather than celebrated. */
.end-panel.is-runner-up {
  background: color-mix(in srgb, #C7CEDB 9%, rgba(4, 8, 16, 0.60));
}

/* Third down to last: the dark on its own, and nothing else. The words carry it — "YOUR TEAM CAME
   3RD" and "YOUR TEAM CAME LAST" already differ in the one way that matters, and three shades of
   losing would only make the worst one feel designed for. */
.end-panel.is-placed {
  background: rgba(4, 8, 16, 0.64);
}

.end-panel.is-runner-up h2,
.end-panel.is-placed h2 {
  color: var(--text);
  font-size: clamp(38px, 7vw, 76px);
  letter-spacing: 0.06em;
  animation: death-title 700ms cubic-bezier(.2, 1.4, .4, 1) both;
}

/* The glow is the whole difference between the two, at half the trophy's strength for the one that
   nearly had it and absent for the ones that did not. */
.end-panel.is-runner-up h2 { text-shadow: 0 0 24px rgba(199, 206, 219, 0.45); }
.end-panel.is-placed h2 { text-shadow: 0 0 24px rgba(0, 0, 0, 0.55); }

.end-panel.is-runner-up p,
.end-panel.is-placed p { color: var(--text-dim); font-size: 16px; }

/* The medal shares the trophy's box and its pop, and only its colour changes: a second-place mark
   that arrived differently from a first-place one would read as a different kind of thing rather
   than as the next one down. Scoped at two classes' worth of specificity for the reason the
   trophy's rule records — `.is-runner-up p` above sets every paragraph on this panel to 16px, and
   a bare `.end-trophy` loses to it and ships at the size of a full stop. */
.end-panel.is-runner-up .end-trophy {
  margin: 0;
  font-size: clamp(44px, 9vw, 64px);
  line-height: 1;
  filter: drop-shadow(0 0 18px rgba(199, 206, 219, 0.55));
  animation: trophy-pop 700ms cubic-bezier(0.2, 1.5, 0.4, 1) both;
}

.end-panel.is-runner-up .end-crown,
.end-panel.is-placed .end-crown {
  margin: -4px 0 0;
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
}

/* ---- where all four sides came --------------------------------------------------------------

   The team round's result, under the verdict on the same shared panel. A losing side used to be
   told "YOU ARE DEAD" and nothing about where it finished, which in a mode whose entire result is
   a team's standing is the one fact the screen existed to carry.

   A fixed-width column rather than a full-width table: the wash covers the screen, and four short
   rows stretched across a 1440px window would read as a list of unrelated words. The rank column
   is an em-based track so "still in" and "1st" cannot shove the names out of line. */
.end-teams {
  list-style: none;
  width: min(100%, 300px);
  margin: 18px 0 0;
  padding: 16px 0 0;
  border-top: 1px solid rgba(255, 255, 255, 0.16);
  display: grid;
  gap: 8px;
  text-align: left;
}

/* The table a round ends with: everybody who was in it, in the order they finished.

   Sits under the team table and matches its width, so a team round reads as one panel with two
   tables in it rather than two panels stacked. The app's twin is FinalTable.razor.css and the two
   are laid out the same way for the same reasons — four columns, three of them fixed, and the dot,
   the name and the You pill sharing ONE cell because two of the three are absent on most rows. */
.end-final {
  width: min(100%, 300px);
  margin: 18px 0 0;
  padding: 16px 0 0;
  border-top: 1px solid rgba(255, 255, 255, 0.16);
  display: grid;
  gap: 8px;
  text-align: left;
}

.end-final h3 {
  margin: 0;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  opacity: 0.66;
}

/* One shared column definition, so the header and every row line up by construction rather than by
   two lists of widths that agree today. */
.end-final-head,
.end-final-row {
  display: grid;
  grid-template-columns: 2.8em minmax(0, 1fr) 3.4em 2.4em;
  align-items: center;
  gap: 8px;
}

.end-final-head {
  padding: 0 10px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  opacity: 0.55;
}

.end-final-rows {
  list-style: none;
  display: grid;
  gap: 3px;
  margin: 0;
  padding: 0;
  /* Thirty rows is taller than a phone. Scrolls inside itself rather than pushing Play again off
     the bottom of the wash — the actions are what somebody came to this panel to press. */
  max-height: 42vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.end-final-row {
  padding: 7px 10px;
  border: 1px solid transparent;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  font-size: 13px;
  line-height: 1.2;
}

/* Your own row, which is the one every player looks for first. A border and a wash — and the "You"
   pill inside it, which is what actually says so rather than the colour doing it alone. */
.end-final-row.is-yours {
  border-color: color-mix(in srgb, var(--ui-accent) 55%, transparent);
  background: color-mix(in srgb, var(--ui-accent) 14%, transparent);
}

.end-final-rank { font-weight: 700; font-variant-numeric: tabular-nums; opacity: 0.72; }

/* A snake with no place yet reads as a note rather than as a rank, because it is not one — the same
   treatment .end-team-place.is-unplaced gets one level up. */
.end-final-rank.is-unplaced {
  font-size: 9.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
  opacity: 0.6;
}

.end-final-name { display: flex; align-items: center; gap: 7px; min-width: 0; }

.end-final-swatch { flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%; }

.end-final-who {
  min-width: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.end-final-you {
  flex: 0 0 auto;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--ui-accent);
  color: var(--ui-accent-ink);
  font-size: 9px;
  font-weight: 800;
  letter-spacing: .05em;
  text-transform: uppercase;
}

.end-final-length,
.end-final-hunts {
  /* Tabular, so a column of numbers is a column rather than a ragged edge. */
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  text-align: right;
}

.end-final-hunts { opacity: 0.7; }

/* Why the table is not final yet. A sentence and not only the per-row marks: the marks are easy to
   miss, and the claim matters — what is on screen is not the result. */
.end-final-caveat { margin: 0; font-size: 11px; line-height: 1.45; opacity: 0.6; }

.end-team-row {
  display: grid;
  grid-template-columns: 4.2em 12px minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  font-size: 15px;
}

.end-team-place { font-weight: 700; opacity: 0.72; }

/* A side that has not finished yet reads as a note rather than as a rank, because it is not one:
   your own placement is final the moment your side is out, and the sides above you are still being
   sorted by a round that has not ended. */
.end-team-place.is-unplaced { font-size: 11.5px; font-weight: 600; opacity: 0.6; }

.end-team-swatch { width: 12px; height: 12px; border-radius: 3px; }

.end-team-name {
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.end-team-yours {
  font-size: 10.5px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0.85;
}

/* The two washes set every paragraph on the panel; the list is not a paragraph, so it needs its
   own colour or it inherits the page's and disappears into the red. */
.end-panel.is-death .end-teams { color: rgba(255, 225, 228, 0.92); }
.end-panel.is-win .end-teams { color: var(--text); }
.end-panel.is-runner-up .end-teams,
.end-panel.is-placed .end-teams { color: var(--text); }

.end-panel.is-death .end-team-row.is-yours .end-team-name,
.end-panel.is-death .end-team-row.is-yours .end-team-yours { color: #fff1f2; }

.end-panel.is-win .end-team-row.is-yours .end-team-name,
.end-panel.is-win .end-team-row.is-yours .end-team-yours { color: var(--accent); }

/* Your own row, on the two tiers that are neither. Silver for the side that nearly had it and
   plain text for the rest — the same one-step difference the headline makes, so a reader finding
   their row and a reader reading the verdict are told the same thing. */
.end-panel.is-runner-up .end-team-row.is-yours .end-team-name,
.end-panel.is-runner-up .end-team-row.is-yours .end-team-yours { color: #edf1f7; }

.end-panel.is-placed .end-team-row.is-yours .end-team-name,
.end-panel.is-placed .end-team-row.is-yours .end-team-yours { color: var(--text); }

/* A team verdict is a sentence where an individual one is a number. "YOUR TEAM CAME LAST" at the
   76px ceiling above took three lines on a 390px phone and pushed the standings — the thing the
   headline exists to introduce — off the bottom of the screen. */
.end-panel.is-death:has(.end-teams:not(.is-hidden)) h2,
.end-panel.is-win:has(.end-teams:not(.is-hidden)) h2,
.end-panel.is-runner-up:has(.end-teams:not(.is-hidden)) h2,
.end-panel.is-placed:has(.end-teams:not(.is-hidden)) h2 {
  font-size: clamp(26px, 6vw, 44px);
}

/* Fixed rather than contained, so the burst covers the screen instead of the panel's content box,
   and inert to the pointer so it can never swallow a tap on Play again. */
.win-confetti {
  position: fixed;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 7;
}

.win-confetti span {
  position: absolute;
  top: -6vh;
  left: var(--x);
  width: 9px;
  height: 14px;
  border-radius: 2px;
  background: hsl(var(--hue) 90% 62%);
  animation: confetti-fall 2.8s linear var(--delay) forwards;
}

@keyframes win-breathe {
  0%, 100% { background-color: color-mix(in srgb, var(--accent) 14%, rgba(4, 8, 16, 0.62)); }
  50%      { background-color: color-mix(in srgb, var(--accent) 26%, rgba(4, 8, 16, 0.52)); }
}

@keyframes trophy-pop {
  0%   { transform: scale(0.2) rotate(-25deg); opacity: 0; }
  60%  { transform: scale(1.15) rotate(6deg); opacity: 1; }
  100% { transform: scale(1) rotate(0deg); opacity: 1; }
}

@keyframes confetti-fall {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); opacity: 1; }
  85%  { opacity: 1; }
  100% { transform: translate3d(var(--drift), 112vh, 0) rotate(var(--spin)); opacity: 0; }
}

@keyframes death-wash { from { opacity: 0; } to { opacity: 1; } }
@keyframes death-breathe {
  0%, 100% { background-color: rgba(92, 8, 18, 0.5); }
  50%      { background-color: rgba(126, 14, 30, 0.68); }
}
@keyframes death-title { from { transform: scale(.82); opacity: 0; } to { transform: scale(1); opacity: 1; } }

.pause-panel {
  position: fixed;
  z-index: 5;
  top: 50%;
  left: 50%;
  display: grid;
  gap: 14px;
  padding: 18px;
  transform: translate(-50%, -50%);
  pointer-events: auto;
}

/* One column, because there is one action. It was two — "Home" and "Build Snake" — and the second
   left the round for a screen that cannot affect it, which is the bug this row was cut down for. A
   1fr 1fr grid would leave the survivor sitting in half the panel with a hole beside it. */
.pause-actions {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
}

.end-panel h2 {
  font-size: 28px;
  line-height: 1.05;
}

.end-panel p {
  color: var(--text-dim);
}

.end-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.revive-button {
  grid-column: 1 / -1;
}

@media (max-width: 560px) {
  .menu-shell {
    align-items: safe center;
    justify-items: center;
    /* Named so .menu-panel's max-height follows the padding rather than guessing at it. */
    --shell-top: 54px;
    --shell-bottom: 12px;
    padding:
      calc(var(--shell-top) + var(--safe-top))
      calc(12px + var(--safe-right))
      calc(var(--shell-bottom) + var(--safe-bottom))
      calc(12px + var(--safe-left));
  }

  .menu-panel,
  .builder-panel,
  .end-panel,
  .pause-panel {
    width: 100%;
    max-width: calc(100vw - 24px - var(--safe-left) - var(--safe-right));
  }

  h1 {
    font-size: 30px;
  }

  .compact-row,
  .builder-grid,
  .action-grid,
  .builder-actions,
  .account-card,
  .account-main,
  .local-auth,
  .audio-grid,
  .launch-grid,
  .home-dock {
    grid-template-columns: 1fr;
  }

  #auth-email,
  #auth-password,
  .auth-submit-row {
    grid-column: 1 / -1;
  }

  .app-menu-panel {
    width: min(390px, 100vw);
  }

  .coin-badge {
    justify-items: start;
  }

  .auth-actions {
    justify-content: start;
    max-width: none;
    flex-wrap: wrap;
  }

  .range-row {
    grid-template-columns: 1fr;
    gap: 4px;
  }

  .match-field {
    max-width: none;
  }

  .pattern-grid,
  .skin-grid,
  .theme-grid,
  .trail-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Larger lobby identity; still removed completely for every active match state. */
.site-brand {
  /* The top padding is clearance for the crown, not decoration. The crown is absolutely placed at
     top: -17px against this header, so it hangs above the box and the wordmark has to start below
     where it lands. Measured on the live desktop layout: the crown's lower edge sat at y=42 while
     the wordmark's box began at y=28, overlapping the first line by 14px. Wider viewports were the
     only ones affected because this breakpoint sets the wordmark at 30px against the phone's 24px
     while lifting the crown only 4px further, so the larger type grew up into it.

     Keyed to the crown's own numbers rather than a round figure: 17px of lift, plus the 4px the
     crown's tilt carries it past its box, plus 5px of air. Change one and this has to move. */
  min-height: 90px !important;
  padding: 26px 10px 6px 4px !important;
}
.brand-type strong { font-size: 30px !important; }
.brand-type strong + strong { font-size: 28px !important; }
.brand-crown {
  top: -17px !important;
  left: 27px !important;
  width: 47px !important;
  height: 38px !important;
}
@media (max-width: 560px) {
  .site-brand {
    min-height: 58px !important;
    padding: 6px 8px 4px 3px !important;
  }
  .brand-type strong { font-size: 24px !important; }
  .brand-type strong + strong { font-size: 22px !important; }
  .brand-crown {
    top: -13px !important;
    left: 21px !important;
    width: 38px !important;
    height: 31px !important;
  }
}

/* The lobby brand is useful before launch, but competes with the arena HUD during play. */
body.is-playing .site-brand {
  opacity: 0 !important;
  visibility: hidden !important;
  pointer-events: none !important;
  transform: translateY(-12px) !important;
}

/* Keep the wordmark readable without placing a panel over the arena. */
.site-brand {
  min-height: 48px !important;
  padding: 4px 6px 4px 2px !important;
  border: 0 !important;
  border-radius: 0 !important;
  background: transparent !important;
  box-shadow: none !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

/* The account/guest pill belongs to the lobby, never over the in-game HUD. */
body.is-playing .account-menu-toggle {
  opacity: 0 !important;
  visibility: hidden !important;
  pointer-events: none !important;
  transform: translateY(-12px) !important;
}

/* Final lobby composition: the menu is an open game scene, with glass reserved for actions. */
.site-brand {
  display: inline-flex;
  align-items: center;
  position: fixed;
  gap: 0;
  min-height: 48px;
  padding: 4px 6px 4px 2px;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.brand-crown {
  position: absolute;
  top: -11px;
  left: 18px;
  width: 31px;
  height: 25px;
  object-fit: contain;
  filter: drop-shadow(0 0 5px rgba(248, 211, 106, 0.25));
  transform: rotate(-16deg);
}

.brand-type {
  display: grid;
  line-height: 0.86;
  text-align: left;
  margin-left: 0;
}

.brand-type strong {
  color: var(--text);
  font-family: Poppins, Inter, sans-serif;
  font-size: 20px;
  font-style: italic;
  font-weight: 950;
  letter-spacing: 0.035em;
}

.brand-type strong + strong {
  color: var(--accent);
  font-size: 18px;
}

.winners-open-button {
  grid-column: 1 / -1;
}

.scoreboard-panel {
  width: min(760px, calc(100vw - 56px - var(--safe-left) - var(--safe-right)));
  max-height: calc(100vh - 122px - var(--safe-top) - var(--safe-bottom));
  gap: 22px;
  padding: 28px;
  border-radius: 18px;
  overflow: auto;
}

.page-heading {
  display: flex;
  align-items: center;
  gap: 14px;
}

.page-heading h1 {
  color: var(--text);
  font-size: clamp(30px, 5vw, 46px);
  font-weight: 900;
  line-height: 1;
}

.scoreboard-search {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  gap: 10px;
}

.scoreboard-search .play-button {
  min-width: 112px;
  min-height: 48px;
}

.scoreboard-page-list {
  min-height: 300px;
  align-content: start;
  gap: 8px;
}

.scoreboard-page-list .scoreboard-row {
  min-height: 52px;
  padding: 0 12px;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  background: rgba(30, 38, 51, 0.54);
}

.scoreboard-pagination,
.pagination-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.scoreboard-page-status {
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 750;
}

.pagination-actions .plain-button {
  min-height: 42px;
  padding-inline: 14px;
}

.sr-live {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

@media (max-width: 560px) {
  .site-brand {
    min-height: 48px;
    padding: 5px 11px 5px 9px;
  }

  .brand-crown { top: -9px; left: 13px; width: 25px; height: 21px; }
  .brand-type strong { font-size: 16px; }
  .brand-type strong + strong { font-size: 15px; }

  .scoreboard-panel {
    width: min(calc(100vw - 24px - var(--safe-left) - var(--safe-right)), 620px);
    max-height: calc(100vh - 88px - var(--safe-top) - var(--safe-bottom));
    padding: 18px 14px;
  }

  .scoreboard-search {
    grid-template-columns: 1fr;
  }

  .scoreboard-search .play-button { width: 100%; }
  .scoreboard-pagination { align-items: stretch; flex-direction: column; }
  .pagination-actions { width: 100%; }
  .pagination-actions .plain-button { flex: 1; }
}

@media (max-height: 560px) and (orientation: landscape) {
  .menu-shell {
    align-items: safe center;
  }

  .menu-panel {
    max-width: 680px;
  }

  #home-panel {
    align-items: start;
  }

  #home-panel .panel-head,
  #home-panel .status {
    grid-column: 1 / -1;
  }

  .snake-preview {
    height: 104px;
  }
}

/* Premium lobby/settings refresh */
:root {
  --bg: #0e141a;

  /* Translucent, not the opaque #141c24 this used to be. The panels carry backdrop-filter, which
     does nothing behind a solid colour — so the glass was only ever glass in the stylesheet, and
     the live arena now running behind the lobby was completely hidden by it. Kept dark enough that
     white text on it still clears 4.5:1 against the brightest thing the arena can put behind it. */
  --panel: rgba(20, 28, 36, 0.68);
  --panel-strong: rgba(30, 38, 51, 0.82);
  --panel-soft: rgba(30, 38, 51, 0.72);
  --panel-border: rgba(255, 255, 255, 0.08);
  --text: #ffffff;
  --text-dim: #a7b0bb;
  --muted: rgba(167, 176, 187, 0.7);
  --accent: #14b8a6;
  --accent-hover: #25d2bf;
  --blue: #3882f6;
  /* This is the SECOND :root in this file and it wins, so it is the one that decides the radius
     for every panel, button and row that reads the token. It said 14px while the block at the top
     said 8 — neither on the guide's scale, and the top one had no effect at all. */
  --radius: var(--radius-button);
  --radius-lg: var(--radius-card);
  --shadow-soft: 0 22px 70px rgba(0, 0, 0, 0.34);
  --shadow-card: 0 18px 42px rgba(0, 0, 0, 0.22);
}

html {
  background: var(--bg);
}

body {
  background:
    radial-gradient(circle at 18% 18%, rgba(20, 184, 166, 0.08), transparent 28%),
    radial-gradient(circle at 78% 22%, rgba(56, 130, 246, 0.08), transparent 30%),
    var(--bg);
  color: var(--text);
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

h1,
h2,
.site-brand,
.play-button,
.plain-button,
.segment,
.source-button,
.auth-toggle-button {
  font-family: Poppins, Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

button,
input {
  transition:
    background-color 180ms ease-out,
    border-color 180ms ease-out,
    box-shadow 180ms ease-out,
    color 180ms ease-out,
    opacity 180ms ease-out,
    transform 180ms ease-out;
}

button:focus-visible,
input:focus-visible {
  outline: 2px solid rgba(20, 184, 166, 0.95);
  outline-offset: 3px;
}

.site-brand {
  top: calc(24px + var(--safe-top));
  left: calc(28px + var(--safe-left));
  right: auto;
  width: auto;
  min-height: 42px;
  padding: 0 16px;
  transform: none;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  background: rgba(20, 28, 36, 0.78);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.22);
  color: var(--text);
  letter-spacing: 0;
  pointer-events: none;
  backdrop-filter: blur(10px);
}

.brand-light {
  width: 10px;
  height: 10px;
  border-radius: 999px;
  background: var(--accent);
  box-shadow: 0 0 18px rgba(20, 184, 166, 0.68);
}

.account-menu-toggle {
  position: fixed;
  z-index: 7;
  top: calc(22px + var(--safe-top));
  right: calc(28px + var(--safe-right));
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 46px;
  max-width: calc(100vw - 56px - var(--safe-left) - var(--safe-right));
  padding: 6px 12px 6px 8px;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  background: rgba(20, 28, 36, 0.82);
  color: var(--text);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.24);
  backdrop-filter: blur(10px);
  cursor: pointer;
}

.account-menu-toggle:hover {
  border-color: rgba(20, 184, 166, 0.32);
  background: rgba(30, 38, 51, 0.92);
  transform: translateY(-1px);
}

.hamburger-icon,
.hamburger-icon::before,
.hamburger-icon::after,
.hamburger-icon > span {
  display: block;
  width: 20px;
  height: 2px;
  border-radius: 999px;
  background: currentColor;
}

.hamburger-icon {
  position: relative;
  flex: 0 0 auto;
  background: transparent;
}

.hamburger-icon::before,
.hamburger-icon::after {
  content: "";
  position: absolute;
  left: 0;
}

.hamburger-icon::before {
  top: -6px;
}

.hamburger-icon::after {
  top: 6px;
}

.top-coin-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 32px;
  padding: 0 10px;
  border: 1px solid rgba(20, 184, 166, 0.28);
  border-radius: 999px;
  background: rgba(20, 184, 166, 0.1);
  color: var(--text);
  font-size: 13px;
  font-weight: 800;
  line-height: 1;
}

.top-coin-pill.is-owing {
  border-color: rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-dim);
}

/* Darker than the text, and the bright rim goes too. Measured on the app's twin: greying the coin
   to a mid-tone made the disc the heaviest element in the badge, so the eye landed on it rather
   than the number. A state that recedes has to recede everywhere. */
.top-coin-pill.is-owing .coin-dot {
  border-color: rgba(255, 255, 255, 0.14);
  background: linear-gradient(135deg, #4a4560, #232036);
  box-shadow: none;
}

.coin-dot {
  width: 13px;
  height: 13px;
  border: 2px solid rgba(255, 255, 255, 0.7);
  border-radius: 999px;
  background:
    radial-gradient(circle at 38% 30%, #fff7b0 0 22%, transparent 23%),
    linear-gradient(135deg, #fef08a, #f59e0b);
  box-shadow: 0 0 14px rgba(245, 158, 11, 0.34);
}

.player-avatar {
  position: relative;
  display: inline-block;
  flex: 0 0 auto;
  border-radius: 999px;
  background:
    radial-gradient(circle at 42% 34%, rgba(255, 255, 255, 0.88) 0 8%, transparent 9%),
    radial-gradient(circle at 60% 34%, rgba(255, 255, 255, 0.88) 0 8%, transparent 9%),
    linear-gradient(135deg, var(--accent), var(--blue));
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.2),
    0 0 20px rgba(20, 184, 166, 0.18);
}

.player-avatar::before,
.player-avatar::after {
  content: "";
  position: absolute;
  top: 36%;
  width: 4px;
  height: 4px;
  border-radius: 999px;
  background: #071017;
}

.player-avatar::before {
  left: 39%;
}

.player-avatar::after {
  right: 28%;
}

.mini-avatar {
  width: 34px;
  height: 34px;
}

.account-avatar {
  width: 52px;
  height: 52px;
}

.top-account-name {
  color: var(--text);
  font-size: 14px;
  font-weight: 750;
  white-space: nowrap;
}

.chevron-icon {
  width: 9px;
  height: 9px;
  border-right: 2px solid var(--text-dim);
  border-bottom: 2px solid var(--text-dim);
  transform: translateY(-2px) rotate(45deg);
}

.menu-shell {
  align-items: center;
  justify-items: center;
  min-height: 100vh;
  /* Named so .menu-panel's max-height follows the padding rather than guessing at it. This is the
     pair a desktop and a phone in landscape both get, and the one the cap was furthest out on:
     114px of padding against a cap that reserved 36. */
  --shell-top: 86px;
  --shell-bottom: 28px;
  padding:
    calc(var(--shell-top) + var(--safe-top))
    calc(28px + var(--safe-right))
    calc(var(--shell-bottom) + var(--safe-bottom))
    calc(28px + var(--safe-left));
}

.menu-panel,
.builder-panel,
.end-panel,
.pause-panel {
  border: 1px solid var(--panel-border);
  background: rgba(20, 28, 36, 0.82);
  box-shadow: var(--shadow-card);
  backdrop-filter: blur(12px);
}

.home-head {
  justify-content: center;
  text-align: center;
}

.home-head h1 {
  font-size: clamp(42px, 5vw, 56px);
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.52);
}

.launch-grid {
  display: grid;
  grid-template-columns: minmax(420px, 1.2fr) minmax(330px, 0.8fr);
  align-items: stretch;
  gap: 18px;
}

.preview-column {
  min-width: 0;
}

.play-column {
  display: grid;
  align-content: start;
  gap: 14px;
  min-height: 276px;
  padding: 18px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  background: var(--panel-strong);
  box-shadow: var(--shadow-card);
}

.compact-row {
  display: grid;
  grid-template-columns: 1fr minmax(120px, 150px);
  gap: 12px;
}

.segmented,
.auth-mode-switch,
.source-row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 4px;
  padding: 4px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: rgba(12, 18, 24, 0.55);
}

.segment,
.source-button {
  min-height: 44px;
  border: 1px solid transparent;
  border-radius: 10px;
  background: transparent;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 800;
  letter-spacing: 0;
  cursor: pointer;
}

.segment:hover,
.source-button:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.04);
}

.segment.is-active,
.source-button.is-active {
  border-color: rgba(20, 184, 166, 0.28);
  background: linear-gradient(180deg, var(--ui-accent-hover), var(--ui-accent));
  color: var(--text);
  box-shadow: 0 10px 24px rgba(20, 184, 166, 0.18);
}

.field {
  gap: 6px;
  min-width: 0;
}

.field span {
  color: var(--muted);
  font-size: 11px;
  font-weight: 850;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.field input,
.local-auth input {
  height: 48px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-button);
  background: rgba(12, 18, 24, 0.74);
  color: var(--text);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02);
}

.field input::placeholder,
.local-auth input::placeholder {
  color: rgba(167, 176, 187, 0.55);
}

.action-grid {
  display: grid;
  gap: 12px;
}

.play-button,
.plain-button,
.auth-toggle-button,
.auth-button {
  min-height: 60px;
  border-radius: var(--radius-button);
  font-size: 16px;
  font-weight: 850;
  letter-spacing: 0;
}

/* The one .auth-button on this drawer that is a link rather than a button: Download my data hands
   over to /privacy.html, which owns the form. A <button> centres its label and carries no
   underline for free; an <a> does neither, and it rendered as underlined text jammed into the top
   left of its own pill. Everything else about it is already right, so this is only what the tag
   costs. */
a.auth-button {
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

.play-button {
  border: 1px solid rgba(255, 255, 255, 0.06);
  background: linear-gradient(180deg, var(--ui-accent-hover), var(--ui-accent));
  color: var(--text);
  box-shadow: 0 16px 34px rgba(20, 184, 166, 0.22);
}

.play-button:hover {
  background: linear-gradient(180deg, #35e0cd, #16c6b3);
  transform: translateY(-1px);
}

.play-button.secondary {
  background: linear-gradient(180deg, #4a92ff, var(--blue));
  color: var(--text);
  box-shadow: 0 16px 34px rgba(56, 130, 246, 0.22);
}

.play-button.secondary:hover {
  background: linear-gradient(180deg, #5ca0ff, #428cff);
}

.plain-button {
  border: 1px solid var(--panel-border);
  background: rgba(12, 18, 24, 0.7);
  color: rgba(255, 255, 255, 0.84);
  box-shadow: none;
}

.plain-button:hover {
  border-color: rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.055);
  color: var(--text);
  transform: translateY(-1px);
}

#builder-open {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
}

#builder-open::before {
  content: "";
  width: 18px;
  height: 18px;
  background: currentColor;
  opacity: 0.8;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m14.6 4.6 4.8 4.8'/%3E%3Cpath d='M18.5 2.5a2.1 2.1 0 0 1 3 3L7 20l-4 1 1-4Z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m14.6 4.6 4.8 4.8'/%3E%3Cpath d='M18.5 2.5a2.1 2.1 0 0 1 3 3L7 20l-4 1 1-4Z'/%3E%3C/svg%3E") center / contain no-repeat;
}

.home-dock {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

.scoreboard-card {
  padding: 18px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  background: var(--panel-strong);
  box-shadow: var(--shadow-card);
}

.scoreboard-head {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 14px;
}

.trophy-icon {
  position: relative;
  width: 44px;
  height: 44px;
  border: 1px solid rgba(20, 184, 166, 0.14);
  border-radius: 13px;
  background: rgba(20, 184, 166, 0.1);
}

.trophy-icon::before {
  content: "";
  position: absolute;
  inset: 11px;
  background: var(--brand-gold);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9H4.5a2.5 2.5 0 0 1 0-5H6'/%3E%3Cpath d='M18 9h1.5a2.5 2.5 0 0 0 0-5H18'/%3E%3Cpath d='M4 22h16'/%3E%3Cpath d='M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22'/%3E%3Cpath d='M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22'/%3E%3Cpath d='M18 2H6v7a6 6 0 0 0 12 0Z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9H4.5a2.5 2.5 0 0 1 0-5H6'/%3E%3Cpath d='M18 9h1.5a2.5 2.5 0 0 0 0-5H18'/%3E%3Cpath d='M4 22h16'/%3E%3Cpath d='M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22'/%3E%3Cpath d='M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22'/%3E%3Cpath d='M18 2H6v7a6 6 0 0 0 12 0Z'/%3E%3C/svg%3E") center / contain no-repeat;
}

.scoreboard-head h2 {
  margin: 0;
  color: var(--text);
  font-size: 18px;
  font-weight: 850;
  letter-spacing: 0;
}

.scoreboard-subtitle {
  margin-top: 3px;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.35;
}

.scoreboard-list {
  margin-top: 14px;
}

.scoreboard-row,
.scoreboard-empty {
  border-color: var(--panel-border);
  background: rgba(12, 18, 24, 0.42);
  color: var(--text-dim);
}

.refresh-button {
  background: rgba(255, 255, 255, 0.04);
}

.app-menu {
  display: grid;
  justify-items: end;
  align-items: stretch;
  padding: 0;
  background: rgba(3, 7, 11, 0.58);
  opacity: 1;
  visibility: visible;
  backdrop-filter: blur(3px);
  transition:
    opacity 200ms ease-out,
    backdrop-filter 200ms ease-out;
}

.app-menu.is-hidden {
  display: grid !important;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
}

.app-menu-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
  width: min(416px, calc(100vw - 24px));
  height: calc(100vh - var(--safe-top) - var(--safe-bottom));
  min-height: 0;
  padding: calc(24px + var(--safe-top)) calc(24px + var(--safe-right)) calc(20px + var(--safe-bottom)) 24px;
  border: 1px solid var(--panel-border);
  border-right: 0;
  border-radius: var(--radius-lg) 0 0 var(--radius-lg);
  background:
    linear-gradient(180deg, rgba(20, 28, 36, 0.98), rgba(15, 22, 30, 0.98)),
    var(--panel);
  box-shadow: -26px 0 70px rgba(0, 0, 0, 0.42);
  overflow-y: auto;
  transform: translateX(0);
  transition: transform 210ms ease-out;
}

.app-menu.is-hidden .app-menu-panel {
  transform: translateX(24px);
}

.app-menu-head {
  flex: 0 0 auto;
}

.app-menu-head h2 {
  color: var(--text);
  font-size: 28px;
  font-weight: 850;
  letter-spacing: 0;
}

.panel-actions {
  gap: 8px;
}

.icon-button {
  border: 1px solid var(--panel-border);
  background-color: rgba(255, 255, 255, 0.045);
}

.icon-button:hover {
  border-color: rgba(255, 255, 255, 0.14);
  background-color: rgba(255, 255, 255, 0.075);
}

.drawer-section {
  display: grid;
  gap: 12px;
}

.control-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
  margin-top: 10px;
  cursor: pointer;
}

.control-row span {
  display: grid;
  gap: 3px;
  font-size: 14px;
  font-weight: 650;
}

.control-row small {
  color: var(--muted);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.35;
}

.control-row input[type="checkbox"] {
  flex: none;
  width: 20px;
  height: 20px;
  margin-top: 2px;
  accent-color: var(--accent);
}

/* A control-row whose control is too wide to sit beside its label, so it goes underneath it.
   The range reuses .volume-slider: one slider look in the drawer, not two. */
.slider-row {
  display: grid;
  gap: 8px;
  margin-top: 10px;
  cursor: pointer;
}

.slider-row span {
  display: grid;
  gap: 3px;
  font-size: 14px;
  font-weight: 650;
}

.slider-row small {
  color: var(--muted);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.35;
}

/* A subordinate row whose parent is off. Dimmed and inert rather than removed, so the
   relationship between the two is visible instead of the row simply vanishing. */
.control-row.is-disabled {
  opacity: 0.42;
  cursor: not-allowed;
}

.drawer-label {
  margin: 0;
  color: var(--muted);
  font-size: 12px;
  font-weight: 850;
  letter-spacing: 0.12em;
  line-height: 1;
  text-transform: uppercase;
}

.account-section {
  padding-bottom: 20px;
  border-bottom: 1px solid var(--panel-border);
}

.drawer-card,
.account-card {
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  background: var(--panel-strong);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.18);
}

.account-card {
  display: grid;
  gap: 16px;
  padding: 18px;
}

.account-main {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 13px;
}

.account-copy {
  display: grid;
  min-width: 0;
  gap: 4px;
}

#account-name {
  min-width: 0;
  overflow: hidden;
  color: var(--text);
  font-size: 16px;
  font-weight: 820;
  line-height: 1.2;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#account-sub {
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.35;
}

.coin-badge {
  min-width: 58px;
  padding: 8px 10px;
  border: 1px solid rgba(20, 184, 166, 0.18);
  border-radius: 12px;
  background: rgba(20, 184, 166, 0.08);
  color: var(--text);
  text-align: right;
}

.coin-badge span {
  color: var(--muted);
  font-size: 10px;
  font-weight: 850;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.coin-badge strong {
  display: block;
  margin-top: 2px;
  font-size: 18px;
  line-height: 1;
}

/* The pending-verification notice, which the browser did not have at all.
 
   The app's is the reference (SettingsSheet.razor.css) and this is the same shape in this page's
   tokens: an accent-tinted card inside the account card, because it is asking for something rather
   than reporting something. It sits above the sign-out toggle so it is read before the controls
   underneath it, not after them. */
.verify-notice {
  margin: 2px 0 4px;
  padding: 12px;
  border: 1px solid color-mix(in srgb, var(--ui-accent) 45%, transparent);
  border-radius: 12px;
  background: color-mix(in srgb, var(--ui-accent) 10%, transparent);
}

.verify-notice strong { display: block; color: var(--text); font-size: 13px; }

.verify-notice p {
  margin: 6px 0 10px;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.45;

  /* An email address has no spaces to break at, and a long one pushed the app's panel wider than
     the sheet before this. The drawer here is narrower still. */
  overflow-wrap: anywhere;
}

.verify-notice .auth-status { margin: 8px 0 0; }

.verify-actions {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 8px;
}

.verify-actions button {
  appearance: none;
  min-height: 42px;
  padding: 10px;
  border-radius: 10px;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}

.verify-actions button:disabled { opacity: .5; cursor: default; }

.verify-refresh {
  border: 1px solid var(--ui-accent);
  background: var(--ui-accent);
  color: var(--accent-ink);
}

.verify-resend {
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: var(--control-surface);
  color: var(--text);
}

.auth-toggle-button {
  width: 100%;
  min-height: 48px;
  border: 1px solid rgba(20, 184, 166, 0.46);
  background: rgba(20, 184, 166, 0.06);
  color: var(--ui-accent-hover);
  cursor: pointer;
}

.auth-toggle-button:hover {
  border-color: rgba(20, 184, 166, 0.7);
  background: rgba(20, 184, 166, 0.11);
  color: var(--text);
}

.account-auth-panel {
  display: grid;
  gap: 14px;
  padding: 15px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 14px;
  background: rgba(6, 11, 16, 0.46);
}

.account-auth-panel.is-hidden,
.local-auth.is-hidden,
.auth-providers.is-hidden,
.auth-actions.is-hidden,
.google-slot.is-hidden,
.auth-button.is-hidden {
  display: none !important;
}

.auth-panel-intro {
  display: grid;
  gap: 5px;
}

.auth-panel-intro h4,
.auth-panel-intro p {
  margin: 0;
}

.auth-panel-intro h4 {
  color: var(--text);
  font-size: 18px;
}

.auth-panel-intro p {
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.45;
}

.auth-mode-tabs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px;
  padding: 4px;
  border: 1px solid var(--panel-border);
  border-radius: 11px;
  background: rgba(0, 0, 0, 0.2);
}

.auth-mode-tabs button {
  min-height: 39px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 850;
  cursor: pointer;
}

.auth-mode-tabs button.is-active {
  background: rgba(20, 184, 166, 0.17);
  color: var(--text);
  box-shadow: inset 0 0 0 1px rgba(20, 184, 166, 0.26);
}

.auth-providers {
  display: grid;
  gap: 13px;
}

.local-auth {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
}

.auth-field {
  display: grid;
  gap: 6px;
  color: var(--text);
  font-size: 12px;
  font-weight: 800;
}

.auth-field input {
  width: 100%;
  padding: 0 13px;
}

.auth-field small {
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 600;
  line-height: 1.35;
}

.auth-submit-row {
  display: grid;
  gap: 10px;
}

.auth-mode-switch {
  min-width: 0;
}

.auth-button {
  width: 100%;
  min-height: 46px;
  border: 1px solid var(--panel-border);
  background: rgba(12, 18, 24, 0.72);
  color: var(--text);
  cursor: pointer;
}

.auth-button.primary {
  border-color: rgba(20, 184, 166, 0.3);
  background: linear-gradient(180deg, var(--accent-hover), var(--accent));
  color: var(--text);
}

.auth-actions {
  display: grid;
  gap: 10px;
}

/* Google's published skins, straight from their branding page — see --google-fill in app.js.
   The G is never recoloured and never replaced with a letter, which is what this used to be. */
.auth-button.google {
  border-color: var(--google-border, #8E918F);
  background: var(--google-fill, #131314);
  color: var(--google-ink, #E3E3E3);
  font-family: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.25px;
}

/* Both marks lead the label as one centred group, which is what Apple and Google each ask for
   and the only arrangement a long localised label cannot pull apart. */
.auth-button.apple,
.auth-button.google {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

.auth-button.apple {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
  font-weight: 500;
  letter-spacing: -0.2px;
}

.provider-mark {
  width: 18px;
  height: 18px;
  flex: none;
}

/* Apple draw their mark a shade taller than the cap height, and its visual centre sits below its
   bounding box. */
.auth-button.apple .provider-mark {
  width: 19px;
  height: 19px;
  margin-top: -2px;
}

/* Deleting an account is destructive and irreversible: quiet until it is asked for, and
   unmistakable once it is. */
.auth-button.danger {
  border-color: color-mix(in srgb, var(--danger, #F0475F) 45%, transparent);
  background: color-mix(in srgb, var(--danger, #F0475F) 10%, transparent);
  color: var(--danger, #F0475F);
}

.auth-button.danger-solid {
  border-color: var(--danger, #F0475F);
  background: var(--danger, #F0475F);
  color: #FFFFFF;
}

.danger-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.danger-actions.is-hidden {
  display: none;
}

.auth-button:disabled {
  cursor: not-allowed;
  opacity: 0.62;
}

.provider-status {
  margin: -4px 2px 1px;
  color: var(--text-dim);
  font-size: 11px;
  line-height: 1.4;
  text-align: center;
}

.provider-status.is-hidden {
  display: none;
}

.auth-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text-dim);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--panel-border);
}

.auth-link-button {
  justify-self: start;
  padding: 2px 0;
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font-size: 12px;
  font-weight: 750;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.auth-link-button:hover {
  color: var(--accent-hover);
}

.auth-link-button.is-hidden {
  display: none !important;
}

.auth-field.is-hidden {
  display: none !important;
}

.password-reset-panel {
  display: grid;
  gap: 11px;
  padding: 12px;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.16);
}

.password-reset-panel.is-hidden {
  display: none !important;
}

.password-reset-panel > p {
  margin: 0;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.4;
}

.password-reset-panel input {
  width: 100%;
  min-height: 42px;
  padding: 0 12px;
  border: 1px solid var(--panel-border);
  border-radius: 9px;
  background: rgba(7, 12, 17, 0.68);
  color: var(--text);
}

.auth-status {
  margin: 0;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.35;
}

.audio-settings {
  gap: 16px;
}

.audio-control {
  display: grid;
  gap: 10px;
}

.audio-control-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  color: var(--text);
  font-size: 15px;
  font-weight: 760;
}

.audio-mute-button {
  width: 36px;
  height: 36px;
  position: relative;
}

.audio-mute-button::before {
  content: "";
  width: 18px;
  height: 18px;
  background: var(--text);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11 5 6 9H2v6h4l5 4Z'/%3E%3Cpath d='M15.54 8.46a5 5 0 0 1 0 7.07'/%3E%3Cpath d='M19.07 4.93a10 10 0 0 1 0 14.14'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11 5 6 9H2v6h4l5 4Z'/%3E%3Cpath d='M15.54 8.46a5 5 0 0 1 0 7.07'/%3E%3Cpath d='M19.07 4.93a10 10 0 0 1 0 14.14'/%3E%3C/svg%3E") center / contain no-repeat;
}

.audio-mute-button.is-off::after {
  content: "";
  position: absolute;
  top: 10px;
  left: 17px;
  width: 2px;
  height: 18px;
  border-radius: 999px;
  background: #ff6b6b;
  transform: rotate(-40deg);
  box-shadow: 0 0 0 2px rgba(20, 28, 36, 0.82);
}

.visually-hidden {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

.volume-slider {
  --range-progress: 55%;
  width: 100%;
  height: 24px;
  margin: 0;
  padding: 0;
  accent-color: var(--accent);
  background: transparent;
  cursor: pointer;
  appearance: none;
}

.volume-slider::-webkit-slider-runnable-track {
  height: 8px;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  background:
    linear-gradient(90deg, var(--accent) 0 var(--range-progress), rgba(7, 12, 17, 0.9) var(--range-progress) 100%);
}

.volume-slider::-webkit-slider-thumb {
  width: 18px;
  height: 18px;
  margin-top: -6px;
  border: 2px solid var(--text);
  border-radius: 999px;
  background: var(--text);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
  appearance: none;
}

.volume-slider::-moz-range-track {
  height: 8px;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  background: rgba(7, 12, 17, 0.9);
}

.volume-slider::-moz-range-progress {
  height: 8px;
  border-radius: 999px;
  background: var(--accent);
}

.volume-slider::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border: 2px solid var(--text);
  border-radius: 999px;
  background: var(--text);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.32);
}

.volume-slider:hover::-webkit-slider-thumb,
.volume-slider:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 6px rgba(20, 184, 166, 0.12), 0 4px 14px rgba(0, 0, 0, 0.32);
}

.music-source-card {
  gap: 12px;
  padding: 18px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-lg);
  background: var(--panel-strong);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.16);
}

.source-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.info-icon {
  position: relative;
  width: 22px;
  height: 22px;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.035);
}

.info-icon::before {
  content: "i";
  position: absolute;
  inset: 0;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 850;
  line-height: 20px;
  text-align: center;
}

.settings-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: auto;
  padding-top: 2px;
  color: var(--muted);
}

.privacy-link {
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: 13px;
  cursor: pointer;
}

.privacy-link:hover {
  color: var(--text);
}

@media (max-width: 860px) {

  .launch-grid {
    grid-template-columns: 1fr;
  }

  .play-column {
    min-height: 0;
  }
}

@media (max-width: 560px) {
  .site-brand {
    top: calc(14px + var(--safe-top));
    left: calc(14px + var(--safe-left));
    min-height: 38px;
    padding: 0 12px;
    font-size: 14px;
  }

  .account-menu-toggle {
    top: calc(12px + var(--safe-top));
    right: calc(12px + var(--safe-right));
    gap: 7px;
    min-height: 40px;
    padding: 5px 9px 5px 6px;
  }

  .top-account-name {
    display: none;
  }

  .top-coin-pill {
    min-height: 29px;
    padding: 0 8px;
  }

  .mini-avatar {
    width: 30px;
    height: 30px;
  }

  .menu-shell {
    /* Named so .menu-panel's max-height follows the padding rather than guessing at it. */
    --shell-top: 70px;
    --shell-bottom: 12px;
    padding:
      calc(var(--shell-top) + var(--safe-top))
      calc(12px + var(--safe-right))
      calc(var(--shell-bottom) + var(--safe-bottom))
      calc(12px + var(--safe-left));
  }

  .home-head h1 {
    font-size: clamp(34px, 10vw, 42px);
  }

  .compact-row {
    grid-template-columns: 1fr;
  }

  .play-button,
  .plain-button {
    min-height: 56px;
  }

  .scoreboard-card,
  .play-column {
    padding: 15px;
  }

  .app-menu-panel {
    width: 100vw;
    height: 100vh;
    padding: calc(20px + var(--safe-top)) calc(18px + var(--safe-right)) calc(18px + var(--safe-bottom)) calc(18px + var(--safe-left));
    border-radius: 0;
    border-right: 0;
    border-left: 0;
  }

  .account-main {
    grid-template-columns: auto minmax(0, 1fr) auto;
  }

  .coin-badge {
    grid-column: auto;
    justify-self: end;
    text-align: right;
  }
}

@media (max-width: 360px) {
  .account-main {
    grid-template-columns: auto 1fr;
  }

  .coin-badge {
    grid-column: 1 / -1;
    justify-self: stretch;
    text-align: left;
  }
}

.lobby-preview-buffer {
  display: none !important;
}

.launch-grid {
  grid-template-columns: minmax(0, 1fr) minmax(310px, 330px);
  align-items: start;
}

.home-dock {
  width: min(660px, calc(100% - 360px));
  align-self: end;
}

.settings-footer-actions {
  display: flex;
  align-items: center;
  gap: 18px;
}

@media (max-width: 860px) {

  .launch-grid {
    grid-template-columns: 1fr;
  }

  .home-dock {
    width: 100%;
  }

  
}

/* Live lobby background */
.lobby-preview-buffer {
  display: none !important;
}

.launch-grid {
  grid-template-columns: minmax(300px, 360px);
  align-self: center;
  justify-self: end;
  width: min(360px, 100%);
}

.play-column {
  background: rgba(30, 38, 51, 0.82);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.home-dock {
  width: min(620px, 62%);
  align-self: end;
  justify-self: start;
}

.scoreboard-card {
  background: rgba(30, 38, 51, 0.66);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

@media (max-width: 860px) {

  .launch-grid {
    width: 100%;
    justify-self: stretch;
  }

  .home-dock {
    width: 100%;
  }

  
}

.home-head h1 {
  font-size: 56px;
}

@media (max-width: 860px) {

  .home-head h1 {
    font-size: 48px;
  }
}

@media (max-width: 560px) {
  

  .home-head h1 {
    font-size: 40px;
  }
}

@media (max-width: 360px) {
  .home-head h1 {
    font-size: 36px;
  }
}

/* Final web publish polish */
#game {
  touch-action: auto;
}

body.is-playing {
  overscroll-behavior: none;
}

body.is-playing #game {
  touch-action: none;
}

.cog-button {
  position: relative;
}

.cog-button::before {
  content: "";
  width: 19px;
  height: 19px;
  background: var(--text);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.51a2 2 0 0 1 1-1.72l.15-.1a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.51a2 2 0 0 1 1-1.72l.15-.1a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E") center / contain no-repeat;
}

.cog-button.is-active {
  border-color: rgba(20, 184, 166, 0.42);
  background: rgba(20, 184, 166, 0.12);
  box-shadow: 0 0 0 4px rgba(20, 184, 166, 0.08);
}

.cog-button.is-active::before {
  background: var(--ui-accent-hover);
}

.quick-settings-toggle {
  position: fixed;
  z-index: 7;
  right: calc(18px + var(--safe-right));
  bottom: calc(18px + var(--safe-bottom));
  width: 48px;
  height: 48px;
  border: 1px solid rgba(20, 184, 166, 0.34);
  border-radius: 50%;
  background: rgba(13, 21, 28, 0.88);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.32);
  backdrop-filter: blur(12px);
  cursor: pointer;
}

body.is-playing .quick-settings-toggle {
  display: none;
}

.drawer-setting-hint {
  margin: -2px 0 2px;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.4;
}

.drawer-theme-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.drawer-theme-option {
  min-width: 0;
  min-height: 54px;
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr);
  align-items: center;
  gap: 9px;
  padding: 7px 9px;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font-size: 12px;
  font-weight: 800;
  text-align: left;
  cursor: pointer;
}

.drawer-theme-option.is-active {
  border-color: rgba(20, 184, 166, 0.72);
  background: rgba(20, 184, 166, 0.12);
  box-shadow: inset 0 0 0 1px rgba(20, 184, 166, 0.18);
}

.drawer-theme-swatch {
  width: 34px;
  height: 34px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 9px;
}

.drawer-settings-pane {
  animation: drawerPaneIn 180ms ease-out;
}

.drawer-settings-pane.is-hidden {
  display: none !important;
}

@keyframes drawerPaneIn {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

a.plain-button,
a.privacy-link {
  text-decoration: none;
}

.how-to-play-button::before {
  content: "?";
  display: inline-grid;
  place-items: center;
  width: 22px;
  height: 22px;
  margin-right: 8px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 999px;
  color: var(--ui-accent-hover);
  font-size: 13px;
  font-weight: 900;
}

.home-dock {
  width: min(620px, 56%);
  margin-top: auto;
}

.settings-footer {
  gap: 14px;
}

.settings-footer-actions {
  display: flex;
  align-items: center;
  gap: 16px;
}

@media (max-width: 860px) {

  .home-dock {
    width: 100%;
  }
}

@media (max-width: 560px) {

  .action-grid {
    gap: 10px;
  }
}

@media (max-width: 560px) {
  .menu-shell {
    width: 100vw;
    justify-items: start;
    /* Named so .menu-panel's max-height follows the padding rather than guessing at it. This is
       the pair a phone actually gets, and the one measured overflowing at 320x568. */
    --shell-top: 72px;
    --shell-bottom: 16px;
    padding:
      calc(var(--shell-top) + var(--safe-top))
      calc(12px + var(--safe-right))
      calc(var(--shell-bottom) + var(--safe-bottom))
      calc(12px + var(--safe-left));
    overflow-x: hidden;
  }

  .home-head {
    min-width: 0;
    height: auto;
    justify-content: center;
  }

  .home-head h1 {
    max-width: 100%;
    overflow: hidden;
    font-size: clamp(30px, 9vw, 36px);
    line-height: 1.04;
    text-overflow: clip;
    white-space: nowrap;
  }

  .home-dock,
  .scoreboard-card,
  .compact-row,
  .action-grid,
  .match-field,
  .field input {
    width: min(100%, 330px);
    min-width: 0;
    max-width: 330px;
    justify-self: center;
  }

  .compact-row {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Deliberately no `left` and no `right: auto` here.

     This rule used to pin the profile button with `left: min(100vw - 64px, 326px)`, from the
     layout where the cog was a floating action button in the BOTTOM-right corner. The lobby
     rewrite moved the cog to the top bar at `right: 14px; width: 50px` — whose left edge is
     `100vw - 64px`, exactly the coordinate this rule was aiming at. And because `left` plus an
     explicit `width` over-constrains a fixed box, CSS discarded the `right: 70px` the mobile
     block below sets, so the button had no way to get back out from under it.

     Measured before the fix: at 320/360/375/390 the overlap was the button's whole 44px and
     `elementFromPoint` at its centre returned the cog, so the profile button was invisible AND
     untappable on every phone; 20px of overlap at 414, 4px at 430; and from 480 to 560 it was
     stranded at a fixed 326px with a 126px hole between it and the cog. Letting `right` govern
     gives the same 6px gap the layout already has above 560px. */
  .account-menu-toggle {
    width: 44px;
    height: 44px;
    min-height: 44px;
    justify-content: center;
    padding: 0;
    overflow: hidden;
  }

  .account-menu-toggle .chevron-icon,
  .top-account-name {
    display: none;
  }

  .account-menu-toggle .top-coin-pill {
    display: none !important;
  }
}

/* Off at the owner's request. The markup stays in index.html so this is one line to undo, and
   because the poke interaction and its animations are worth keeping around rather than deleting.
   `display: none` also stops the crown float and the blink from running, which is a small win for
   the idle scene's cost. */
.lobby-mascot {
  display: none;

  width: min(330px, 86%);
  margin: -104px auto -6px -22px;
  pointer-events: auto;
  cursor: pointer;
  touch-action: manipulation;
}

.lobby-mascot svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  filter:
    drop-shadow(0 0 22px rgba(173, 69, 242, 0.4))
    drop-shadow(0 18px 20px rgba(0, 0, 0, 0.45));
}

.mascot-crown {
  transform-origin: 243px 60px;
  animation: mascotCrownFloat 2.8s ease-in-out infinite;
}

.mascot-eyes {
  transform-box: fill-box;
  transform-origin: center;
  animation: mascotBlink 6.2s ease-in-out infinite;
  animation-delay: -1.4s;
}

.mascot-eye { cursor: pointer; }

.mascot-pupil {
  /* SVG translate transforms use a different coordinate space across browsers;
     keep pupils in their authored eye coordinates so they cannot float away. */
  transform: none !important;
  transition: none;
}

.lobby-mascot.is-poked .mascot-eyes {
  animation: mascotPokeBlink 440ms ease-in-out both;
}

.lobby-mascot.is-poked[data-poke-side="left"] .mascot-pupil {
  transform: none !important;
}

.lobby-mascot.is-poked[data-poke-side="right"] .mascot-pupil {
  transform: none !important;
}

@keyframes mascotBlink {
  0%, 42%, 47%, 100% { transform: scaleY(1); }
  44%, 45% { transform: scaleY(0.08); }
}

@keyframes mascotPokeBlink {
  0%, 100% { transform: scaleY(1); }
  28%, 48% { transform: scaleY(0.08); }
}

@keyframes mascotCrownFloat {
  0%, 100% { transform: translateY(0) rotate(-5deg); }
  50% { transform: translateY(-5px) rotate(2deg); }
}

/* The crown on the wordmark gets the same lift, so the two read as one idea. */
.brand-crown {
  animation: brandCrownFloat 3s ease-in-out infinite;
  transform-origin: 50% 80%;
  /* Float above the first letter instead of sitting on top of the wordmark. */
  top: -8px;
  left: 12px;
  transform: rotate(-10deg);
}

@keyframes brandCrownFloat {
  0%, 100% { transform: translateY(0) rotate(-8deg); }
  50% { transform: translateY(-4px) rotate(-2deg); }
}

/* --- rounds --- */

.rounds-strip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  margin-top: 14px;
  padding: 12px 14px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 13px;
  background: rgba(5, 9, 14, 0.34);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 750;
  text-align: left;
}

.rounds-strip:hover { border-color: rgba(59, 245, 200, 0.34); color: var(--text); }
.rounds-strip-label { display: inline-flex; align-items: center; gap: 9px; }

.rounds-pip {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--brand-green);
  box-shadow: 0 0 10px rgba(99, 255, 72, .85);
}

.rounds-strip[data-quiet="true"] .rounds-pip {
  background: rgba(148, 160, 184, 0.5);
  box-shadow: none;
}

.rounds-strip-chevron { font-size: 12px; color: rgba(148, 160, 184, 0.8); }
.rounds-strip-chevron::after { content: " \25BE"; }
.rounds-strip[aria-expanded="true"] .rounds-strip-chevron::after { content: " \25B4"; }

.rounds-drawer {
  margin-top: 12px;
  padding-top: 4px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.rounds-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 2px 9px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.rounds-title,
.rounds-server {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
  color: rgba(148, 160, 184, 0.8);
}

.rounds-title { letter-spacing: 0.15em; text-transform: uppercase; }
.rounds-server { font-variant-numeric: tabular-nums; }
.rounds-server b { color: var(--accent); font-weight: 700; }

.rounds-list {
  display: block;
  max-height: min(52dvh, 360px);
  margin: 0;
  padding: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  list-style: none;
}

.round-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 11px;
  padding: 12px 2px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.055);
}

.round-row:last-child { border-bottom: 0; }

.round-id {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  font-weight: 600;
}

.round-meta { min-width: 0; }

.round-line {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 7px;
}

.round-tag {
  padding: 2.5px 6px;
  border-radius: 5px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 9.5px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}

.round-tag.classic { color: var(--accent); background: rgba(59, 245, 200, 0.12); }
.round-tag.royale { color: #c069ff; background: rgba(173, 69, 242, 0.15); }

.round-state {
  font-size: 11.5px;
  color: rgba(148, 160, 184, 0.85);
  font-variant-numeric: tabular-nums;
}

.round-state.is-starting { color: var(--gold); }

.round-bar {
  position: relative;
  height: 5px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.1);
  overflow: hidden;
}

.round-bar i {
  position: absolute;
  inset: 0 auto 0 0;
  display: block;
  border-radius: 3px;
  background: linear-gradient(90deg, #12a596, var(--accent));
}

.round-row.royale .round-bar i { background: linear-gradient(90deg, #7a2de2, #c069ff); }
.round-row.is-shut .round-bar i { background: rgba(148, 160, 184, 0.4); }
.round-row.is-shut .round-id { color: var(--text-dim); }

.round-right { display: flex; align-items: center; gap: 11px; }

.round-count {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.round-count b { color: var(--text); font-weight: 700; }

.round-join {
  padding: 8px 15px;
  border: 1px solid rgba(59, 245, 200, 0.34);
  border-radius: 10px;
  background: rgba(59, 245, 200, 0.1);
  color: var(--accent);
  font-size: 12px;
  font-weight: 800;
}

.round-join:hover { background: rgba(59, 245, 200, 0.2); }
.round-row.royale .round-join {
  border-color: rgba(173, 69, 242, 0.45);
  background: rgba(173, 69, 242, 0.12);
  color: #c069ff;
}

.round-shut {
  padding: 8px 2px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(148, 160, 184, 0.75);
}

.rounds-empty { padding: 18px 2px; color: var(--text-dim); font-size: 13px; }
.rounds-empty b { display: block; margin-bottom: 4px; color: var(--text); font-size: 14px; }

.rounds-code { display: flex; gap: 8px; margin-top: 13px; }

.rounds-code input {
  flex: 1;
  min-width: 0;
  padding: 11px 13px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 11px;
  background: rgba(5, 9, 14, 0.7);
  color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  letter-spacing: 0.08em;
}

.rounds-code input::placeholder { color: rgba(148, 160, 184, 0.6); letter-spacing: 0.04em; }
.rounds-code input:focus { outline: 0; border-color: rgba(59, 245, 200, 0.34); }

.rounds-code button {
  padding: 0 17px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 11px;
  background: rgba(255, 255, 255, 0.07);
  color: var(--text-dim);
  font-size: 12.5px;
  font-weight: 800;
}

.rounds-code button:hover { color: var(--text); border-color: rgba(59, 245, 200, 0.34); }

/* --- chips --- */

.lobby-chips {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin-top: 16px;
}

.lobby-chips .chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 12px 6px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 13px;
  background: rgba(5, 9, 14, 0.34);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--text-dim);
  font-size: 11.5px;
  font-weight: 750;
}

.lobby-chips .chip:hover { border-color: rgba(59, 245, 200, 0.34); color: var(--text); }

.lobby-chips .chip svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
}

@media (max-width: 560px) {
  .lobby-mascot { width: min(280px, 82%); margin: -84px auto -4px -16px; }
}

@media (prefers-reduced-motion: reduce) {
  .end-panel.is-death,
  .end-panel.is-death h2,
  .end-panel.is-win,
  .end-panel.is-win h2,
  .end-panel.is-win .end-trophy,
  .end-panel.is-runner-up,
  .end-panel.is-runner-up h2,
  .end-panel.is-runner-up .end-trophy,
  .end-panel.is-placed,
  .end-panel.is-placed h2,
  .mascot-crown,
  .brand-crown,
  .mascot-eyes,
  .mascot-pupil { animation: none; transition: none; }

  /* Not merely stilled. A screenful of confetti held motionless mid-fall is a screenful of
     coloured rectangles over the score, which is worse than either the animation or nothing. The
     app's results page makes the same call for the same reason. */
  .win-confetti { display: none; }
}

/* The chip carries its own icon, so the old button's masked pencil would be a
   second one stacked above the label. */
#builder-open::before{
  content: none !important;
}

#builder-open {
  display: flex !important;
  flex-direction: column !important;
  gap: 6px !important;
}

/* ==========================================================================
   THE LOBBY
   One panel, one primary action, the live rounds behind a strip, and the
   crowned snake behind the glass. It owns every class it uses — the old
   .home-panel rules were removed rather than overridden, because eleven
   generations of them were fighting each other over the same four properties
   and the result was a near-miss of the design in every browser width.
   ========================================================================== */

.lobby {
  position: relative;
  z-index: 1;
  display: block;
  width: min(400px, calc(100vw - 28px - var(--safe-left) - var(--safe-right)));
  padding: 0;
  border: 0;
  background: none;
  pointer-events: auto;
}

/* The glass. A separate layer so the snake can sit between it and the arena. */
.lobby::before {
  content: "";
  position: absolute;
  z-index: 1;
  inset: 0;
  border: 1px solid rgba(59, 245, 200, 0.16);
  border-radius: 22px;
  background:
    linear-gradient(180deg, rgba(17, 29, 40, 0.94), rgba(9, 15, 22, 0.96)),
    radial-gradient(circle at 50% 0%, rgba(20, 184, 166, 0.1), transparent 62%);
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  transition: border-color 240ms ease-out, background 240ms ease-out, box-shadow 240ms ease-out;
}

.lobby.map-royale::before {
  border-color: rgba(173, 69, 242, 0.3);
  background:
    linear-gradient(180deg, rgba(26, 20, 34, 0.94), rgba(15, 12, 22, 0.96)),
    radial-gradient(circle at 50% 0%, rgba(173, 69, 242, 0.16), transparent 62%);
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.5), 0 0 46px rgba(173, 69, 242, 0.08);
}

.lobby-preview-buffer { display: none; }

/* --- the snake, behind the glass ---------------------------------------- */

.lobby-mascot {
  position: absolute;
  z-index: 0;
  left: -212px;
  bottom: calc(100% - 206px);
  width: 430px;
  pointer-events: none;
}

.lobby-mascot svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  filter:
    drop-shadow(0 0 26px rgba(173, 69, 242, 0.45))
    drop-shadow(0 18px 22px rgba(0, 0, 0, 0.5));
}

.mascot-crown {
  transform-origin: 243px 60px;
  animation: mascotCrownFloat 2.8s ease-in-out infinite;
}

@keyframes mascotCrownFloat {
  0%, 100% { transform: translateY(0) rotate(-5deg); }
  50% { transform: translateY(-5px) rotate(2deg); }
}

.brand-crown {
  animation: brandCrownFloat 3s ease-in-out infinite;
  transform-origin: 50% 80%;
  top: -8px;
  left: 12px;
  transform: rotate(-10deg);
}

@keyframes brandCrownFloat {
  0%, 100% { transform: translateY(0) rotate(-8deg); }
  50% { transform: translateY(-4px) rotate(-2deg); }
}

/* --- controls, above the glass ------------------------------------------ */

.lobby-body {
  position: relative;
  z-index: 2;
  padding: 20px;
}

.lobby-controls { display: block; }

.lobby-modes {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  padding: 4px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 15px;
  background: rgba(5, 9, 14, 0.30);
  backdrop-filter: blur(26px) saturate(1.4);
  -webkit-backdrop-filter: blur(26px) saturate(1.4);
}

.lobby-mode {
  padding: 12px 8px;
  border: 0;
  border-radius: 11px;
  background: transparent;
  color: var(--text-dim);
  font-size: 14.5px;
  font-weight: 800;
  line-height: 1.25;
  transition: background-color 200ms ease-out, box-shadow 200ms ease-out, color 200ms ease-out;
}

.lobby-mode span {
  display: block;
  margin-top: 3px;
  font-size: 10.5px;
  font-weight: 600;
  color: rgba(148, 160, 184, 0.72);
}

.lobby-mode.is-active {
  background: rgba(255, 255, 255, 0.07);
  color: var(--lobby-text);
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--mode-accent) 40%, transparent);
}

/* The subtitle carries the card's OWN accent — green on Classic, purple on Royale — which is what
   makes the two cards read as two destinations. It was var(--accent), the arena's. */
.lobby-mode.is-active span { color: var(--mode-accent); }

.lobby-play {
  width: 100%;
  min-height: 62px;
  margin-top: 14px;
  border: 0;
  border-radius: 15px;
  background: linear-gradient(180deg, #2fe3d0, #12a596);
  color: #04222a;
  font-size: 19px;
  font-weight: 900;
  letter-spacing: -0.01em;
  box-shadow: 0 12px 28px rgba(23, 208, 191, 0.22), inset 0 1px rgba(255, 255, 255, 0.3);
  transition: transform 140ms ease, filter 140ms ease, background 240ms ease-out;
}

.lobby.map-royale .lobby-play {
  background: linear-gradient(180deg, #c069ff, #7a2de2);
  color: #f8f0ff;
  box-shadow: 0 12px 28px rgba(140, 45, 226, 0.28), inset 0 1px rgba(255, 255, 255, 0.22);
}

.lobby-play:hover { transform: translateY(-2px); filter: brightness(1.06); }

/* Solo is the secondary action, not a link. It is a <button> and always has been, but nothing in
   this stylesheet ever claimed the class, so it fell through to the bare button reset and rendered
   as plain text under a large green primary — which reads as a caption rather than something you
   can press, and gives a text-sized tap target where the button above it has a 62px one.

   Deliberately quieter than .lobby-play: an outline and a translucent ground rather than a second
   filled slab, so the hierarchy still says "online first". Matches .lobby-card .play-solo in the
   app, which had the same problem stated the other way round — there it was underlined on purpose. */
.lobby-solo {
  width: 100%;
  min-height: 46px;
  margin-top: 10px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 13px;
  background: color-mix(in srgb, var(--lobby-surface-1, rgba(5, 9, 14, 0.34)) 72%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--lobby-text, var(--text));
  font-size: 15px;
  font-weight: 750;
}

.lobby-solo:hover {
  border-color: rgba(59, 245, 200, 0.34);
  background: var(--lobby-surface-2, rgba(5, 9, 14, 0.5));
}

.lobby-solo:active { transform: scale(.975); filter: brightness(.9); transition-duration: 60ms; }

.lobby-chips {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin-top: 16px;
}

.lobby-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 12px 6px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 13px;
  background: rgba(5, 9, 14, 0.34);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--text-dim);
  font-size: 11.5px;
  font-weight: 750;
}

.lobby-chip::before { content: none; }
.lobby-chip:hover { border-color: rgba(59, 245, 200, 0.34); color: var(--text); }

.lobby-chip svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
}

.lobby .status { margin: 10px 0 0; text-align: center; }
.lobby .status:empty { display: none; }

@media (max-width: 560px) {
  .lobby-mascot { left: -34px; bottom: calc(100% - 126px); width: 264px; }
  .lobby-body { padding: 17px; }
}

@media (prefers-reduced-motion: reduce) {
  .mascot-crown,
  .brand-crown,
  .mascot-eyes,
  .mascot-pupil { animation: none; transition: none; }
  .lobby-play { transition: none; }
}

/* --- phone lobby: mascot, wordmark ---------------------------------------
   These three selectors have picked up override layers over several passes, several of them
   !important, so this block deliberately sits last and owns the phone layout outright rather than
   adding a fourth set of exceptions somewhere in the middle of the cascade. Untangling the earlier
   layers would mean re-testing the desktop lobby too, which is not what is broken. */
@media (max-width: 560px) {
  /* Rising from behind the top edge of the panel, the way the desktop lobby does it: the mascot
     is part of the card rather than something floating in the arena behind it.
     What was wrong before was the PROPORTION, not the anchor — at 264px wide with its bottom edge
     126px below the panel top, only about 50px of a 177px mascot cleared the glass, so all you saw
     was a crown and two eyes. Smaller, and tucked in by far less, so the head and most of the body
     are above the edge and only the tail disappears behind it. */
  .lobby-mascot {
    left: -26px;
    right: auto;
    bottom: calc(100% - 92px);
    width: min(300px, 80%);
    margin: 0;
    transform: none;
  }

  /* Clear of the status bar and the notch, and far enough from the mascot that the crown on the
     wordmark and the crown on the mascot are not competing for the same corner. */
  .site-brand {
    top: calc(38px + var(--safe-top)) !important;
    left: calc(18px + var(--safe-left)) !important;
  }

  /* The crown's phone placement used to live here, as `top: -2px` over a `.brand-type` pushed down
     26px to make room for it. The lobby block at the end of this file then took `top` and `left`
     for itself — same specificity, later in the file, so its !important won — and left the 26px
     behind. The crown kept the top of a gap the wordmark no longer filled, and on a phone it
     floated 28.7px clear of the caps: not a lockup, a stray emoji in the corner. Measured at 320,
     375 and 390 wide; the desktop lockup, whose numbers were never split, sat 9.4px INTO the caps
     throughout.
     So the geometry is in one place now — see .brand-crown at the end of this file, where one
     number sizes the wordmark and the crown together. Nothing about the lockup is declared here. */
}

/* Thumbs, not cursors. The footer links were 15px against the 44px that a finger actually needs.
   The padding grows the hit area without changing how any of them look.

   The short axis is in the query as well as the long one, because a phone held sideways is still
   a phone and every one of these rules used to be keyed to max-width alone. Measured at 844x390 —
   an iPhone 14 in landscape — the drawer's footer links were back to 15.0px tall while the same
   finger got 44px in portrait. Nothing here changes how anything looks at any size, so a short
   desktop window picking the rules up costs nothing. */
@media (max-width: 560px), (max-height: 560px) {
  .privacy-link {
    padding: 14px 6px;
    margin: -14px -6px;
  }

  .icon-button {
    min-width: 40px;
    min-height: 40px;
  }

  /* Ten 32.0x32.0 dots, in a grid that had 9px of the row spare — the smallest controls in the
     builder and the ones you are meant to poke repeatedly. Six 44px columns fit the same 328px
     row, so the swatches grow into space that was already there. */
  .swatches {
    grid-template-columns: repeat(auto-fill, minmax(44px, 44px));
  }

  .swatch {
    width: 44px;
    height: 44px;
  }

  /* 52.1x32.0, in a list scrolled by the same thumb that has to hit it. */
  .round-join {
    min-height: 40px;
  }

  /* The pause panel's sliders are the browser's own, and measured 328.0x16.0 — full width, but a
     16px band to land a thumb on while a round is paused behind it. The drawer's styled sliders
     are 24px; match them rather than invent a third height. */
  .range-row input[type="range"] {
    height: 24px;
  }
}

/* --- coin store and bundle editor --------------------------------------- */
.coin-store { display: grid; gap: 12px; }
.coin-store-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(120px, 100%), 1fr));
  gap: 10px;
}

.coin-bundle {
  display: grid;
  gap: 3px;
  padding: 12px;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.coin-bundle:hover:not(:disabled) { border-color: var(--ui-accent); background: rgba(59, 245, 200, 0.08); }
.coin-bundle:disabled { opacity: 0.55; cursor: not-allowed; }
.coin-bundle strong { font-size: 15px; font-weight: 850; }
.coin-bundle span { color: var(--brand-gold); font-size: 13px; font-weight: 800; }
.coin-bundle small { color: var(--muted); font-size: 11px; }

/* Sits under the buy buttons, not beside them: it has to be read before a purchase, but it is not
   competing with the prices for attention. */
.coin-store-terms {
  margin: 4px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
}

.coin-store-terms a { color: var(--muted); text-decoration: underline; }
.coin-store-terms a:hover { color: var(--text); }

/* The revive button carries its own way out when the coins are not there, rather than standing
   next to a second button competing for the same decision. */
.revive-button { display: inline-flex; align-items: center; justify-content: center; gap: 10px; }

.revive-cta {
  padding: 3px 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.16);
  border: 1px solid rgba(255, 255, 255, 0.28);
  font-size: 12px;
  font-weight: 800;
  letter-spacing: 0.02em;
}

/* Short of coins: the button is still live, but it reads as the way to fix that rather than as
   the revive it cannot yet do. */
.revive-button.is-short { filter: saturate(0.75) brightness(0.94); }

.revive-note {
  margin: 0;
  font-size: 13px;
  color: rgba(255, 225, 228, 0.8);
  text-align: center;
}

.revive-button:disabled { opacity: 0.55; cursor: default; }

/* --- invoices, in the player's own drawer -------------------------------- */

/* Passkeys. The same card shape as an invoice row, because it is the same kind of thing: a short
   record with a date on it and an action beside it. Deliberately not a table — this list is read
   on a phone far more often than on a desktop. */
.passkey-list { display: grid; gap: 8px; }

.passkey-row {
  display: grid;
  gap: 4px;
  padding: 11px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
}

.passkey-row-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.passkey-row-head strong { font-size: 13.5px; font-weight: 800; }
.passkey-row-head time { color: var(--muted); font-size: 12px; }
.passkey-row-detail { color: var(--muted); font-size: 12px; }

/* A flag gets its own colour AND its own weight AND says what it means. State is never colour
   alone — neon is not something a colour-blind player can read. */
.passkey-row-flag {
  margin-top: 2px;
  font-size: 12px;
  font-weight: 800;
  color: var(--brand-gold, #ffc83d);
}

.passkey-row-actions { display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap; }

/* 44px targets, matching the rest of the account drawer. */
.passkey-row-actions .auth-button { min-height: 44px; padding-inline: 14px; }

.invoice-list { display: grid; gap: 8px; }

.invoice {
  display: grid;
  gap: 4px;
  padding: 11px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
}

.invoice-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.invoice-ref { font-size: 13px; font-weight: 800; letter-spacing: 0.04em; font-variant-numeric: tabular-nums; }
.invoice-date { color: var(--muted); font-size: 12px; }

.invoice-body { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px; }
.invoice-coins { color: var(--brand-gold); font-size: 13px; font-weight: 700; }
.invoice-price { margin-left: auto; font-size: 13px; font-variant-numeric: tabular-nums; }

/* A refund is the one thing on here somebody might be looking for specifically, so it says so
   in words rather than leaving them to compare two numbers. */
.invoice-refund {
  flex-basis: 100%;
  color: var(--gold);
  font-size: 11.5px;
  font-weight: 700;
}

.invoice-receipt {
  justify-self: start;
  margin-top: 2px;
  color: var(--accent);
  font-size: 12px;
  font-weight: 700;
  text-decoration: none;
}

.invoice-receipt:hover { text-decoration: underline; }

/* The arena theme also sits behind the lobby wordmark. Light themes need dark lettering and dark
   themes need light lettering; these variables are updated with the selected game theme. */
.brand-type strong {
  color: var(--brand-primary, var(--text));
  text-shadow: 0 1px 2px var(--brand-shadow, rgba(0, 0, 0, 0.75));
}

.brand-type strong + strong {
  color: var(--brand-secondary, var(--accent));
}

/* The wordmark needs one coherent contrast contract. Its surface now follows the selected arena:
   dark glass/light type on Midnight, pale glass/dark type on every pastel theme. */
.site-brand {
  padding: 7px 11px 7px 8px;
  border: 1px solid var(--brand-border, rgba(255, 255, 255, 0.1));
  border-radius: 13px;
  background: var(--brand-surface, rgba(20, 28, 36, 0.82));
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.16);
  backdrop-filter: blur(9px);
  -webkit-backdrop-filter: blur(9px);
}

.brand-crown { left: 20px; }

/* Background effects are a second setting, not extra colour themes. The miniature treatment is
   intentionally monochrome so the player can understand the shape before combining a palette. */
.background-effect-title { margin-top: 20px; }

.background-effect-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
}

.background-effect-option {
  display: grid;
  gap: 7px;
  min-width: 0;
  padding: 7px;
  border: 1px solid var(--panel-border);
  border-radius: 11px;
  background: rgba(255, 255, 255, 0.035);
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 750;
  text-align: left;
}

.background-effect-option.is-active {
  border-color: var(--accent);
  background: rgba(20, 184, 166, 0.11);
  color: var(--text);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 16%, transparent);
}

.background-effect-art {
  position: relative;
  display: block;
  height: 38px;
  overflow: hidden;
  border-radius: 7px;
  background-color: #111a22;
  opacity: 0.9;
}

.background-effect-art.is-clean {
  background: linear-gradient(145deg, #111a22, #1b2832);
}

.background-effect-art.is-orbits {
  background:
    radial-gradient(circle at 18% 70%, transparent 0 8px, rgba(45, 224, 207, 0.35) 9px 10px, transparent 11px),
    radial-gradient(circle at 74% 30%, transparent 0 13px, rgba(45, 224, 207, 0.28) 14px 15px, transparent 16px),
    #111a22;
}

.background-effect-art.is-waves {
  background:
    repeating-radial-gradient(ellipse at 0 50%, transparent 0 10px, rgba(45, 224, 207, 0.24) 11px 12px, transparent 13px 22px),
    #111a22;
}

.background-effect-art.is-hexes {
  background:
    linear-gradient(30deg, transparent 24%, rgba(45, 224, 207, 0.22) 25% 27%, transparent 28% 74%, rgba(45, 224, 207, 0.22) 75% 77%, transparent 78%),
    linear-gradient(150deg, transparent 24%, rgba(45, 224, 207, 0.22) 25% 27%, transparent 28% 74%, rgba(45, 224, 207, 0.22) 75% 77%, transparent 78%),
    #111a22;
  background-size: 26px 45px;
}

/* Right angles and a junction dot — the swatch says "board", which is what the floor draws. */
.background-effect-art.is-circuit {
  background:
    linear-gradient(to right, transparent 0 18%, rgba(45, 224, 207, 0.26) 18% 20%, transparent 20%),
    linear-gradient(to bottom, transparent 0 55%, rgba(45, 224, 207, 0.26) 55% 58%, transparent 58%),
    radial-gradient(circle at 19% 56%, rgba(45, 224, 207, 0.4) 0 2.5px, transparent 3px),
    #111a22;
  background-size: 26px 34px, 26px 34px, 26px 34px, auto;
}

.background-effect-art.is-drift {
  background:
    repeating-linear-gradient(24deg, transparent 0 9px,
      rgba(45, 224, 207, 0.22) 9px 10px, transparent 10px 19px),
    #111a22;
}

/* --- coin statement ------------------------------------------------------ */
.coin-statement-list { display: grid; gap: 8px; }

.coin-statement-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 5px 12px;
  padding: 11px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
}

.coin-statement-head { display: grid; gap: 2px; min-width: 0; }
.coin-statement-head strong { font-size: 13px; }
.coin-statement-head time { color: var(--muted); font-size: 11.5px; }
.coin-statement-delta { align-self: center; font-size: 13px; font-variant-numeric: tabular-nums; }
.coin-statement-delta.is-credit { color: var(--brand-gold); }
.coin-statement-delta.is-debit { color: #ff9aa9; }

.coin-statement-details {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 3px;
}

.coin-statement-details span {
  padding: 3px 7px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.055);
  color: var(--text-dim);
  font-size: 10.5px;
}

.coin-statement-details .is-note {
  flex-basis: 100%;
  padding-inline: 0;
  background: none;
  border-radius: 0;
  line-height: 1.4;
}

/* ---------- final lobby theme and scrolling ownership ----------
   Keep these rules last: this stylesheet contains older lobby iterations above, and these are
   the current dimensions/scroll contract for both desktop and touch layouts. */
.lobby {
  width: min(520px, calc(100vw - 28px - var(--safe-left) - var(--safe-right)));
}

.rounds-list {
  max-height: min(44dvh, 380px);
  overscroll-behavior-y: auto;
  touch-action: pan-y;
}

.menu-shell {
  overscroll-behavior-y: auto;
  scroll-padding-block: calc(76px + var(--safe-top)) calc(18px + var(--safe-bottom));
}

.rounds-code-block {
  margin-top: 14px;
  padding: 13px;
  border: 1px solid rgba(59, 245, 200, 0.2);
  border-radius: 13px;
  background: rgba(5, 9, 14, 0.5);
}

.rounds-code-block label {
  display: block;
  margin-bottom: 8px;
  color: var(--text);
  font-size: 12px;
  font-weight: 850;
  letter-spacing: 0.02em;
}

.rounds-code-block .rounds-code { margin-top: 0; }
.rounds-code-block small {
  display: block;
  margin-top: 7px;
  color: var(--text-dim);
  font-size: 10.5px;
  line-height: 1.35;
}

/* The canvas now uses the selected arena theme. Let the glass inherit enough of that colour to
   look intentional instead of putting the same near-black card over every light/pastel lobby. */
.lobby::before {
  border-color: color-mix(in srgb, var(--brand-secondary, var(--accent)) 28%, transparent);
}

@media (max-width: 560px) {
  .lobby { width: calc(100vw - 24px - var(--safe-left) - var(--safe-right)); }
  .rounds-list { max-height: min(42dvh, 330px); }
  .round-row { grid-template-columns: minmax(62px, auto) minmax(0, 1fr) auto; gap: 8px; }
  .round-right { gap: 7px; }
  .round-join { padding-inline: 12px; }
  .background-effect-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---------- the coins dialogue ---------- */

.coins-dialog {
  width: min(460px, calc(100vw - 32px));
  /* A dialogue is not a page: it clips rather than scrolls unless told otherwise, and this one
     is opened from the death screen, which on a phone is landscape — about 390px of height for
     a heading, a balance, two explainers and a list of bundles. Without a ceiling and somewhere
     to scroll, the bundles simply were not reachable. */
  max-height: min(88dvh, 640px);
  padding: 22px;
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--panel-border);
  border-radius: 16px;
  background: var(--panel);
  color: var(--text);
}

.coins-dialog::backdrop { background: rgba(4, 8, 12, 0.66); }

/* The confirmation in front of fifty coins. Centred and small: this is a question with two
   answers, not a place to be. UnlockOffer.razor.css is the same sheet on the phone. */
.unlock-dialog {
  width: min(400px, calc(100vw - 32px));
  padding: 22px 20px 18px;
  border: 1px solid var(--panel-border);
  border-radius: 16px;
  /* --panel-strong and a blur, not --panel. The panel token is 84% opaque and the grid under it
     showed straight through the price: the tab pill sat behind the title and the chips behind the
     buttons. Legible in a screenshot and wrong for the one dialogue in the game that is about
     money — a question this specific should not have the answer to a different one showing
     through it. */
  background: var(--panel-strong);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
  color: var(--text);
  text-align: center;
}

.unlock-dialog::backdrop { background: rgba(4, 8, 12, 0.72); }

/* The same padlock the chip wears, so the sheet is visibly about the thing that was tapped. */
.unlock-mark { display: block; font-size: 30px; line-height: 1; margin-bottom: 10px; }

.unlock-title { margin: 0 0 10px; font-size: 20px; }

/* The three numbers. Loudest line after the title, because it is the one that has to be read
   before the button is pressed. */
.unlock-body { margin: 0 0 8px; font-size: 15px; line-height: 1.5; color: var(--text); }

.unlock-earn { margin: 0 0 4px; font-size: 13px; line-height: 1.5; color: var(--text-dim); }

.unlock-said { margin: 12px 0 0; font-size: 13.5px; font-weight: 760; color: var(--danger); }
.unlock-said:empty { margin: 0; }

.unlock-acts { display: flex; gap: 10px; margin-top: 18px; }

/* Both buttons share the row evenly rather than the primary one taking it. Declining is the more
   common answer and must not be the smaller target. */
.unlock-acts > button { flex: 1; min-width: 0; }
.coins-dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.coins-dialog-head h2 { margin: 0; font-size: 20px; }
.coins-dialog-balance { margin: 6px 0 16px; color: var(--text-dim); font-size: 14px; }

.coins-ways { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }

.coins-way {
  padding: 12px;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
}

.coins-way h3 { margin: 0 0 4px; font-size: 13px; letter-spacing: 0.02em; }
.coins-way p { margin: 0; font-size: 12.5px; color: var(--muted); line-height: 1.5; }

.coins-dialog-bundles { display: grid; gap: 8px; }

.coins-bundle {
  display: flex; align-items: center; justify-content: space-between; gap: 6px 12px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  background: var(--panel-strong);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.coins-bundle:hover { border-color: var(--ui-accent); background: rgba(20, 184, 166, 0.12); }
.coins-bundle-amount { font-weight: 800; font-size: 15px; }
.coins-bundle-price { font-variant-numeric: tabular-nums; color: var(--text-dim); }

@media (max-width: 460px) {
  .coins-ways { grid-template-columns: 1fr; }
}

/* Landscape phone — how the game is actually held, and the shape the death screen appears in.
   Everything tightens so the bundles are on screen rather than one scroll away. */
@media (max-height: 560px) {
  .coins-dialog { max-height: 92dvh; padding: 14px 16px; }
  .coins-dialog-head h2 { font-size: 17px; }
  .coins-dialog-balance { margin: 4px 0 10px; font-size: 13px; }
  .coins-ways { grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
  .coins-way { padding: 9px 10px; }
  .coins-way h3 { font-size: 12px; }
  .coins-way p { font-size: 11.5px; line-height: 1.4; }
  .coins-bundle { padding: 9px 12px; }
}

/* Groups the browse list, so eight rounds read as two choices. */
.rounds-heading {
  margin: 10px 0 2px;
  padding: 0 2px;
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  list-style: none;
}

.rounds-heading:first-child { margin-top: 0; }

/* Theme-aware interface surfaces. The arena palette is a player setting, so every label and
   card above it must use the same contrast contract instead of keeping the old dark UI. */
.menu-panel,
.builder-panel,
.end-panel,
.pause-panel,
.play-column,
.scoreboard-card,
.app-menu-panel,
.inner-window,
.coins-dialog {
  border-color: var(--panel-border);
  background: var(--panel);
  color: var(--text);
}

html,
body {
  background: var(--bg);
}

.lobby::before {
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--panel-strong) 94%, transparent), var(--panel)),
    radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 62%);
}

.lobby.map-royale::before {
  background:
    linear-gradient(180deg, color-mix(in srgb, var(--panel-strong) 94%, transparent), var(--panel)),
    radial-gradient(circle at 50% 0%, rgba(173, 69, 242, 0.13), transparent 62%);
}

.lobby-modes,
.lobby-chip,
.round-row,
.rounds-code-block,
.rounds-code input,
.rounds-code button,
.field input,
.local-auth input,
.segmented,
.auth-mode-switch,
.source-row,
.builder-tabs,
.snake-chip,
.drawer-card,
.account-card,
.icon-button,
.option,
.background-effect-option,
.drawer-theme-option,
.profile-stat,
.profile-win,
.coin-statement-row,
.invoice,
.coins-bundle,
.coins-way {
  border-color: var(--panel-border);
  background: var(--control-surface);
  color: var(--text);
}

.lobby-mode.is-active,
.segment:hover,
.source-button:hover,
.option:hover,
.lobby-chip:hover,
.round-row:hover {
  background: var(--control-hover);
}

.lobby-mode span,
.lobby-chip,
.field span,
.option-label,
.face-name,
.round-meta,
.round-state,
.rounds-title,
.rounds-server,
.rounds-code-block small,
.drawer-setting-hint {
  color: var(--text-dim);
}

/* Split, because these three were never the same kind of thing.
   A selected OPTION is chrome and takes the brand. A selected background-effect or theme swatch is
   showing what that effect or theme looks like IN YOUR THEME, and painting it purple would
   misrepresent every one of them — that is the guide's one stated exception (§1) and it stays. */
.option.is-active {
  border-color: color-mix(in srgb, var(--ui-accent) 68%, transparent);
  background: color-mix(in srgb, var(--ui-accent) 13%, var(--control-surface));
  color: var(--ui-accent);
}

.background-effect-option.is-active,
.drawer-theme-option.is-active {
  border-color: color-mix(in srgb, var(--accent) 68%, transparent);
  background: color-mix(in srgb, var(--accent) 13%, var(--control-surface));
  color: var(--accent);
}

.swatch.is-active {
  border-color: var(--text);
  box-shadow:
    inset 0 0 0 2px rgba(0, 0, 0, 0.24),
    0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent);
}

/* Complete the theme contract for interactive chrome. Icons are masks filled with currentColor,
   so changing these foreground tokens updates the hamburger, close, sound and refresh artwork as
   well as their labels. Provider-branded buttons deliberately keep their provider colours. */
body {
  background:
    radial-gradient(circle at 18% 18%, color-mix(in srgb, var(--accent) 8%, transparent), transparent 28%),
    radial-gradient(circle at 78% 22%, color-mix(in srgb, var(--blue) 8%, transparent), transparent 30%),
    linear-gradient(180deg, var(--bg), var(--bg-end, var(--bg)));
}

button:focus-visible,
input:focus-visible,
summary:focus-visible {
  outline-color: var(--ui-accent);
}

.account-menu-toggle,
.quick-settings-toggle,
.rounds-strip,
.scoreboard-row,
.scoreboard-empty,
.auth-mode-tabs,
.account-auth-panel,
.password-reset-panel,
.password-reset-panel input,
.music-source-card,
.plain-button,
.profile-refresh {
  border-color: var(--panel-border);
  background: var(--control-surface);
  color: var(--text);
}

.super-boost-buy { display:grid; grid-template-columns:32px minmax(0,1fr) auto; align-items:center; gap:10px; width:100%; min-height:58px; margin-top:12px; padding:10px 12px; border:1px solid rgba(255,200,61,.42); border-radius:14px; background:rgba(255,200,61,.08); color:var(--text); text-align:left; cursor:pointer; }
.super-boost-buy > span:nth-child(2) { display:grid; gap:2px; }
.super-boost-buy small, .super-boost-note { color:var(--text-dim); }
.super-boost-buy b { color:#ffc83d; }
.super-boost-note { min-height:16px; margin:7px 2px 0; font-size:12px; }

.account-menu-toggle:hover,
.quick-settings-toggle:hover,
.rounds-strip:hover,
.plain-button:hover,
.profile-refresh:hover {
  border-color: color-mix(in srgb, var(--ui-accent) 44%, var(--panel-border));
  background: var(--control-hover);
  color: var(--text);
}

.icon-button,
.refresh-button,
.quick-settings-toggle,
.rounds-strip,
.profile-refresh {
  color: var(--text-dim);
}

/* Brand, not theme (brand guide §1). Every one of these is chrome: a primary button, a selected
   tab, a chosen source. The brand gradient is the app's, verbatim. */
.play-button,
.auth-button.primary,
.segment.is-active,
.source-button.is-active,
.builder-tab.is-active {
  border-color: color-mix(in srgb, var(--ui-accent) 58%, transparent);
  background: var(--brand-gradient);
  color: var(--ui-accent-ink);
  box-shadow: 0 12px 28px color-mix(in srgb, var(--ui-accent) 24%, transparent);
}

.play-button.secondary {
  background: linear-gradient(180deg, color-mix(in srgb, var(--blue) 82%, white), var(--blue));
  color: var(--secondary-ink);
  box-shadow: 0 12px 28px color-mix(in srgb, var(--blue) 22%, transparent);
}

.lobby:not(.map-royale) .lobby-play {
  background: linear-gradient(180deg, var(--accent-hover), var(--accent));
  color: var(--accent-ink);
  box-shadow: 0 12px 28px color-mix(in srgb, var(--accent) 24%, transparent);
}

/* The drawer's links and its secondary button. These read var(--accent), which is the whole
   reason the sign-in and register panel rendered green rather than purple. */
.auth-toggle-button,
.auth-link-button,
.invoice-receipt,
.privacy-link:hover {
  color: var(--ui-accent);
}

.auth-toggle-button {
  border-color: color-mix(in srgb, var(--ui-accent) 50%, transparent);
  background: color-mix(in srgb, var(--ui-accent) 8%, var(--control-surface));
}

/* ---------------------------------------------------------------------------------------------
   The sign-in / create-account switch.

   Reported as "the register bit doesn't look clear that it's creating an account vs logging in",
   and the panel earned it. The switch was two pill buttons the same size, shape, radius and weight
   as the four pill buttons stacked under it, and the selected one was marked with a faint tint and
   a 1px inset ring — so in register mode the panel showed the words "Create account" twice, on two
   things that looked identical, one of which switched the form and one of which submitted it.

   So the switch stops being buttons. An underlined strip shares no shape with anything else on the
   panel and cannot be pressed by mistake for the primary. The current mode is accent AND heavier
   AND underlined, which is the brand guide's own rule for a current item (§7) and the reason it is
   not signalled by colour alone — the 3px bar survives greyscale and a colour-blind reader.

   Targets stay at 44px tall (§7) even though the strip is visually lighter: the weight comes from
   fill and type, not from shrinking the thing you have to hit.
   --------------------------------------------------------------------------------------------- */
.auth-mode-tabs {
  gap: 0;
  margin: 0 0 2px;
  padding: 0;
  border: 0;
  border-bottom: 1px solid var(--panel-border);
  border-radius: 0;
  background: transparent;
}

.auth-mode-tabs button {
  position: relative;
  min-height: 44px;
  padding: 0 4px 9px;
  border-radius: 0;
  color: var(--text-dim);
  font-size: 14px;
  font-weight: 700;
}

/* Every button carries the bar; only the active one shows it. Toggling opacity rather than
   inserting the element is what lets it transition at all, and keeps the two labels from shifting
   by a pixel as the mode changes. */
.auth-mode-tabs button::after {
  content: "";
  position: absolute;
  right: 6px;
  bottom: -1px;
  left: 6px;
  height: 3px;
  border-radius: 999px 999px 0 0;
  background: var(--ui-accent);
  opacity: 0;
  transform: scaleX(0.55);
  transition: opacity 180ms ease-out, transform 180ms ease-out;
}

.auth-mode-tabs button.is-active {
  background: transparent;
  box-shadow: none;
  color: var(--text);
  font-weight: 850;
}

.auth-mode-tabs button.is-active::after {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  /* The movement goes, the feedback stays: the bar still appears, it just stops growing into
     place. A switch that says nothing is worse for this reader, not better. */
  .auth-mode-tabs button::after {
    transform: none;
    transition: opacity 180ms ease-out;
  }
}

/* Exactly one filled control on the panel, and it is the one that does the thing. 50px because the
   guide asks 48 for anything primary and this sits directly under a 44px field. */
.auth-button.primary {
  min-height: 50px;
  font-size: 16px;
  font-weight: 850;
}

/* The other ways in, demoted below the divider that names them. Google keeps its own mandated
   skin — never restyle that one. */
.auth-providers {
  margin-top: 2px;
}

.auth-actions .auth-button:not(.google) {
  font-size: 14px;
  font-weight: 700;
}

/* The providers were sized to their own text, not to the column: 221px buttons under a 281px
   field, so the panel had two right-hand edges. An `auto` track in a grid whose `justify-content`
   is `start` is packed at max-content rather than stretched — the `justify-content: start` this
   inherits was written for the flex layout this used to be. Naming the column restores the single
   edge that makes the panel read as one form. */
.auth-actions {
  grid-template-columns: minmax(0, 1fr);
  justify-content: stretch;
}

/* A passkey signs an existing account in; it cannot create one. Left visible under a divider
   reading "Or sign up with", it promised something it does not do. */
.account-auth-panel.is-register #passkey-signin {
  display: none !important;
}

.auth-terms-note {
  margin: 0;
  color: var(--text-dim);
  font-size: 11.5px;
  font-weight: 500;
  line-height: 1.45;
}

.auth-terms-note.is-hidden {
  display: none;
}

.auth-terms-note a {
  color: var(--ui-accent);
  text-decoration: underline;
}

.field input::placeholder,
.local-auth input::placeholder,
.rounds-code input::placeholder,
.password-reset-panel input::placeholder {
  color: var(--text-dim);
  opacity: 0.7;
}

.coin-badge,
.top-coin-pill,
.trophy-icon {
  border-color: color-mix(in srgb, var(--brand-gold) 28%, transparent);
  background: color-mix(in srgb, var(--brand-gold) 10%, var(--control-surface));
}

.rounds-drawer,
.rounds-head,
.round-row {
  border-color: var(--panel-border);
}

:root[data-theme-tone="light"] .site-brand,
:root[data-theme-tone="light"] .lobby-mascot {
  filter: drop-shadow(0 8px 20px rgba(42, 42, 53, 0.16));
}

.help-grid section,
.background-effect-art,
.scoreboard-row,
.scoreboard-empty {
  border-color: var(--panel-border);
  background-color: var(--control-surface);
}

.background-effect-art.is-clean {
  background: linear-gradient(145deg, var(--panel-strong), var(--control-surface));
}

.background-effect-art.is-orbits {
  background:
    radial-gradient(circle at 18% 70%, transparent 0 8px, color-mix(in srgb, var(--accent) 35%, transparent) 9px 10px, transparent 11px),
    radial-gradient(circle at 74% 30%, transparent 0 13px, color-mix(in srgb, var(--accent) 28%, transparent) 14px 15px, transparent 16px),
    var(--panel-strong);
}

.background-effect-art.is-waves {
  background:
    repeating-radial-gradient(ellipse at 0 50%, transparent 0 10px, color-mix(in srgb, var(--accent) 26%, transparent) 11px 12px, transparent 13px 22px),
    var(--panel-strong);
}

.background-effect-art.is-hexes {
  background:
    linear-gradient(30deg, transparent 24%, color-mix(in srgb, var(--accent) 24%, transparent) 25% 27%, transparent 28% 74%, color-mix(in srgb, var(--accent) 24%, transparent) 75% 77%, transparent 78%),
    linear-gradient(150deg, transparent 24%, color-mix(in srgb, var(--accent) 24%, transparent) 25% 27%, transparent 28% 74%, color-mix(in srgb, var(--accent) 24%, transparent) 75% 77%, transparent 78%),
    var(--panel-strong);
}

.rounds-strip-chevron,
.rounds-title,
.rounds-server,
.round-state,
.round-shut {
  color: var(--text-dim);
}

.round-bar,
.volume-slider::-moz-range-track {
  background: var(--control-hover);
}

.volume-slider::-webkit-slider-runnable-track {
  background: linear-gradient(
    90deg,
    var(--accent) 0 var(--range-progress),
    var(--control-surface) var(--range-progress) 100%);
}

/* The danger colour, on the two screens that mean danger. A placement is deliberately not one of
   them and is left to the neutral rule above — this block sits far below it in the file and would
   otherwise repaint "of 4 teams · Final length 240" in the red the whole tier exists to drop. */
.end-panel.is-death p,
.revive-note {
  color: var(--death-text, #fff1f2);
}

.coins-bundle:hover,
.cog-button.is-active {
  border-color: color-mix(in srgb, var(--ui-accent) 48%, transparent);
  background: color-mix(in srgb, var(--ui-accent) 13%, var(--control-surface));
}

.play-button:hover,
.auth-button.primary:hover {
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--accent-hover) 88%, white),
    var(--accent));
  color: var(--accent-ink);
}

.play-button.secondary:hover {
  background: linear-gradient(180deg, color-mix(in srgb, var(--blue) 72%, white), var(--blue));
  color: var(--secondary-ink);
}

.auth-toggle-button:hover {
  border-color: color-mix(in srgb, var(--ui-accent) 70%, transparent);
  background: color-mix(in srgb, var(--ui-accent) 13%, var(--control-surface));
  color: var(--text);
}

.round-tag.classic,
.round-join:not(.royale) {
  border-color: color-mix(in srgb, var(--accent) 36%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
}

/* The winners board with nothing on it. Without this the panel is a tall blank gap with
   pagination stranded at the bottom of it. */
.scoreboard-empty {
  margin: 28px 0;
  padding: 0 8px;
  text-align: center;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.5;
}


/* A name the server would refuse. Marked as it is typed rather than corrected under the cursor. */
#name.is-bad {
  border-color: var(--danger, #ff4d6d);
}

/* A signed-in player's name, which comes from their account and is not this device's to change.
   Read-only rather than disabled: a greyed-out box reads as broken, and this one is still
   selectable and copyable. The border goes and the cursor stops pretending, so it reads as a fact
   rather than as a field somebody has failed to fill in. */
#name.is-fixed {
  border-color: transparent;
  background: transparent;
  cursor: default;
  padding-left: 0;
}

.field-hint {
  display: block;
  margin-top: 5px;
  color: var(--text-dim);
  font-size: 11.5px;
  line-height: 1.4;
}

.field-hint.is-bad {
  color: var(--danger, #ff4d6d);
}

/* ---------- phone layout ----------------------------------------------------------------------
   Everything below came out of walking the DOM at 360, 390 and 430 wide (and the same three
   sideways), reporting every box whose right edge passed the viewport, every element whose
   scrollWidth beat its clientWidth, and every control the fixed chrome hit-tests in front of.
   Measured, not eyeballed: two pixels is invisible to the eye and unmistakable to
   getBoundingClientRect, and two pixels is exactly what the first of these was. */

/* The UA stylesheet puts `margin: 2px` on input[type=range], and margin sits OUTSIDE border-box —
   so a `width: 100%` slider overhangs its track by 2px on each side. That is the whole of the bug
   that let the iOS settings sheet be dragged sideways. `.volume-slider` already reset it, but the
   pause panel's plain sliders never did: they measured L=33..R=361 inside a `.range-row` running
   L=31..R=359, and the row reported scrollWidth 330 against clientWidth 328. Reset it once for
   every range in the client, so the next slider that gets added cannot bring it back. */
input[type="range"] {
  margin: 0;
}

/* When one overflow axis is anything but `visible`, the other computes from `visible` to `auto`.
   Every panel listed here sets only `overflow-y` (or `overflow: auto`), so each was already a
   horizontal scroll container that nobody asked for — `.app-menu-panel` measured overflow-x: auto
   without a single declaration saying so. Nothing overflows them today (scrollWidth == clientWidth
   at 360, 390 and 430), so this is a deliberate guard rather than a fix: sideways panning is never
   wanted in any of them, and one stray pixel is all it takes. `hidden` rather than `clip` because
   `auto` already clips — the only thing being taken away is the dragging. */
.menu-panel,
.inner-window,
.scoreboard-panel,
.app-menu-panel,
.coins-dialog,
.rounds-list {
  overflow-x: hidden;
}

/* And the reason a guard was worth having: a grid item's `min-width` defaults to `auto`, so it
   refuses to shrink below its own min-content and pushes the track out past the panel instead.
   The winners header measured a 280.9px min-content — a trophy, a heading and two 40px icon
   buttons — inside a 268px content box, so at 320px wide every child of the panel ran from L=27
   to R=307.9 against a content edge of 293.0, and the whole panel could be dragged 15px sideways.
   `minmax(0, 1fr)` lets the track be the width it was told to be; the headings inside already
   ellipsis rather than spill. */
.menu-panel {
  grid-template-columns: minmax(0, 1fr);
}

/* "Round Glasses" is the one accessory name that will not fit a 78px tile: 80px of text in a 66px
   content box, rendering as "Round Glass...". Every other name measured under budget, but the
   names are data in Core, so widening the tile to today's longest one only moves the problem.
   Let the label take a second line instead; the tile's 94px min-height and the row's stretch
   absorb it. */
.face-name {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  white-space: normal;
  text-align: center;
}

/* At 360px wide the five builder tabs get 54px of track each, and "Colour" needs 56px — the last
   letter was being shaved off by the ellipsis. The two pixels come out of the button's own UA side
   padding rather than out of the type or the 38px hit area, and the label is centred, so nothing
   moves at any wider size. */
.builder-tab {
  padding-inline: 4px;
}

/* On a phone the lobby panels run nearly edge to edge, so the two pieces of fixed chrome land ON
   them instead of beside them. Measured at 390x844: the wordmark painted over the whole of the
   builder's <h2>Snake</h2> — 84x28px of it — and over 101x36px of the Classic tab once the rounds
   drawer made the lobby tall enough to reach y=112. The cog is worse, because it takes the tap as
   well as the pixels: 35x48px of "Play Online" at 430x932, the whole of the winners board's
   "Next" at 360x740, and 35x32px of a round's "Join" at 390x844.

   The app already does exactly this while a game is running (`body.is-playing`); a panel open over
   the lobby is the same situation, and the drawer behind the hamburger is still the way to the
   settings the cog opens. Phones only — on a desktop the panel is centred and the chrome sits
   beside it, where it is wanted. */
@media (max-width: 560px) {
  body:has(.menu-panel:not(.is-hidden)) .site-brand,
  body:has(#rounds-drawer:not([hidden])) .site-brand {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }

  body:has(.menu-panel:not(.is-hidden)) .quick-settings-toggle,
  body:has(.inner-window:not(.is-hidden)) .quick-settings-toggle,
  body:has(#rounds-drawer:not([hidden])) .quick-settings-toggle {
    display: none;
  }
}

/* ---------- the colour painter ----------

   Sits under the single-colour swatches in the Colour tab. The iOS builder has the same control
   with the same gestures (ColourPainter.razor); a snake a player can paint in one client and not
   the other is the "the snake I designed is not the snake I got" failure this game keeps guarding
   against, so the two are built to look and behave alike rather than merely both existing. */

.paint-start {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--control-surface);
  color: var(--text);
  text-align: left;
}

.paint-start:hover { background: var(--control-hover); }

/* Three stripes rather than a word: the button has to look like the thing it makes. */
.paint-start-art {
  flex: 0 0 auto;
  width: 46px;
  height: 22px;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}

.paint-start-words { display: grid; gap: 2px; min-width: 0; }
.paint-start-title { font-weight: 800; font-size: 14px; }
.paint-start-sub { font-size: 11.5px; font-weight: 600; color: var(--text-dim); }

.paint-editor { display: grid; gap: 9px; }

.paint-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.paint-head h3 { margin: 0; }

/* Always on screen while a painting is: the way out of a tool has to be as easy to find as the
   way in, or single colour stops being the default in practice. */
.paint-plain {
  padding: 5px 11px;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  background: var(--control-surface);
  color: var(--text-dim);
  font-size: 11.5px;
  font-weight: 760;
}

.paint-plain:hover { color: var(--text); background: var(--control-hover); }

.paint-row { display: flex; gap: 8px; align-items: stretch; }

/* Butted together, so the cycle reads as one stretch of painted snake rather than as a row of
   separate answers. */
.paint-strip {
  flex: 1;
  display: flex;
  gap: 2px;
  min-width: 0;
  border-radius: 999px;
  overflow: hidden;
}

.paint-slot {
  flex: 1;
  min-width: 0;
  height: 44px;
  border: none;
  background: var(--swatch, #3bf5c8);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}

/* Drawn inside the slot rather than around it: the slots have no gaps, so an outer ring would
   sit on top of its neighbours. */
.paint-slot.is-active {
  box-shadow:
    inset 0 0 0 3px var(--text),
    inset 0 0 0 4px rgba(0, 0, 0, 0.35);
}

.paint-slot.is-add {
  flex: 0 0 44px;
  border-radius: 999px;
  background: var(--control-surface);
  box-shadow: inset 0 0 0 1px var(--panel-border);
  color: var(--text-dim);
  font-size: 21px;
  font-weight: 800;
  line-height: 1;
}

.paint-slot.is-add.is-active {
  box-shadow: inset 0 0 0 2px var(--ui-accent);
  color: var(--ui-accent);
}

/* A fixed number of BANDS rather than a fixed number of cycles, so every band is the same width
   whatever the cycle length and the last cycle may run out part-way — which is what a real snake
   does. The fade says "and on down the body" instead of ending at a hard edge. */
.paint-repeat {
  height: 14px;
  border-radius: 999px;
  background-repeat: repeat-x;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
  -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 72%, transparent 100%);
  mask-image: linear-gradient(90deg, #000 0%, #000 72%, transparent 100%);
}

/* Says out loud what the bar above it is. Without it the bar is only a second, longer version of
   the strip, and the thing it exists to show — that the cycle comes round again — is left to be
   inferred. */
.paint-repeat-note {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--text-dim);
}

.paint-acts { display: flex; gap: 8px; }

.paint-act {
  min-width: 44px;
  padding: 7px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  background: var(--control-surface);
  color: var(--text);
  font-size: 12.5px;
  font-weight: 760;
}

.paint-act:hover:not(:disabled) { background: var(--control-hover); }
.paint-act:disabled { opacity: 0.32; cursor: default; }

.paint-lift {
  margin-left: auto;
  border-color: color-mix(in srgb, var(--danger, #ff4d6d) 45%, transparent);
  background: color-mix(in srgb, var(--danger, #ff4d6d) 12%, transparent);
  color: var(--danger, #ff4d6d);
}

/* ---------- the lobby's legal footer -----------------------------------------------------------
   Terms, Privacy and Support existed only inside the settings drawer, where they inherit
   `visibility: hidden` while it is shut — so they had a layout box and were never painted, and
   nobody who did not open the drawer could find them. Apple and the payment terms both expect
   these reachable from the front page, and a player looking for support should not have to guess
   which panel hides it.

   Part of the lobby column rather than fixed to the viewport, deliberately: a recent audit found
   this client's fixed chrome winning elementsFromPoint over the buttons beneath it, and a footer
   pinned to the bottom of a phone screen is exactly how that happens again. */
.lobby-legal {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px 18px;
    margin-top: 18px;
    padding-top: 2px;
}

.lobby-independence {
    flex-basis: 100%;
    margin: 0 0 2px;
    color: var(--text-dim, #94a0b8);
    font-size: 11.5px;
    line-height: 1.45;
    text-align: center;
}

.lobby-legal a {
    /* 44px of hit area from padding rather than height, so the row stays visually light while a
       thumb still has something to land on. Vertical only — a negative margin does not undo a
       horizontal overhang, which is what let the legal pages be dragged sideways. */
    padding: 12px 0;
    font-size: 12.5px;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--text-dim, #94a0b8);
    text-decoration: none;
}

.lobby-legal a:hover,
.lobby-legal a:focus-visible { color: var(--text, #e8edf6); text-decoration: underline; }

/* --- Year in Review -------------------------------------------------------------------------
   The one screen in this game whose whole job is to be a pleasure, and the only one a player is
   likely to screenshot on purpose. Everything here is built by wwwroot/js/year-in-review.js, which
   is imported on demand — the round-up is an event an admin switches on, not a permanent fixture,
   so none of this markup exists in the page until somebody asks for it.

   Two rules shaped the whole block. The reveal is ONE timeline — the brand settles, the year
   lands, then the numbers arrive in order — because a handful of independent effects reads as a
   page that has not finished loading, while a sequence reads as something being presented to you.
   And nothing here is fixed chrome: the overlay is created on open and removed on close. The audit
   that preceded this work found fixed chrome over the lobby swallowing taps meant for the buttons
   underneath it, and a full-screen layer parked under a class is that bug waiting to come back. */

.yir-overlay {
  position: fixed;
  z-index: 12;                      /* above the drawer at 9, which is the highest thing here */
  inset: 0;
  overflow: hidden;
  background: color-mix(in srgb, var(--bg, #05070c) 88%, transparent);
  backdrop-filter: blur(14px) saturate(1.1);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
}

.yir-overlay[data-motion="playing"] {
  animation: yirOverlayIn 260ms ease-out both;
}

/* The colour behind everything, on its own layer so the compositor moves a transform and an
   opacity instead of repainting the panel sitting on top of it every frame. */
.yir-aurora {
  position: absolute;
  inset: -25%;
  pointer-events: none;
  background:
    radial-gradient(38% 34% at 22% 18%, color-mix(in srgb, var(--accent, #3bf5c8) 34%, transparent), transparent 70%),
    radial-gradient(42% 38% at 82% 74%, color-mix(in srgb, var(--gold, #ffd166) 26%, transparent), transparent 72%),
    radial-gradient(46% 40% at 62% 8%, color-mix(in srgb, var(--blue, #4da6ff) 20%, transparent), transparent 74%);
  opacity: 0.78;
  will-change: transform;
}

.yir-overlay[data-motion="playing"] .yir-aurora {
  animation: yirAurora 26s ease-in-out infinite alternate;
}

.yir-scroll {
  position: relative;
  display: grid;
  /* "safe" for the same reason .menu-shell uses it: a centred item taller than its container
     overflows both edges, and the half above the top cannot be scrolled back to. */
  align-items: safe center;
  justify-items: center;
  height: 100%;
  padding:
    calc(20px + var(--safe-top))
    calc(16px + var(--safe-right))
    calc(24px + var(--safe-bottom))
    calc(16px + var(--safe-left));
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.yir-card {
  position: relative;
  display: grid;
  gap: 16px;
  width: min(560px, 100%);
  padding: 24px;
  border: 1px solid var(--panel-border, rgba(255, 255, 255, 0.14));
  border-radius: 20px;
  background:
    radial-gradient(76% 44% at 8% 0, color-mix(in srgb, var(--accent, #3bf5c8) 13%, transparent), transparent 68%),
    radial-gradient(66% 40% at 100% 6%, color-mix(in srgb, var(--gold, #ffd166) 10%, transparent), transparent 70%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.06), transparent 34%),
    var(--panel-strong, rgba(9, 14, 23, 0.92));
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.1);
  overflow: hidden;
}

/* One diagonal sweep across the panel as it arrives, and then never again. A repeating sheen on a
   screen a player is reading is a distraction; a single one is the sense that something has just
   been handed to them. */
.yir-overlay[data-motion="playing"] .yir-card::after {
  content: "";
  position: absolute;
  inset: -40% -60%;
  pointer-events: none;
  background: linear-gradient(104deg, transparent 42%, rgba(255, 255, 255, 0.13) 50%, transparent 58%);
  animation: yirSheen 1500ms 380ms ease-out both;
}

.yir-brand {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* The padding is the crown's headroom, and it is load-bearing. In the page header the crown hangs
   off the top of a fixed bar that has the safe-area inset above it; here the wordmark is the first
   thing in the card, so a negative offset put the crown straight through the middle of SLITHER —
   which reads as a typo, not a brand. */
.yir-mark {
  position: relative;
  display: inline-flex;
  align-items: center;
  padding-top: 22px;
  padding-left: 2px;
}

/* Sized and angled the way .brand-crown is in the page header. */
.yir-crown {
  position: absolute;
  top: 0;
  left: 14px;
  width: 26px;
  height: 21px;
  object-fit: contain;
  filter: drop-shadow(0 0 5px rgba(248, 211, 106, 0.28));
  transform: rotate(-16deg);
}

.yir-wordmark {
  display: grid;
  line-height: 0.86;
  text-align: left;
}

/* The site wordmark, not a second one. Same family, same italic, same weight, same tracking as
   .brand-type strong — a retrospective that invented its own logotype would be the one screen in
   the game that looked like it came from somewhere else. */
.yir-wordmark strong {
  color: var(--text, #eef3ff);
  font-family: Poppins, Inter, sans-serif;
  font-size: 19px;
  font-style: italic;
  font-weight: 950;
  letter-spacing: 0.035em;
}

.yir-wordmark strong + strong {
  color: var(--accent, #3bf5c8);
  font-size: 17px;
}

.yir-year {
  margin: 0;
  font-family: Poppins, Inter, sans-serif;
  font-size: clamp(34px, 11vw, 46px);
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  line-height: 0.9;
  letter-spacing: -0.02em;
  background: linear-gradient(140deg, var(--accent, #3bf5c8), var(--gold, #ffd166));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* The player's other years. Quiet by design: this is a way back to 2025, not a navigation bar,
   and it only exists at all when the caller has offered to fetch another one. */
.yir-years {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: -4px;
}

.yir-year-chip {
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid var(--panel-border, rgba(255, 255, 255, 0.14));
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--muted, var(--text-dim, #94a0b8));
  font-size: 12.5px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

.yir-year-chip.is-current {
  border-color: color-mix(in srgb, var(--accent, #3bf5c8) 40%, transparent);
  background: color-mix(in srgb, var(--accent, #3bf5c8) 14%, transparent);
  color: var(--text, #eef3ff);
  cursor: default;
}

.yir-headline {
  margin: 0;
  color: var(--text, #eef3ff);
  font-size: clamp(26px, 7.4vw, 38px);
  font-weight: 900;
  line-height: 1.04;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.yir-subhead {
  margin: -6px 0 0;
  color: var(--muted, var(--text-dim, #94a0b8));
  font-size: 14.5px;
  line-height: 1.45;
}

.yir-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin: 2px 0 0;
  padding: 0;
  list-style: none;
}

/* The same card the profile drawer uses, so a number here and a number there are recognisably the
   same kind of thing. Left-aligned rather than centred: these carry a unit and often a caption. */
.yir-stat {
  display: grid;
  align-content: start;
  gap: 5px;
  min-width: 0;
  padding: 14px 14px 15px;
  border: 1px solid var(--panel-border, rgba(255, 255, 255, 0.14));
  border-radius: 14px;
  background:
    radial-gradient(circle at 50% 0, color-mix(in srgb, var(--accent, #3bf5c8) 11%, transparent), transparent 72%),
    rgba(255, 255, 255, 0.035);
}

.yir-stat.is-hero {
  grid-column: 1 / -1;
  padding: 18px 18px 20px;
  border-color: color-mix(in srgb, var(--accent, #3bf5c8) 30%, transparent);
  background:
    radial-gradient(84% 130% at 12% 0, color-mix(in srgb, var(--accent, #3bf5c8) 18%, transparent), transparent 70%),
    rgba(255, 255, 255, 0.045);
}

.yir-stat-label {
  color: var(--muted, var(--text-dim, #94a0b8));
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.yir-stat-figure {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 0;
  min-width: 0;
}

.yir-stat-value {
  color: var(--text, #eef3ff);
  font-family: Poppins, Inter, sans-serif;
  font-size: clamp(25px, 7.6vw, 32px);
  font-weight: 900;
  /* Tabular figures, or a number counting up jitters sideways on every frame. */
  font-variant-numeric: tabular-nums;
  line-height: 1;
  letter-spacing: -0.01em;
}

.yir-stat.is-hero .yir-stat-value {
  font-size: clamp(44px, 15vw, 62px);
  background: linear-gradient(140deg, var(--accent, #3bf5c8), var(--gold, #ffd166));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.yir-stat-unit {
  color: var(--muted, var(--text-dim, #94a0b8));
  font-size: 12px;
  font-weight: 750;
}

.yir-stat.is-hero .yir-stat-unit {
  font-size: 15px;
}

.yir-stat-caption {
  color: var(--muted, var(--text-dim, #94a0b8));
  font-size: 11.5px;
  line-height: 1.35;
}

.yir-empty {
  margin: 0;
  padding: 22px 16px;
  border: 1px dashed var(--panel-border, rgba(255, 255, 255, 0.14));
  border-radius: 14px;
  color: var(--muted, var(--text-dim, #94a0b8));
  font-size: 14px;
  line-height: 1.5;
  text-align: center;
}

.yir-footnote {
  margin: 0;
  color: var(--muted, var(--text-dim, #94a0b8));
  font-size: 11.5px;
  line-height: 1.45;
}

.yir-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
}

.yir-export {
  display: grid;
  gap: 10px;
  margin: 0;
  padding-top: 16px;
  border-top: 1px solid var(--panel-border, rgba(255, 255, 255, 0.14));
}

.yir-export.is-hidden {
  display: none !important;
}

/* The picture has to be saveable by hand, and `body` switches both of these off for the whole
   client so a drag on the arena never selects text or raises iOS's callout menu. Without the
   override, long-pressing the card does nothing at all — which would leave a download the page
   starts itself as the only way out, and that is exactly the one iOS Safari refuses. */
.yir-export-image {
  width: 100%;
  height: auto;
  border: 1px solid var(--panel-border, rgba(255, 255, 255, 0.14));
  border-radius: 14px;
  background: var(--bg, #05070c);
  -webkit-touch-callout: default;
  -webkit-user-select: auto;
  user-select: auto;
}

.yir-export-caption {
  color: var(--muted, var(--text-dim, #94a0b8));
  font-size: 12px;
  line-height: 1.45;
  text-align: center;
}

.yir-download {
  display: grid;
  place-items: center;
  min-height: 44px;
  padding: 0 14px;
  text-decoration: none;
}

/* The door in the profile drawer. Ships hidden; app.js reveals it when /review/year answers. */
#year-review-section .play-button {
  width: 100%;
}

/* The reveal. One timeline: brand, year, headline, then the cards in order, then the buttons. The
   stat delay is derived from --yir-index, which the module sets per card, so the pacing lives here
   rather than in script — and it is the same 90ms step the count-up uses. */
.yir-overlay[data-motion="playing"] .yir-brand,
.yir-overlay[data-motion="playing"] .yir-years,
.yir-overlay[data-motion="playing"] .yir-headline,
.yir-overlay[data-motion="playing"] .yir-subhead,
.yir-overlay[data-motion="playing"] .yir-empty,
.yir-overlay[data-motion="playing"] .yir-footnote,
.yir-overlay[data-motion="playing"] .yir-actions {
  animation: yirRise 560ms cubic-bezier(0.2, 0.9, 0.25, 1) both;
}

.yir-overlay[data-motion="playing"] .yir-brand { animation-delay: 60ms; }
.yir-overlay[data-motion="playing"] .yir-years { animation-delay: 150ms; }
.yir-overlay[data-motion="playing"] .yir-headline { animation-delay: 200ms; }
.yir-overlay[data-motion="playing"] .yir-subhead { animation-delay: 280ms; }
.yir-overlay[data-motion="playing"] .yir-empty { animation-delay: 420ms; }
.yir-overlay[data-motion="playing"] .yir-footnote { animation-delay: 900ms; }
.yir-overlay[data-motion="playing"] .yir-actions { animation-delay: 980ms; }

.yir-overlay[data-motion="playing"] .yir-year {
  animation: yirYear 700ms 120ms cubic-bezier(0.2, 1.2, 0.3, 1) both;
}

.yir-overlay[data-motion="playing"] .yir-stat {
  animation: yirRise 620ms cubic-bezier(0.2, 0.9, 0.25, 1) both;
  animation-delay: calc(var(--yir-index, 0) * 90ms + 420ms);
}

@keyframes yirOverlayIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes yirRise {
  from { opacity: 0; transform: translateY(16px) scale(0.985); }
  to { opacity: 1; transform: none; }
}

@keyframes yirYear {
  from { opacity: 0; transform: translateY(10px) scale(0.82); }
  to { opacity: 1; transform: none; }
}

@keyframes yirSheen {
  from { opacity: 0; transform: translateX(-46%); }
  30% { opacity: 1; }
  to { opacity: 0; transform: translateX(46%); }
}

@keyframes yirAurora {
  from { transform: translate3d(-3%, -2%, 0) scale(1); }
  to { transform: translate3d(3%, 4%, 0) scale(1.12); }
}

@media (max-width: 560px) {
  .yir-scroll {
    padding:
      calc(14px + var(--safe-top))
      calc(12px + var(--safe-right))
      calc(18px + var(--safe-bottom))
      calc(12px + var(--safe-left));
  }

  .yir-card { padding: 18px; gap: 14px; }
  .yir-mark { padding-top: 20px; }
  .yir-crown { top: 0; left: 11px; width: 24px; height: 20px; }
  .yir-wordmark strong { font-size: 16px; }
  .yir-wordmark strong + strong { font-size: 15px; }
}

/* Not merely stilled. A player who has turned on Reduce Motion has already told every site they do
   not want a screenful of movement, and the honest answer to that on a celebration screen is the
   numbers without the ceremony — the same call the results panel makes when it hides the confetti
   rather than freezing it mid-fall. The count-up is switched off in script for the same reason:
   this stylesheet cannot stop a number ticking. */
@media (prefers-reduced-motion: reduce) {
  .yir-overlay,
  .yir-overlay .yir-aurora,
  .yir-overlay .yir-card::after,
  .yir-overlay .yir-brand,
  .yir-overlay .yir-years,
  .yir-overlay .yir-year,
  .yir-overlay .yir-headline,
  .yir-overlay .yir-subhead,
  .yir-overlay .yir-empty,
  .yir-overlay .yir-stat,
  .yir-overlay .yir-footnote,
  .yir-overlay .yir-actions {
    animation: none !important;
    transition: none !important;
  }

  .yir-overlay .yir-card::after { content: none; }
}

/* ---- Team Deathmatch ---------------------------------------------------------------------
   The lobby entry and the party panel. Kept to the same words, the same order and the same
   shape as the app's — TeamDeathmatchParityTests holds the strings, and this holds the layout
   they sit in. */

/* Shaped like the year-in-review card the app puts above it, and for the same reason: both are
   "another thing you could do from here" rather than a mode in the chooser, and two entry points
   of the same kind that looked different would read as two kinds of thing.

   The margin is the whole difference between that being true and being merely intended. It was
   zero, and this row is the only block in the lobby that had none — .lobby-play has 14, .lobby-solo
   10, .rounds-strip 14 — so it sat with its top edge exactly on the mode chooser's bottom edge, at
   the same radius and against the same border colour. Measured at 390: the chooser ends at 324 and
   this began at 324. What that draws is a THIRD ROW OF THE CHOOSER, directly above the two verbs
   that act on the chooser — and pressing Play online after reading it does not start a deathmatch.
   Twelve, to match the 12 the app already carries below this same row. */
.team-deathmatch {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  width: 100%;
  margin-top: 12px;
  padding: 11px 12px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--panel-soft);
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition: transform 90ms ease, filter 90ms ease;
}

.team-deathmatch:hover { background: var(--control-hover); }
.team-deathmatch:active { transform: scale(.975); filter: brightness(.9); transition-duration: 60ms; }

/* The four team colours as the mark, in Core's order — painted from SlitherTeams rather than
   written in here, so they are the same four the arena will draw.

   Quartered rather than a row of four bars, and both halves of that are the point. A 2x2 tile is
   the shape of the thing it stands for — four sides — and it is a MARK, sitting in the row where
   the chips below put an icon, which is what makes the row read as a door rather than a banner.
   A 45x22 stripe of four saturated hues, flush against the left edge of a lobby that is otherwise
   black and one green, was the loudest element on the panel and the first thing the eye landed
   on, for something that is not the primary action. Quartering it drops the colour from 792 square
   pixels to 400 without losing a single one of the four, and hands the 23px it gives back to the
   line of text beside it, which is the one thing on the row that runs out of room first.

   Markup untouched in both clients — four <i> elements painted from Teams.All — so this is a
   layout change and neither client has learned a colour of its own. */
.tdm-flags {
  display: grid;
  grid-template-columns: repeat(2, 10px);
  gap: 2px;
}

.tdm-flags i {
  display: block;
  width: 10px;
  height: 10px;
  border-radius: 2.5px;
}

.tdm-main { display: grid; gap: 2px; min-width: 0; }
.tdm-main strong { font-size: 13.5px; font-weight: 750; }

.tdm-main small {
  overflow: hidden;
  color: var(--text-dim);
  font-size: 11px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tdm-arrow { color: var(--text-dim); font-size: 18px; }

/* ---- the party lobby --------------------------------------------------------------------- */

.party-panel { gap: 12px; }

.party-head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 10px;
}

.party-head h1 { margin: 0; font-size: 20px; font-weight: 850; }
.party-tagline { margin: 3px 0 0; font-size: 12.5px; color: var(--text-dim); }

.party-state { margin: 0; font-size: 14.5px; line-height: 1.35; text-align: center; }
.party-queue { margin: 0; font-size: 12.5px; color: var(--ui-accent); text-align: center; }

.party-tip,
.party-builder,
.party-game-choice {
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--panel-soft);
}

.party-tip { display: grid; gap: 3px; padding: 9px 11px; color: var(--text-dim); font-size: 11.5px; }
.party-tip strong { color: var(--ui-accent); font-size: 10px; letter-spacing: .05em; text-transform: uppercase; }

.party-builder {
  display: grid;
  gap: 2px;
  padding: 10px 11px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}
.party-builder strong { font-size: 13px; }
.party-builder small { color: var(--text-dim); }

.party-game-choice { display: grid; gap: 9px; padding: 11px; }
.party-choice-head { display: flex; justify-content: space-between; gap: 8px; font-size: 12.5px; }
.party-choice-head span { color: var(--ui-accent); }
.party-game-choice label { display: grid; grid-template-columns: 1fr auto; gap: 5px 8px; color: var(--text-dim); font-size: 11.5px; }
.party-game-choice select {
  min-width: 150px;
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  background: var(--control-surface);
  color: var(--text);
  padding: 6px;
}
.party-game-choice > small,
.party-private-options > small { color: var(--text-dim); font-size: 11px; line-height: 1.35; }
.party-private-options { display: grid; gap: 8px; }
.party-private-options input[type="range"] { grid-column: 1 / -1; width: 100%; accent-color: var(--accent); }
.party-private-options output { color: var(--text); }

.party-seats {
  list-style: none;
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
}

/* Two columns when the screen is wide and short, which is a phone held sideways.
 *
 * Measured when a side was eight: eight rows in a single column is 378px of list, and a 844x390
 * landscape phone leaves the panel 276px for everything it has to say — the list showed one seat
 * and a scrollbar. There is no shortage of width there: the panel is 520px in an 844px window.
 *
 * A side is four now, so the list is half that and the overflow it was fixing is gone. The rule
 * stays because the shortage it was measured against has not: seats are only part of what the
 * panel holds — a state line, a queue line, a ready line, a share box, a code and two buttons —
 * and two columns of two still buys back 83px of the 276 for nothing.
 *
 * Not on a desktop, where the height is not the constraint: four seats in one column read as a
 * list of four, and two columns of two read as two pairs. */
@media (min-width: 620px) and (max-height: 620px) {
  .party-seats { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* And the two buttons side by side, for the same reason and against the same shortage: stacked
     they are 120px of a 276px panel, which is most of what a sideways phone has to give. */
  .party-acts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.party-seat {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--panel-soft);
  font-size: 13.5px;
}

/* An empty seat is drawn quietly rather than left out. It is the room the party still has, which
   is exactly what somebody deciding whether to send another link is looking for. */
.party-seat.is-empty { opacity: 0.42; border-style: dashed; }
.party-seat.is-ready { border-color: color-mix(in srgb, var(--ui-accent) 45%, var(--panel-border)); }

.party-seat-number { color: var(--text-dim); font-size: 11.5px; text-align: right; }
.party-seat-name { font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.party-seat-status { font-size: 11.5px; color: var(--text-dim); white-space: nowrap; }
.party-seat.is-ready .party-seat-status { color: var(--ui-accent); }

/* Air above and below. Butted straight against the last seat and the share box it read as one more
   row of the list rather than as a summary of it. */
.party-ready-line { margin: 2px 0; text-align: center; font-size: 12.5px; color: var(--text-dim); }

.party-share {
  display: grid;
  justify-items: center;
  gap: 8px;
  padding: 14px 12px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--panel-soft);
}

.party-share-line { font-size: 12.5px; color: var(--text-dim); }

/* Bordered rather than filled, and deliberately so: Start is the decision on this screen and
   Share supports it. Both drawn in the accent put two equal-weight greens one above the other and
   left nothing saying which one the party is waiting for — the same mistake the connect screen
   records making with two primaries side by side. */
.party-share-button {
  width: 100%;
  min-height: 44px;
  border: 1px solid color-mix(in srgb, var(--ui-accent) 55%, transparent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ui-accent) 12%, transparent);
  color: var(--ui-accent);
  font-weight: 850;
  cursor: pointer;
}

.party-share-button:disabled { cursor: not-allowed; filter: saturate(0.62) brightness(0.78); }

/* The code in full, because somebody will read it out rather than send it — and because it is the
   one string on this screen anybody can repeat back. Monospace is how a person knows to read it
   character by character. */
.party-code-shown {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 14.5px;
  letter-spacing: 0.06em;
  user-select: all;
}

/* The one decision on this screen, held where it can be seen.
 *
 * A party lobby grows: a row per seat, a share box and a code, and on a 320x568 phone a full party
 * of eight — a side's worth, before a side became four — put that 279px past what the panel can
 * show. "Start the round" was below the fold with nothing on screen to suggest it existed, and the
 * owner scrolls past their own party to find the button their party is waiting for. Four rows have
 * far more room, and the rule stays because being below the fold at all is the fault, not being a
 * long way below it.
 *
 * Sticky rather than a nested scroller on the seat list. A list that scrolls inside a panel that
 * scrolls is two gestures a thumb has to tell apart, and the rounds drawer is the one place that
 * earns it. Here nothing needs to be hidden; the action just has to stop leaving.
 *
 * Full-bleed, on negative margins that cancel the panel's own 18px padding, so the rows slide
 * underneath it rather than past it in a gutter. Painted in `--panel-strong` rather than `--panel`
 * for the reason the builder's pinned band records: the panel's ground is translucent, and a
 * translucent band over it composites to a different colour. */
.party-acts {
  display: grid;
  gap: 6px;
  position: sticky;
  bottom: -18px;
  z-index: 1;
  margin: 0 -18px -18px;
  padding: 8px 18px 18px;
  background: var(--panel-strong);
}

.party-blocked { margin: 0; text-align: center; font-size: 12px; color: var(--text-dim); }

.party-start { display: grid; gap: 20px; }
.party-code-block { display: grid; gap: 8px; }
.party-code-block label { font-size: 12.5px; color: var(--text-dim); }
.party-code-block small { font-size: 11.5px; color: var(--text-dim); }

.party-code-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }

.party-code-row input {
  min-width: 0;
  min-height: 44px;
  padding: 0 12px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--control-surface);
  color: var(--text);
  /* 16px or larger, or mobile Safari zooms the whole page in on focus and leaves the lobby
     scrolled sideways with no way back that does not look like a bug. */
  font: 700 16px ui-monospace, Menlo, monospace;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.party-code-row button {
  min-height: 44px;
  padding: 0 18px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  background: var(--control-hover);
  color: var(--text);
  font-weight: 800;
  cursor: pointer;
}

.party-code-row button:disabled { cursor: not-allowed; filter: saturate(0.62) brightness(0.78); }

/* Party lobby: party → players → game → invite → start. */
.party-panel { max-width: 680px; }
.party-body { gap: 18px; }
.party-roster-card, .party-game-choice, .party-share {
  border: 1px solid rgba(117,255,63,.23); border-radius: 22px;
  background: linear-gradient(145deg, rgba(8,22,17,.97), rgba(3,12,9,.97));
  box-shadow: 0 16px 38px rgba(0,0,0,.22), inset 0 0 22px rgba(117,255,63,.025);
}
.party-roster-card { padding: 16px; }
.party-summary { display: grid; grid-template-columns: 46px minmax(0,1fr) auto; align-items: center; gap: 12px; margin-bottom: 14px; }
.party-summary > span:nth-child(2) { display: grid; gap: 2px; }
.party-summary strong { color: #f5f7f6; font-size: 19px; }
.party-summary small { color: #91a39d; font-size: 12px; }
.party-summary-mark { display: grid; width: 44px; height: 44px; place-items: center; border-radius: 50%; background: rgba(117,255,63,.1); color: #75ff3f; font-size: 23px; }
.party-summary-mark svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.party-access-pill { min-height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid rgba(117,255,63,.2); border-radius: 999px; color: #f5f7f6; font-size: 12px; font-weight: 750; }
.party-state { margin-bottom: 10px; color: #91a39d; font-size: 12px; text-align: left; }
.party-seat { min-height: 64px; grid-template-columns: 44px minmax(0,1fr) auto; padding: 9px 11px; border-radius: 15px; background: rgba(7,18,14,.68); }
.party-seat.is-empty { opacity: 1; border-style: dashed; cursor: pointer; }
.party-seat.is-empty .party-seat-number { display: grid; width: 42px; height: 42px; place-items: center; border: 1px solid rgba(117,255,63,.4); border-radius: 50%; color: #75ff3f; font-size: 27px; }
.party-seat-number { color: #75ff3f; font-size: 21px; text-align: center; }
.party-seat-name { color: #f5f7f6; }
.party-seat-status { color: #91a39d; }
.party-seat-edit, #party-change-game, #party-copy { min-height: 44px; padding: 0 13px; border: 1px solid rgba(117,255,63,.4); border-radius: 11px; background: rgba(117,255,63,.06); color: #75ff3f; font-weight: 800; cursor: pointer; }
.party-game-choice { padding: 16px; }
.party-choice-head { align-items: center; }
.party-choice-head > span { display: grid; gap: 4px; }
.party-choice-head small { color: #75ff3f; font-size: 10px; font-weight: 850; letter-spacing: .08em; text-transform: uppercase; }
.party-choice-head strong { color: #f5f7f6; font-size: 17px; }
.party-game-config { display: grid; gap: 9px; padding-top: 12px; border-top: 1px solid rgba(145,180,170,.14); }
.party-royale-helper { color: #91a39d; font-size: 11px; line-height: 1.4; }
.party-share { justify-items: stretch; padding: 16px; }
.party-share > strong { color: #75ff3f; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
.party-share-line { color: #91a39d; }
.party-share .party-code-row { width: 100%; padding: 7px 7px 7px 13px; border: 1px dashed rgba(117,255,63,.25); border-radius: 12px; }
.party-share-button { min-height: 52px; border-radius: 14px; }
.party-acts { position: static; margin: 0; padding: 0; background: transparent; }
.party-acts .play-button { min-height: 78px; border-radius: 19px; background: linear-gradient(90deg,#a8ff24,#62f234); color: #03100d; font-size: 20px; font-weight: 900; box-shadow: 0 0 25px rgba(117,255,63,.2); }
.party-acts .plain-button { min-height: 48px; color: #91a39d; }
.party-ready-line { display: none; }
.party-panel button:focus-visible, .party-seat[role=button]:focus-visible { outline: 2px solid #75ff3f; outline-offset: 3px; }
.party-seat-kick { color: #ff8b9c !important; border-color: rgba(255,77,105,.3) !important; }
@media (hover:hover) { .party-seat.is-empty:hover, .party-panel button:not(:disabled):hover { border-color: rgba(117,255,63,.6); filter: brightness(1.07); transform: translateY(-1px); } }

@media (min-width: 900px) {
  .party-panel { width: min(980px, calc(100vw - 56px)); max-width: 980px; }
  .party-panel .party-body { display: grid; grid-template-columns: minmax(0,1.18fr) minmax(340px,.82fr); gap: 20px; align-items: start; }
  .party-panel .party-roster-card { grid-column: 1; grid-row: 1 / span 4; padding: 22px; }
  .party-panel .party-game-choice { grid-column: 2; grid-row: 1; padding: 20px; }
  .party-panel .party-queue { grid-column: 2; }
  .party-panel .party-share { grid-column: 2; padding: 20px; }
  .party-panel .party-acts, .party-panel .party-ready-line, .party-panel .party-blocked { grid-column: 2; }
  .party-panel .party-seat { min-height: 76px; padding-inline: 16px; }
}

/* The chosen mode flavours the lobby again; the premium pass had hard-coded lime over this. */
.lobby.map-royale {
  /* Brand purple, named. This was `var(--danger, #b949ff)` — "danger, or purple if danger is
     missing" — and --danger is NEVER missing, so the fallback never fired and choosing Royale
     turned the Play button, the Browse link and the live dot RED (#ff4d6d, measured) while the
     Royale card beside them stayed purple. The app's tokens.css records shipping this exact bug
     and fixing it; the browser twin was never updated. */
  --cta-ink: #ffffff;
  --lobby-green: var(--brand-purple);
  --lobby-green-bright: color-mix(in srgb, var(--brand-purple) 78%, white);
  --lobby-green-dark: color-mix(in srgb, var(--brand-purple) 78%, black);
  --lobby-border-strong: color-mix(in srgb, var(--brand-purple) 55%, transparent);

  /* Royale's drifting band DARKENS, where Classic's in :root lightens, and the asymmetry is the
     point. This button is white on purple; Classic is near-black on green. A sweep of white here
     would be a sweep of contrast loss, and this fill has none to give — its pale end,
     color-mix(--brand-purple 78%, white), measures 2.77:1 under white text, which is below even
     the 3:1 the guide allows type this size and weight. So the band is the brand's own VIOLET:
     it pulls the fill back towards the dark end as it passes, and the worst frame in the cycle is
     the unswept button, unchanged. Matches the app's --cta-sheen value for value.

     Declared in both copies of this block deliberately. There are two, they are already identical
     twins for --cta-ink, and only the later one wins — a sheen in just one is a Royale button that
     silently reverts to Classic's pale green the day somebody tidies the duplicate away. */
  --cta-sheen: rgba(122, 40, 255, .45);

  /* Blue against the purple button, and measured the same way for the same reason. Tinted to the
     same purple, the snake and the gradient were one flat mass with only the art's own black
     linework between them — at arm's length, a smudge. The app's Royale token, value for value.
     In BOTH copies of this block, exactly as --cta-sheen above is and for its reason. */
  --cta-cobra: hue-rotate(108deg) saturate(1.12) brightness(.74) contrast(1.12)
    drop-shadow(0 3px 12px rgba(15, 0, 40, .6));
}
.lobby.map-royale .lobby-play { background: linear-gradient(90deg,var(--lobby-green-bright),var(--lobby-green) 55%,var(--lobby-green-dark)) !important; box-shadow: 0 0 14px color-mix(in srgb,var(--lobby-green) 35%,transparent),0 0 32px color-mix(in srgb,var(--lobby-green) 16%,transparent); }
.lobby-mode-classic { --mode-accent: var(--theme-mode-classic, var(--accent)); }

/* Slightly denser without turning controls into small targets. */
.lobby-mode { min-height: 156px; padding: 14px 12px 12px; }
.lobby-mode-icon { width: 54px; height: 54px; }
.lobby-mode strong { font-size: clamp(22px,5vw,30px); }
.lobby-play { min-height: 78px; }
.lobby-solo { min-height: 60px; }
.rounds-strip { min-height: 58px; }
.team-deathmatch { min-height: 76px; padding-block: 12px; }
.lobby-mode-icon img { width: 64px; height: 64px; object-fit: contain; filter: hue-rotate(var(--theme-icon-hue, 0deg)) saturate(1.2) drop-shadow(0 0 8px color-mix(in srgb,var(--mode-accent) 30%,transparent)); }

/* ---------- premium arcade lobby -----------------------------------------------------------
   Final lobby-only visual contract. Gameplay, routing, account state and match controls retain
   their existing ids and handlers; these rules only restyle the open lobby above the live arena. */
:root {
  --lobby-bg-primary: #030909;
  --lobby-bg-secondary: #07110f;

  /* Glass over the live arena, never an opaque panel (brand guide §3), and the app's exact
     surface values. These were opaque green-blacks (#081210 / #0c1715 / #101c19), which is both a
     panel rather than glass and a different ink from the app's blue-violet. */
  --lobby-surface-1: rgba(10, 14, 25, .88);
  --lobby-surface-2: rgba(18, 16, 38, .9);
  --lobby-surface-3: rgba(24, 22, 46, .92);
  --lobby-border: var(--panel-border);

  /* Brand, not theme. This read `color-mix(var(--accent) 42%)`, so the lobby's furniture was
     recoloured by the player's ARENA choice — the exact failure the app's tokens.css records
     having shipped, under a comment claiming the opposite. Ember made the whole home screen gold
     and destroyed the money signal with it; Nebula made it blue. */
  --lobby-border-strong: rgba(216, 60, 255, .55);

  /* The lobby's primary fill. Still named "green" because forty rules read it, but it is the
     CLASSIC accent — green means Classic (brand guide §1), and `.lobby.map-royale` below swaps it
     for the brand purple, the way the app's --cta-fill does. It used to be var(--accent). */
  --lobby-green: var(--brand-green);
  --lobby-green-bright: color-mix(in srgb, var(--brand-green) 78%, white);
  --lobby-green-dark: color-mix(in srgb, var(--brand-green) 78%, black);
  --lobby-pink: var(--danger);
  --lobby-purple: var(--brand-purple);
  --lobby-blue: #4d8dff;
  --lobby-yellow: var(--brand-gold);

  /* Fixed brand text, because these surfaces are fixed brand ink. Reading --text/--text-dim put
     the eight PASTEL themes' dark text on the lobby's dark cards: measured on Blush, "Play solo
     against bots" came out at contrast 1.07, the two quick-action tiles at 1.23 and the party row
     at 1.02 — invisible, on better than half the catalogue. The arena behind still goes pink. */
  --lobby-text: var(--brand-text);
  --lobby-text-secondary: var(--brand-text-secondary);
  --lobby-text-muted: var(--brand-text-muted);

  /* 28 cards · 22 buttons and rows · 12 small · 999 pills (brand guide §3). Was 20/18/10 — off the
     scale on three of the four, which is why the web lobby read tighter than the phone's. */
  /* Ink for the CTA, paired with its fill rather than with the theme. See .lobby-play. */
  --cta-ink: #06210a;

  /* The band that drifts across the CTA, matching the app's --cta-sheen value for value. This is
     the CLASSIC one, because this :root is the Classic default and `.lobby.map-royale` swaps it
     the way it swaps --lobby-green.

     It LIGHTENS, and the direction is not a taste: the ink above is #06210a, near-black, so every
     step paler is a step further from it. Royale's darkens, for the mirror-image reason given
     there. Neither sweep can take the label anywhere the still button was not already, which is
     the whole trick — a moving background is the easy way to pass a contrast check on the first
     frame and fail it on the fortieth. Pale GREEN rather than white, so what moves is still
     Classic's own colour. */
  --cta-sheen: rgba(212, 255, 173, .45);

  /* The striking cobra on the CTA, tinted. One asset, not a second PNG of the same animal in
     another colour — on green the artwork's own green would vanish into the button.

     140deg, MEASURED, and copied from the app's token value for value. `hue-rotate` is a matrix
     approximation rather than a rotation in HSL, so it does not move a colour by the degrees you
     name: the matrix takes the art's dominant green (96,176,80) to hue 243, indigo, while the app's
     first attempt at 214deg by eye produced hue 330 — salmon pink on a green button. Run the
     matrix before touching this number. */
  --cta-cobra: hue-rotate(140deg) saturate(1.15) brightness(.66) contrast(1.15)
    drop-shadow(0 3px 12px rgba(0, 24, 6, .6));

  --lobby-radius-small: 12px;
  --lobby-radius-button: 22px;
  --lobby-radius-card: 28px;
  --lobby-radius-pill: 9999px;
}

body:not(.is-playing)::after {
  position: fixed;
  z-index: -1;
  inset: 0;
  pointer-events: none;
  background-image:
    linear-gradient(rgba(117, 255, 63, .035) 1px, transparent 1px),
    linear-gradient(90deg, rgba(117, 255, 63, .035) 1px, transparent 1px);
  background-size: 52px 52px;
  content: "";
  mask-image: linear-gradient(to bottom, rgba(0, 0, 0, .8), transparent 80%);
}

/* ONE number drives the lockup. --wordmark is COBRA's size, ROYALE is a ratio of it, and the
   crown's width, height and offsets are ratios of it too — so a breakpoint that resizes the type
   cannot leave the crown behind. That is exactly how the crown detached: the type shrank from 48px
   to 24px across three media queries while `top: -5px`, `left: 13px` and the 24px of padding above
   the type all stayed literal, and the crown ended up floating in the gap. Ratios taken from the
   desktop lockup, which was the one that still read as a lockup.

   align-items is flex-start, not centre, and that is load-bearing rather than tidying: min-height
   is larger than the lockup on a short screen, and a centred lockup drifts down into the slack
   while the absolutely-positioned crown stays pinned to the top of the box. Same separation,
   different cause. */
.site-brand {
  --wordmark: clamp(34px, 4vw, 48px);
  top: calc(20px + var(--safe-top)) !important;
  left: max(24px, calc(50% - 450px + var(--safe-left))) !important;
  right: auto !important;
  display: inline-flex !important;
  align-items: flex-start !important;
  min-height: 86px !important;
  padding: 24px 8px 4px 2px !important;
  gap: 8px !important;
  color: var(--lobby-text) !important;
}

.brand-lockup { display: grid; gap: 6px; }
.brand-type { display: grid; line-height: .83; }
.brand-type strong { color: var(--lobby-text) !important; font-size: var(--wordmark) !important; font-style: italic; font-weight: 950; letter-spacing: -.045em; }
/* ROYALE is the brand's purple with a glow, and it does not move. It read --lobby-green, which
   meant the logotype was recoloured by the player's arena theme and again by the selected map —
   the word was lime on Forest, gold on Ember, blue on Nebula and red the moment you chose Royale.
   A logotype that changes colour with a setting is not a logotype. Flat fill plus a text-shadow,
   never background-clip over a gradient: the app records that the transformed letter spans paint
   outside the parent's text clip and the whole word disappeared. */
.brand-type strong + strong {
  color: var(--brand-purple) !important;
  font-size: calc(var(--wordmark) * .94) !important;
  text-shadow: 0 0 22px rgba(216, 60, 255, .55), 0 0 8px rgba(234, 66, 245, .4);
}
.brand-lockup small { color: var(--lobby-text-secondary); font-size: 11px; font-weight: 750; letter-spacing: .1em; text-transform: uppercase; }
/* Everything here is a ratio of --wordmark, and the one literal is the 24px of padding above the
   type in .site-brand, which is what `top` has to reach down from. At 48px the calc gives back the
   -5px/13px/48x40 this rule used to state outright, so the desktop lockup is unchanged to the
   pixel; every smaller wordmark now gets the same lockup instead of a crown left behind at the
   size the last person to touch it was looking at. */
.brand-crown {
  top: calc(24px - var(--wordmark) * .604) !important;
  left: calc(var(--wordmark) * .271) !important;
  width: var(--wordmark) !important;
  height: calc(var(--wordmark) * .833) !important;
  filter: drop-shadow(0 0 9px rgba(255, 200, 61, .24));
}

.account-menu-toggle,
.quick-settings-toggle {
  top: calc(26px + var(--safe-top)) !important;
  border-color: var(--lobby-border) !important;
  background: rgba(7, 18, 16, .88) !important;
  color: var(--lobby-text) !important;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .2) !important;
}

.account-menu-toggle { right: max(88px, calc(50% - 384px + var(--safe-right))) !important; min-height: 58px !important; border-radius: var(--lobby-radius-pill) !important; }
.quick-settings-toggle { right: max(24px, calc(50% - 450px + var(--safe-right))) !important; width: 58px !important; height: 58px !important; border-radius: 50% !important; }
/* :not(.is-owing), because the bare rule's !important was beating `.top-coin-pill.is-owing`
   further up the file and the debt pill stayed GOLD. The guide is explicit (§1): a balance a
   refund has taken below zero drops its gold in BOTH clients, because a gold zero reads as a
   purse somebody merely emptied rather than one with nothing to spend from. app.js already
   toggles .is-owing and carries the comment explaining why; only the paint was missing. */
.top-coin-pill:not(.is-owing) { color: var(--lobby-yellow) !important; }

/* --lobby-top is declared beside every padding-top that moves it, and #home-panel's scroll cap
   reads it. The cap is the shell's own padding, not a sixth number hoping to match it — the
   mistake .menu-panel's max-height records.

   And --shell-top is bound to the same number here, because .menu-panel's cap was making exactly
   that mistake one level up. This rule and the 700px one below are what actually move the shell's
   top padding — 142px and 118px — while --shell-top still said 58/72/86 from the five earlier
   blocks that no longer decide anything. So every .menu-panel was allowed to be up to 56px taller
   than the space it sits in: measured at 375x812 the season panel's cap came out 724px against
   678px of real room and its bottom edge landed 30px BELOW the viewport, with its own scroller —
   and therefore the last rung of a fifty-tier track — ending off the screen. Written as
   `var(--lobby-top)` rather than restated, so the two cannot drift apart one edit at a time. */
.menu-shell { --lobby-top: 142px; --shell-top: var(--lobby-top); padding-top: calc(var(--lobby-top) + var(--safe-top)); align-items: start; overflow-x: hidden; }
.lobby {
  width: min(760px, calc(100vw - 28px - var(--safe-left) - var(--safe-right))) !important;
  max-width: 760px !important;
  padding: 0 !important;
  overflow: visible !important;
  background: transparent !important;
}

.lobby::before { content: none !important; }
.lobby-mascot { display: none !important; }
.lobby-body { padding: 0 !important; }
.lobby-controls { display: grid; gap: 16px; }

.lobby-modes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.lobby-mode {
  --mode-accent: var(--lobby-green);
  position: relative;
  isolation: isolate;
  display: flex;
  min-width: 0;
  min-height: 230px;
  padding: 20px 16px 16px;
  overflow: hidden;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: 1px solid var(--lobby-border);
  border-radius: var(--lobby-radius-card);
  background: linear-gradient(145deg, color-mix(in srgb, var(--mode-accent) 9%, var(--theme-mode-surface, var(--panel))), var(--theme-mode-surface, var(--panel)));
  color: var(--lobby-text);
  text-align: center;
  transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
}

.lobby-mode-royale { --mode-accent: var(--theme-mode-royale, var(--accent)); }
.lobby-mode::after { position: absolute; z-index: -1; right: -25%; bottom: -35%; width: 80%; aspect-ratio: 1; border-radius: 50%; background: var(--mode-accent); content: ""; filter: blur(55px); opacity: .08; }
.lobby-mode.is-active { border: 2px solid var(--mode-accent); background: linear-gradient(145deg, color-mix(in srgb, var(--mode-accent) 13%, var(--theme-mode-surface, var(--panel))), var(--theme-mode-surface, var(--panel))); box-shadow: 0 0 22px color-mix(in srgb, var(--mode-accent) 20%, transparent), inset 0 0 24px color-mix(in srgb, var(--mode-accent) 5%, transparent); }
.lobby-mode.is-active::after { opacity: .18; }

/* ---------- the pieces the app grew in the same pass ------------------------------------------
   Twins of Home.razor.css / GameModeCard.razor.css / ActionCard.razor.css. Written here rather
   than left to the app alone because the two clients are held to one brand contract by
   BrandTokenParityTests, and a screen redesigned on the phone and not in the browser is exactly
   the drift that test exists to catch. */

/* The label over the mode pair. Quiet on purpose: its job is to say the pair is ONE choice, and a
   heading that competed with the cards under it would be doing the opposite. */
.lobby-section-label {
  margin: 2px 0 0;
  /* The ink, faded, not the dim token — see the app's .section-label. At 11.5px with .16em
     tracking and no surface behind it, #6B7280 disappears into the pastel themes. */
  color: var(--lobby-text, var(--text));
  opacity: .74;
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: .16em;
  line-height: 1;
  text-transform: uppercase;
}

/* The selected mark. Colour alone is the one distinction a red-green colour-blind player cannot
   make, and green-versus-purple was carrying the whole message. */
.lobby-mode-check {
  position: absolute;
  z-index: 2;
  top: 10px;
  right: 10px;
  display: grid;
  width: 24px;
  height: 24px;
  place-items: center;
  border-radius: 50%;
  background: var(--mode-accent);
  /* The disc is --mode-accent and both mode colours are bright, so the tick is the DARK one.
     --accent-ink is the token that already answers "what reads on a bright accent" per theme. */
  color: var(--accent-ink, #03100d);
  opacity: 0;
  transform: scale(.6);
  transition: opacity 160ms ease, transform 160ms cubic-bezier(.2, .9, .3, 1.2);
}

.lobby-mode.is-active .lobby-mode-check { opacity: 1; transform: scale(1); }

.lobby-mode-check svg {
  width: 14px;
  height: 14px;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 3.2;
}

/* The CTA's second line. Not italic — the label above is the shout, and a second italic line at
   the same angle read as one two-line title rather than a title with a note under it. `em` so it
   tracks the label's own clamp instead of needing its own breakpoints. */
.lobby-play-main {
  display: flex;
  min-width: 0;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}

.lobby-play-line { display: inline-flex; align-items: center; gap: 12px; }

.lobby-play-sub {
  position: relative;
  z-index: 1;
  max-width: 100%;
  overflow: hidden;
  font-size: .30em;
  font-style: normal;
  font-weight: 800;
  letter-spacing: .13em;
  line-height: 1.2;
  /* Opacity rather than a paler colour: the ink differs between the two modes (dark on green,
     white on purple) and one literal cannot be right on both. */
  opacity: .72;
  text-overflow: ellipsis;
  text-shadow: none;
  white-space: nowrap;
}

/* Friends and Solo, equal. 1fr 1fr rather than auto columns so they stay equal whatever is in
   them — the two descriptions are different lengths and auto sizing would hand the longer one a
   wider card, which is the imbalance this pair replaced. */
.lobby-duo {
  display: grid;
  margin-top: 12px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

.lobby-action {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 76px;
  padding: 14px 12px 14px 14px;
  border: 1px solid var(--panel-border, rgba(255, 255, 255, .1));
  border-radius: 20px;
  background: color-mix(in srgb, var(--lobby-surface-2, rgba(5, 9, 14, .5)) 62%, transparent);
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  color: var(--lobby-text, var(--text));
  text-align: left;
  /* Secondary, and the shadow is the whole of how that is said. No glow: on this screen glow means
     selection, a primary action, or live status, and this is none of the three. */
  box-shadow: 0 10px 24px rgba(0, 0, 0, .22);
  transition: transform 150ms ease, border-color 150ms ease, background-color 150ms ease;
}

.lobby-action-icon {
  display: grid;
  width: 42px;
  height: 42px;
  flex: none;
  place-items: center;
  border-radius: 14px;
  background: color-mix(in srgb, var(--card-tone) 14%, transparent);
  color: var(--card-tone);
}

.lobby-action-icon svg {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 1.9;
}

/* Purple for people, gold for the practice room, both from theme tokens rather than the mockup's
   literals — the pastel themes cannot carry #C633F3 (see the contrast note in ThemeStyle.razor). */
.lobby-action-friends { --card-tone: var(--ui-accent, #d83cff); }
.lobby-action-solo { --card-tone: var(--brand-gold, #ffc83d); }

.lobby-action-copy { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
.lobby-action-copy strong { font-size: 14.5px; font-weight: 800; letter-spacing: -.01em; line-height: 1.15; }

/* Wraps to two lines and is allowed to. Clamped rather than ellipsised: these sentences are the
   only explanation either destination gets, and half of one is not a shorter explanation. */
.lobby-action-copy small {
  display: -webkit-box;
  overflow: hidden;
  color: var(--lobby-text-secondary, var(--text-dim));
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.3;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.lobby-action-chevron {
  display: grid;
  width: 22px;
  height: 22px;
  flex: none;
  margin-left: auto;
  place-items: center;
  color: var(--lobby-text-secondary, var(--text-dim));
}

.lobby-action-chevron svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2.4;
}

.lobby-action:hover {
  border-color: color-mix(in srgb, var(--card-tone) 40%, var(--panel-border, rgba(255, 255, 255, .1)));
  background: color-mix(in srgb, var(--lobby-surface-2, rgba(5, 9, 14, .5)) 76%, transparent);
}

.lobby-action:active {
  transform: scale(.975);
  border-color: color-mix(in srgb, var(--card-tone) 55%, var(--panel-border, rgba(255, 255, 255, .1)));
  transition-duration: 60ms;
}

@media (max-width: 380px) {
  .lobby-action { min-height: 70px; gap: 9px; padding: 12px 10px 12px 11px; }
  .lobby-action-icon { width: 36px; height: 36px; border-radius: 12px; }
  .lobby-action-icon svg { width: 20px; height: 20px; }
  .lobby-action-copy strong { font-size: 13.5px; }
  .lobby-action-copy small { font-size: 11px; }
  .lobby-action-chevron { width: 16px; }
  .lobby-mode-check { top: 8px; right: 8px; width: 21px; height: 21px; }
}

/* Below the narrowest phone this ships to the words genuinely stop fitting, so the pair stacks
   rather than clipping. One card's height is the right price for a readable label. */
@media (max-width: 330px) {
  .lobby-duo { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  .lobby-action { transition: none; }
  .lobby-action:active { transform: none; }
  /* The mark still APPEARS — it is information, not decoration. Only the pop is dropped. */
  .lobby-mode-check { transition: none; transform: none; }
  .lobby-mode.is-active .lobby-mode-check { transform: none; }
}


.lobby-mode-icon { display: grid !important; width: 76px; height: 76px; margin: 0 0 4px !important; place-items: center; border: 1px solid color-mix(in srgb, var(--mode-accent) 44%, transparent); border-radius: 50%; background: color-mix(in srgb, var(--mode-accent) 9%, rgba(3, 9, 9, .9)); color: var(--mode-accent) !important; box-shadow: 0 0 18px color-mix(in srgb, var(--mode-accent) 15%, transparent); }
.lobby-mode-icon svg { width: 44px; height: 44px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2.5; }
.lobby-mode strong { font-size: clamp(28px, 4vw, 36px); font-style: italic; font-weight: 900; letter-spacing: -.025em; line-height: .95; text-transform: uppercase; }
.lobby-mode .lobby-mode-subtitle { display: block; margin: 0; color: var(--mode-accent); font-size: 13px; font-weight: 750; }
.lobby-mode .lobby-mode-badge { display: inline-flex; min-height: 28px; margin: 8px 0 0; padding: 5px 10px; align-items: center; gap: 6px; border: 1px solid color-mix(in srgb, var(--mode-accent) 38%, transparent); border-radius: var(--lobby-radius-pill); background: rgba(3, 9, 9, .7); color: var(--mode-accent); font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }
.lobby-mode-badge b { letter-spacing: -2px; }

/* What a win pays, on the lobby's mode cards and on the party panel's chosen game.
   Gold, and only where there is money: the brand guide reserves --brand-gold for coins, trophies
   and Super Boost precisely so it keeps meaning that, and Classic — which has no winner — gets the
   muted treatment instead of a gold line saying there is nothing to win.
   The literal fallback matches the one already on .passkey-row-flag: the browser has never defined
   the token, and inventing it here would put the definition somewhere nobody would look for it. */
/* Scoped through .lobby-mode, and that is the whole of the Classic card's bug rather than a style
   preference. `.lobby-mode span` is (0,1,1) and beat the bare `.lobby-mode-reward` at (0,1,0), so
   the reward line was never `display: flex` — it was the block that rule gives every other span on
   the card. Which left the coin mark an INLINE box, where `width: 12px; height: 12px` do nothing:
   it painted 1.6px wide and 10.9px tall with a 1.5px border and a 50% radius, and what a player
   saw before "No win reward" was a stray `|`. Measured in the page, not read off the file.
   `.lobby-mode.is-active span` at (0,2,1) was winning the colour the same way, which is why
   Classic's line was the mode accent — green — on the card you had selected and grey on the one
   you had not. Same line, two colours, neither of them the one this rule asks for. */
.lobby-mode .lobby-mode-reward, .party-win-reward { display: flex; align-items: center; gap: 6px; margin-top: 6px; font-size: 12.5px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.25; }
.lobby-mode .lobby-mode-reward.is-paid, .party-win-reward.is-paid { color: var(--brand-gold, #ffc83d); }
.lobby-mode .lobby-mode-reward.is-unpaid, .party-win-reward.is-unpaid { color: var(--muted, #77738f); font-weight: 700; }

/* Classic's mark, drawn as an empty ring rather than a coin — the absence of a reward said in the
   shape of one. Without it the grey line was the only unanchored text on either card and read as an
   element that had failed to load rather than as the counterpart to "Win 25 coins". The app's mode
   card does exactly the same thing. */
.lobby-mode .lobby-mode-reward.is-unpaid .coin-mark { background: none; border: 1.5px solid currentColor; box-shadow: none; opacity: .75; }
.lobby-mode .lobby-mode-reward .coin-mark, .party-win-reward .coin-mark { width: 12px; height: 12px; flex: none; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #fff3a6, var(--brand-gold, #ffc83d) 46%, #c78305); box-shadow: 0 0 8px rgba(255, 200, 61, .4); }

/* The party panel's is a full sentence on a full-width row, so it wraps rather than being clipped
   and sits clear of the Royale helper under it. */
.party-win-reward { margin-top: 10px; font-size: 13px; }
.party-win-reward:empty { display: none; }

.team-deathmatch {
  min-height: 98px;
  margin: 14px 0 0;
  padding: 16px 18px;
  gap: 15px;
  border-color: var(--lobby-border);
  border-radius: var(--lobby-radius-button);
  background: rgba(8, 18, 16, .88);
  box-shadow: 0 14px 34px rgba(0, 0, 0, .2);
  transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
}

.tdm-main strong { color: var(--lobby-text); font-size: 18px; font-weight: 850; text-transform: uppercase; }
.tdm-main small { color: var(--lobby-text-secondary); font-size: 15px; }
.tdm-arrow { color: var(--lobby-green); font-size: 30px; }

.lobby-play {
  /* position and overflow are for the travelling band below: it is absolutely positioned to this
     button and clipped to its 22px radius, so the light never runs off the corners. */
  position: relative;
  display: inline-flex;
  min-height: 100px;
  margin: 0;
  padding: 18px 22px;
  overflow: hidden;
  align-items: center;
  justify-content: center;
  gap: 12px;
  border: 0;
  border-radius: var(--lobby-radius-button);
  background: linear-gradient(90deg, var(--lobby-green-bright), var(--lobby-green) 55%, var(--lobby-green-dark)) !important;
  color: var(--cta-ink) !important;
  box-shadow: 0 0 14px color-mix(in srgb, var(--lobby-green) 35%, transparent), 0 0 32px color-mix(in srgb, var(--lobby-green) 15%, transparent) !important;
  font-size: clamp(28px, 4vw, 34px);
  font-style: italic;
  font-weight: 900;
  text-transform: uppercase;
  animation: lobby-play-ready 5.5s ease-in-out infinite;
}

/* The app's twin, rule for rule: Home.razor.css `.lobby-card .play-online::before`, the same
   --cta-sheen token, the same cta-drift keyframe at the same 11s. A band of the mode's own light
   drifting across the fill, so the two colours on this button do something rather than sitting
   flat.

   The colour is a token and never a literal. That is not style — a hard-coded colour inside an
   animation silently beats the base property, and this repo shipped exactly that on exactly this
   element: @keyframes play-ready-glow held a purple, so Classic's green button glowed purple and
   --cta-glow never reached the screen. This keyframe cannot repeat it, because it animates only a
   transform and names no colour at all. */
.lobby-play::before {
  position: absolute;
  /* TWO button-widths wide, carrying two identical bands exactly one button apart, so sliding it
     by half its own width lands band two precisely where band one was and the loop has no seam.
     The first attempt was one band the button's own size, swept off one edge and back on the
     other: measured, it was on screen for 2.9s of an 11s cycle and the button was flat for the
     other 6.1 — which is the thing being fixed, not a smaller version of it. */
  inset: 0 auto 0 0;
  width: 200%;
  pointer-events: none;
  background: linear-gradient(100deg,
      transparent 0%, var(--cta-sheen) 12.5%, transparent 25%,
      transparent 50%, var(--cta-sheen) 62.5%, transparent 75%, transparent 100%);
  /* Transform only, so the compositor owns it and the main thread never repaints the button. This
     lobby is drawn over a live arena running bots at 60 Hz, and it was measured there rather than
     assumed: a six-second devtools trace records 0 Paint events and one RasterTask, which is what
     the button costs with no animation on it at all. The obvious alternative — one band moved by
     animating background-position — costs 2176 Paint events and 231ms of paint over the same six
     seconds, about two repaints of the button every frame to draw the same picture somewhere else.
     That is why the band is a transformed layer and not a moved background. */
  will-change: transform;
  animation: cta-drift 11s linear infinite;
  content: "";
}

/* The button's own children are static, so without this the band — a positioned pseudo-element —
   would paint OVER the label rather than under it. The app states the same thing on
   `.play-online span`. */
.lobby-play svg, .lobby-play span { position: relative; z-index: 1; }

@keyframes lobby-play-ready {
  0%, 8%, 100% { transform: translateY(0); }
  3% { transform: translateY(-4px); }
  5.5% { transform: translateY(-1px); }
}

/* Slow, even, and endless: 11s is exactly twice the 5.5s hop above, so the two lock in phase
   instead of beating against each other at some third period nobody chose, and one band takes the
   whole 11s to cross one button width — about 30px a second, under the speed at which something in
   the corner of your eye asks to be looked at.

   LINEAR, deliberately. ease-in-out was tried and is wrong here: it crosses the middle fastest, and
   the middle is the part you actually see, so a sweep meant to read as light moving over a surface
   instead read as a shimmer firing. Light crossing a surface travels evenly.

   -50% is exact rather than eyeballed: it is one full period of the background above, so the last
   frame of the cycle is the first frame of the next. Measured rather than assumed — across the wrap
   the picture changes by 0.04 of 255 on average, where an ordinary 250ms step of the same animation
   changes it by 1.4, so the restart is thirty times smaller than the motion it hides inside. The
   residue is the 10deg lean: a tilted gradient's period is not exactly the element's width, which
   leaves about a quarter of one percent of the pixels off by more than 4/255, over half of them on
   the button's own antialiased edge. Straighten the gradient to 90deg and it goes to zero, at the
   cost of the lean. */
@keyframes cta-drift {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

.lobby-play svg { width: 30px; height: 30px; fill: currentColor; stroke: currentColor; }
.lobby-solo { display: inline-flex; min-height: 74px; margin: 0; padding: 14px 20px; align-items: center; justify-content: center; gap: 10px; border: 1px solid rgba(117, 255, 63, .22); border-radius: var(--lobby-radius-button); background: rgba(6, 15, 13, .9); color: var(--lobby-text); font-size: 17px; font-weight: 800; text-transform: uppercase; }
.lobby-solo { background: var(--theme-lobby-surface, rgba(6, 15, 13, .9)); }
.lobby-solo svg { width: 24px; height: 24px; fill: none; stroke: var(--lobby-green); stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }

.rounds-strip { min-height: 72px; margin: 0; padding: 15px 18px; border-color: var(--lobby-border); border-radius: var(--lobby-radius-button); background: rgba(8, 18, 16, .86); color: var(--lobby-text-secondary); font-size: 14px; font-weight: 800; text-transform: uppercase; }
.rounds-strip-chevron { color: var(--lobby-green); font-size: 14px; }
/* Brand green, not --lobby-green: this dot means "live now", and live does not change colour
   because the player is looking at the Royale card. The app's .live-dot is pinned the same way. */
.rounds-pip { background: var(--brand-green); box-shadow: 0 0 10px rgba(99, 255, 72, .7); }
.rounds-drawer { margin: 0; padding: 18px; border: 1px solid var(--lobby-border); border-radius: var(--lobby-radius-card); background: rgba(8, 18, 16, .94); }

.lobby-chips { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin: 0; }
.lobby-chip { --quick-accent: var(--lobby-green); display: flex; min-width: 0; min-height: 140px; padding: 14px 8px; flex-direction: column; align-items: center; justify-content: center; gap: 7px; border: 1px solid var(--lobby-border); border-radius: var(--lobby-radius-button); background: rgba(8, 18, 16, .82); color: var(--lobby-text-secondary); text-align: center; }
.lobby-chip:nth-child(1) { --quick-accent: var(--lobby-pink); }
.lobby-chip:nth-child(2) { --quick-accent: var(--lobby-blue); }
.lobby-chip:nth-child(3) { --quick-accent: var(--lobby-yellow); }
.lobby-chip svg { width: 34px; height: 34px; color: var(--quick-accent); fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2; }
.lobby-chip img { width: 52px; height: 52px; object-fit: contain; filter: drop-shadow(0 0 8px color-mix(in srgb,var(--quick-accent) 28%,transparent)); }
.lobby-chip strong { color: var(--lobby-text); font-size: 14px; font-weight: 850; text-transform: uppercase; }
.lobby-chip small { color: var(--lobby-text-muted); font-size: 12px; }
.lobby .status { color: var(--lobby-text-secondary); }
.lobby-legal { color: var(--lobby-text-muted); }

@media (hover: hover) {
  .lobby-mode:hover, .team-deathmatch:hover, .rounds-strip:hover, .lobby-chip:hover { transform: translateY(-1px); border-color: var(--lobby-border-strong); background-color: rgba(12, 23, 21, .94); }
  .lobby-play:hover { animation: none; transform: translateY(-1px); filter: brightness(1.06); }
  .lobby-solo:hover { transform: translateY(-1px); border-color: var(--lobby-green); background: var(--lobby-surface-2); }
}

/* Final light-theme cascade: all neutral lobby furniture follows the pastel surface. */
:root[data-theme-tone="light"] {
  --panel: rgba(255, 255, 255, .90);
  --panel-strong: rgba(255, 255, 255, .96);
  --panel-soft: rgba(255, 255, 255, .72);
  --panel-border: rgba(23, 32, 42, .18);
  --control-surface: rgba(255, 255, 255, .86);
  --control-hover: rgba(23, 32, 42, .06);
  --accent-ink: #17202A;
  --ui-accent: var(--brand-violet);
  --ui-accent-ink: #ffffff;
  --lobby-surface-1: rgba(255, 255, 255, .88);
  --lobby-surface-2: rgba(255, 255, 255, .94);
  --lobby-surface-3: rgba(255, 255, 255, .97);
  --lobby-border: rgba(23, 32, 42, .18);
  --lobby-text: #17202A;
  --lobby-text-secondary: #59647D;
  --lobby-text-muted: #6D7890;
}

:root[data-theme-tone="light"] .season-strip,
:root[data-theme-tone="light"] .rounds-strip,
:root[data-theme-tone="light"] .lobby-nav,
:root[data-theme-tone="light"] .lobby-solo,
:root[data-theme-tone="light"] .team-deathmatch,
:root[data-theme-tone="light"] .lobby-chip {
  background: var(--lobby-surface-1) !important;
  color: var(--lobby-text) !important;
  border-color: var(--lobby-border) !important;
  box-shadow: 0 10px 28px rgba(23, 32, 42, .08);
}

:root[data-theme-tone="light"] .lobby-nav-item,
:root[data-theme-tone="light"] .season-strip-chevron {
  color: var(--lobby-text-secondary) !important;
}

/* Light-theme chrome must win over the legacy dark lobby block above. Keep this at the end of the
   file so a pastel arena never leaves a charcoal season door, live-round row, or bottom bar behind. */
:root[data-theme-tone="light"] {
  --panel: rgba(255, 255, 255, .90);
  --panel-strong: rgba(255, 255, 255, .96);
  --panel-soft: rgba(255, 255, 255, .72);
  --panel-border: rgba(23, 32, 42, .18);
  --control-surface: rgba(255, 255, 255, .86);
  --control-hover: rgba(23, 32, 42, .06);
  --accent-ink: #17202A;
  --lobby-surface-1: rgba(255, 255, 255, .88);
  --lobby-surface-2: rgba(255, 255, 255, .94);
  --lobby-surface-3: rgba(255, 255, 255, .97);
  --lobby-border: rgba(23, 32, 42, .18);
  --lobby-text: #17202A;
  --lobby-text-secondary: #59647D;
  --lobby-text-muted: #6D7890;
}

:root[data-theme-tone="light"] .season-strip,
:root[data-theme-tone="light"] .rounds-strip,
:root[data-theme-tone="light"] .lobby-nav,
:root[data-theme-tone="light"] .lobby-solo,
:root[data-theme-tone="light"] .team-deathmatch,
:root[data-theme-tone="light"] .lobby-chip {
  background: var(--lobby-surface-1) !important;
  color: var(--lobby-text) !important;
  border-color: var(--lobby-border) !important;
  box-shadow: 0 10px 28px rgba(23, 32, 42, .08);
}

:root[data-theme-tone="light"] .lobby-nav-item,
:root[data-theme-tone="light"] .season-strip-chevron {
  color: var(--lobby-text-secondary) !important;
}

/* A press must LOOK pressed (brand guide §6). This was a lone `scale(.99)` — the guide names that
   exact value as the thing you cannot see on a control your thumb is covering. The app's rule does
   four things at once and this is the same shape: it drops, it darkens, its border lights up and
   its inner glow closes in, in 60ms. */
.lobby button:active {
  animation: none;
  transform: translateY(0) scale(.97);
  filter: brightness(.88);
  border-color: var(--lobby-border-strong);
  box-shadow:
    0 4px 12px rgba(0, 0, 0, .4),
    inset 0 0 30px color-mix(in srgb, var(--mode-accent, var(--lobby-green)) 22%, transparent),
    inset 0 2px 10px rgba(0, 0, 0, .45);
  transition-duration: 60ms;
}
/* Focus rings are brand, everywhere, in both clients (brand guide §1) — a keyboard user should
   not have to learn a new ring colour because they chose Royale on the mode picker. This tracked
   --lobby-green, so the ring was green in Classic and purple in Royale. */
.lobby button:focus-visible, .lobby a:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 3px; }

@media (max-width: 700px) {
  .site-brand { --wordmark: clamp(27px, 8vw, 38px); top: calc(10px + var(--safe-top)) !important; left: max(14px, calc(var(--safe-left) + 14px)) !important; min-height: 88px !important; }
  .brand-lockup small { font-size: 9px; }
  .account-menu-toggle { top: calc(18px + var(--safe-top)) !important; right: max(70px, calc(var(--safe-right) + 70px)) !important; max-width: 48vw; min-height: 50px !important; }
  .quick-settings-toggle { top: calc(18px + var(--safe-top)) !important; right: max(14px, calc(var(--safe-right) + 14px)) !important; width: 50px !important; height: 50px !important; }
  .menu-shell { --lobby-top: 118px; --shell-top: var(--lobby-top); padding-top: calc(var(--lobby-top) + var(--safe-top)); padding-inline: max(14px, var(--safe-left)) max(14px, var(--safe-right)); }
  .lobby-controls { gap: 14px; }
  .lobby-modes { gap: 12px; }
  .lobby-mode { min-height: 180px; padding: 16px 8px; }
  .lobby-mode-icon { width: 60px; height: 60px; }
  .lobby-mode-icon svg { width: 36px; height: 36px; }
  .lobby-mode strong { font-size: clamp(23px, 7vw, 28px); }
  .lobby-mode .lobby-mode-subtitle { font-size: 11px; }
  .lobby-mode .lobby-mode-badge { padding-inline: 7px; font-size: 9px; }
  .team-deathmatch { min-height: 92px; }
  .tdm-main strong { font-size: 14px; }
  .tdm-main small { font-size: 11px; }
  .lobby-play { min-height: 82px; font-size: 26px; }
  .lobby-solo { min-height: 70px; font-size: 15px; }
  .lobby-chip { min-height: 120px; padding-inline: 4px; }
  .lobby-chip strong { font-size: 11px; }
  .lobby-chip small { font-size: 9.5px; }
}

@media (max-width: 400px) {
  /* The tagline goes and the BALANCE STAYS, which is the reverse of what this rule used to say.
     The pill lived inside the account capsule then, and on a 320px screen the capsule, the cog and
     the wordmark did not fit — so the number went. The capsule is gone from this screen now (the
     bar's PROFILE is its door), and what is left in the corner is a bell and a balance, which fit.
     A player who cannot see what they have is a player who cannot decide whether to spend it. */
  .brand-lockup small { display: none !important; }
  .lobby-mode { min-height: 166px; }
  .lobby-mode-icon { width: 50px; height: 50px; }
}

/* Final light-theme cascade. */
:root[data-theme-tone="light"] {
  --panel: rgba(255,255,255,.90);
  --panel-strong: rgba(255,255,255,.96);
  --panel-soft: rgba(255,255,255,.72);
  --panel-border: rgba(23,32,42,.18);
  --control-surface: rgba(255,255,255,.86);
  --control-hover: rgba(23,32,42,.06);
  --accent-ink: #17202A;
  --lobby-surface-1: rgba(255,255,255,.88);
  --lobby-surface-2: rgba(255,255,255,.94);
  --lobby-surface-3: rgba(255,255,255,.97);
  --lobby-border: rgba(23,32,42,.18);
  --lobby-text: #17202A;
  --lobby-text-secondary: #59647D;
  --lobby-text-muted: #6D7890;
}
:root[data-theme-tone="light"] .season-strip,
:root[data-theme-tone="light"] .rounds-strip,
:root[data-theme-tone="light"] .lobby-nav,
:root[data-theme-tone="light"] .lobby-solo,
:root[data-theme-tone="light"] .team-deathmatch,
:root[data-theme-tone="light"] .lobby-chip {
  background: var(--lobby-surface-1) !important;
  color: var(--lobby-text) !important;
  border-color: var(--lobby-border) !important;
  box-shadow: 0 10px 28px rgba(23,32,42,.08);
}
:root[data-theme-tone="light"] .lobby-nav-item,
:root[data-theme-tone="light"] .season-strip-chevron { color: var(--lobby-text-secondary) !important; }
@media (prefers-reduced-motion: reduce) {
  .lobby *, .lobby *::before, .lobby *::after { animation: none !important; transition-duration: .01ms !important; }

  /* Take away the MOVEMENT, not the FEEDBACK (brand guide §6). Killing the transition does not
     kill the transform, so without this a reader who asked for less animation was the one reader
     still being moved — and killing the whole press instead would leave them with a button that
     says nothing when pressed, which is worse for them, not better. The colour, the border and
     the shadow all still change; the control just no longer travels to do it.
     The app's Home.razor.css has this same gap; this is the shape its components use. */
  .lobby button:active,
  .lobby-solo:active,
  .team-deathmatch:active { transform: none; }

  /* The CTA's travelling band goes entirely rather than stopping where it happens to be. Killing
     the animation alone would leave it parked at translateX(-100%) — invisible, which is the right
     answer by accident; stop it anywhere else and a sweep of light becomes a stripe across the
     button, a decoration nobody chose. Stated rather than inferred, so the next person to touch
     the keyframe does not have to work out where it lands. The app says the same thing in the same
     place. What goes is only the shimmer: the fill, the ink, the glow and the press all stay. */
  .lobby-play::before { display: none; }
}

/* Final selected-mode and density overrides must follow the premium lobby block above. */
.lobby.map-royale {
  /* Brand purple, named. This was `var(--danger, #b949ff)` — "danger, or purple if danger is
     missing" — and --danger is NEVER missing, so the fallback never fired and choosing Royale
     turned the Play button, the Browse link and the live dot RED (#ff4d6d, measured) while the
     Royale card beside them stayed purple. The app's tokens.css records shipping this exact bug
     and fixing it; the browser twin was never updated. */
  --cta-ink: #ffffff;
  --lobby-green: var(--brand-purple);
  --lobby-green-bright: color-mix(in srgb, var(--brand-purple) 78%, white);
  --lobby-green-dark: color-mix(in srgb, var(--brand-purple) 78%, black);
  --lobby-border-strong: color-mix(in srgb, var(--brand-purple) 55%, transparent);

  /* Royale's drifting band DARKENS, where Classic's in :root lightens, and the asymmetry is the
     point. This button is white on purple; Classic is near-black on green. A sweep of white here
     would be a sweep of contrast loss, and this fill has none to give — its pale end,
     color-mix(--brand-purple 78%, white), measures 2.77:1 under white text, which is below even
     the 3:1 the guide allows type this size and weight. So the band is the brand's own VIOLET:
     it pulls the fill back towards the dark end as it passes, and the worst frame in the cycle is
     the unswept button, unchanged. Matches the app's --cta-sheen value for value.

     Declared in both copies of this block deliberately. There are two, they are already identical
     twins for --cta-ink, and only the later one wins — a sheen in just one is a Royale button that
     silently reverts to Classic's pale green the day somebody tidies the duplicate away. */
  --cta-sheen: rgba(122, 40, 255, .45);

  /* Blue against the purple button, and measured the same way for the same reason. Tinted to the
     same purple, the snake and the gradient were one flat mass with only the art's own black
     linework between them — at arm's length, a smudge. The app's Royale token, value for value.
     In BOTH copies of this block, exactly as --cta-sheen above is and for its reason. */
  --cta-cobra: hue-rotate(108deg) saturate(1.12) brightness(.74) contrast(1.12)
    drop-shadow(0 3px 12px rgba(15, 0, 40, .6));
}
.lobby.map-royale .lobby-play { background: linear-gradient(90deg,var(--lobby-green-bright),var(--lobby-green) 55%,var(--lobby-green-dark)); box-shadow: 0 0 14px color-mix(in srgb,var(--lobby-green) 35%,transparent),0 0 32px color-mix(in srgb,var(--lobby-green) 16%,transparent); }
.lobby-mode-classic { --mode-accent: var(--theme-mode-classic, var(--accent)); }
.lobby-mode-icon img { width: 58px; height: 58px; object-fit: contain; filter: hue-rotate(var(--theme-icon-hue, 0deg)) saturate(1.2) drop-shadow(0 0 8px color-mix(in srgb,var(--mode-accent) 30%,transparent)); }
.lobby-mode { min-height: 156px; padding: 14px 12px 12px; }
.lobby-mode-icon { width: 54px; height: 54px; }
.lobby-mode strong { font-size: clamp(22px,5vw,30px); }
/* The CTA is the one control this page exists for, and the artwork on it is measured against
   this number — see .play-cobra. At 78 the striking cobra came out 75px across on a 470px
   button and read as a sticker in the corner rather than as the mark it is on the phone. The
   app's is clamp(84px, 15vh, 126px); this is still well under it. */
.lobby-play { min-height: 96px; }
.lobby-solo { min-height: 60px; }
.rounds-strip { min-height: 58px; }
.team-deathmatch { min-height: 76px; padding-block: 12px; }
@media (max-width:700px) {
  .lobby-mode { min-height: 156px; }
  .team-deathmatch { min-height: 76px; }
  .lobby-play { min-height: 88px; }
  .lobby-solo { min-height: 60px; }
}
.team-deathmatch .friend-stack { position: relative; display: block; width: 62px; height: 42px; flex: 0 0 62px; }
.team-deathmatch .friend-stack i { position: absolute; width: 34px; height: 34px; border: 2px solid #081210; border-radius: 50%; box-shadow: 0 3px 10px rgba(0,0,0,.32); }
.team-deathmatch .friend-stack i:nth-child(1) { left: 0; top: 5px; }
.team-deathmatch .friend-stack i:nth-child(2) { left: 18px; top: 0; }
.team-deathmatch .friend-stack i:nth-child(3) { left: 29px; top: 11px; }
.team-deathmatch .friend-stack i:nth-child(4) { right: -2px; bottom: -2px; width: 23px; height: 23px; }

/* The home lobby is a fixed game dashboard, and the shell it sits in stays unscrolled so the fixed
   wordmark and the account buttons never have cards sliding under them. The dashboard's OWN body
   scrolls instead, which is the arrangement .menu-panel has had since the party lobby was found
   with its last row below the bottom of the screen and `overflow: hidden` above it.

   It used to fit, so it was allowed to be the one panel in the page with no scroller and no cap:
   `transform: scale(.9/.78/.43)` shrank it at short viewport heights instead. Two things were
   wrong with that. A transform does not change layout, so nothing could measure the overflow and
   nothing could scroll to it; and the ladder was losing anyway — a day's worth of new rows (the
   two reward lines, Play with friends, the live-rounds strip, three quick-action cards, Friends
   and the global board) took the lobby to 923px, so at 320x664 the last FOUR strips sat between
   96 and 171px below the fold in Chromium, Firefox and WebKit alike, with nothing a finger could
   scroll. The .43 step is what a mechanism looks like when it has run out: 12.5px type painted at
   5.4px. Measured, all three engines, before this rule existed.

   So: a cap, and a scroller. dvh and not vh for the reason .menu-panel gives — on a phone browser
   `100vh` is the viewport with the address bar hidden, which is not the viewport a player is
   looking at while it is showing. */
body:not(.is-playing):has(#home-panel:not(.is-hidden)) .menu-shell { overflow: hidden; }

#home-panel:not(.is-hidden) .lobby-body {
  max-height: calc(100vh - var(--lobby-top, 142px) - var(--shell-bottom, 18px)
    - var(--safe-top) - var(--safe-bottom));
  max-height: calc(100dvh - var(--lobby-top, 142px) - var(--shell-bottom, 18px)
    - var(--safe-top) - var(--safe-bottom));
  overflow-y: auto;
  overscroll-behavior: contain;
  /* The scroll box clips, and what it would clip is the Play button's 32px glow and the mode
     cards' bloom. The shell's own inline padding is never less than 14px, so the body borrows all
     of it back and the halos live in the margin rather than being cut off square at the column. */
  margin-inline: -14px;
  padding: 0 14px !important;
}

/* Browsing rounds opens a list inside the lobby. Remove the fixed chrome that otherwise overlays
   the join controls and let the shell scroll too, so a long list of matches has the whole page to
   use rather than only the dashboard's own cap. */
body:not(.is-playing):has(#home-panel:not(.is-hidden)):has(#rounds-drawer:not([hidden])) .menu-shell {
  overflow-y: auto;
  --lobby-top: 18px;
  --shell-top: var(--lobby-top);
  padding-top: calc(var(--lobby-top) + var(--safe-top));
}
body:has(#rounds-drawer:not([hidden])) .site-brand,
body:has(#rounds-drawer:not([hidden])) .account-menu-toggle,
body:has(#rounds-drawer:not([hidden])) .quick-settings-toggle { display: none !important; }

@media (max-height: 850px) {
  body:not(.is-playing):has(#home-panel:not(.is-hidden)) .menu-shell {
    --lobby-top: 112px;
    --shell-top: var(--lobby-top);
    padding-top: calc(var(--lobby-top) + var(--safe-top));
  }
  .site-brand { --wordmark: clamp(29px, 3.5vw, 38px); top: calc(10px + var(--safe-top)) !important; min-height: 72px !important; }
}

@media (max-height: 720px) {
  body:not(.is-playing):has(#home-panel:not(.is-hidden)) .menu-shell {
    --lobby-top: 90px;
    --shell-top: var(--lobby-top);
    padding-top: calc(var(--lobby-top) + var(--safe-top));
  }
  .site-brand { --wordmark: clamp(24px, 3vw, 31px); }
}

@media (max-height: 600px) {
  body:not(.is-playing):has(#home-panel:not(.is-hidden)) .menu-shell {
    --lobby-top: 86px;
    --shell-top: var(--lobby-top);
    padding-top: calc(var(--lobby-top) + var(--safe-top));
  }
}

/* ---------- reporting a player ----------------------------------------------------------------
   The sheet is plain and deliberately not styled like the rest of the arcade: somebody opening it
   is upset, and neon and italics are the wrong register for "tell us what happened". See
   docs/online-safety.md. */

/* The one door to everything a player might want to say about the round.

   It replaced a full-width "Report a player" pill in this slot, which wore the same shape as Play
   again and Return to lobby — three stacked rows of identical weight, one of which was an
   accusation. So this is deliberately the quietest thing on the panel: not full width, no border
   until it is touched, and the dim text colour. Twin of .feedback-link in Results.razor.css. */
.feedback-open {
  display: inline-block;
  min-height: 44px;
  margin: 16px auto 0;
  padding: 12px 28px;
  border: 1px solid transparent;
  border-radius: 22px;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 15px;
  font-weight: 650;
  /* Underlined rather than boxed: without it a dim word under two big buttons reads as a caption
     on them, and a control nobody recognises as a control is a control nobody presses — which for
     the reporting route inside it is a duty quietly dropped. */
  text-decoration: underline;
  text-underline-offset: 4px;
  cursor: pointer;
  transition: transform 60ms ease-out, background-color 60ms ease-out, border-color 60ms ease-out,
              color 60ms ease-out;
}

.feedback-open:hover { color: var(--text); }

/* Four things at once, per the brand guide: the border arrives, the ground darkens, the text comes
   up to full strength, and it drops. Never brightness(), which lightens. */
.feedback-open:active {
  transform: translateY(1px);
  border-color: rgba(255, 255, 255, .16);
  background: rgba(255, 255, 255, .06);
  color: var(--text);
}

.sheet-backdrop {
  position: fixed;
  z-index: 60;
  inset: 0;
  display: grid;
  padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom));
  place-items: center;
  background: rgba(3, 4, 12, .74);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.report-sheet {
  width: min(100%, 460px);
  max-height: 100%;
  padding: 18px;
  overflow-y: auto;
  border: 1px solid var(--panel-border);
  border-radius: 18px;
  background: var(--panel-strong);
}

.report-hint { margin: 4px 0 14px; color: var(--text-dim); font-size: 14px; line-height: 1.4; }
.report-step { display: block; }
.report-players { display: grid; gap: 8px; }

/* Names come off another player's screen, so they are put in the page as TEXT and never as markup
   — and the box they sit in has to survive one that is all one word. */
.report-players button,
.report-reasons button {
  display: block;
  width: 100%;
  padding: 12px 14px;
  overflow-wrap: anywhere;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  background: var(--panel-soft);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.report-players button:hover,
.report-reasons button:hover { border-color: rgba(255, 255, 255, .3); background: rgba(255, 255, 255, .06); }

.report-reasons { display: grid; gap: 8px; margin-bottom: 14px; }
.report-reasons button.is-chosen { border-color: #d83cff; background: rgba(216, 60, 255, .12); }
.report-reasons strong { display: block; font-size: 15px; }
.report-reasons small { display: block; margin-top: 3px; color: var(--text-dim); font-size: 12.5px; line-height: 1.35; }

/* The four grounds that jump the queue say so, because a player choosing one should know it is
   taken seriously rather than wonder whether anybody will look. */
.report-reasons button.is-urgent strong::after {
  content: " · looked at first";
  color: #ffc83d;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
}

.report-detail { display: block; margin-bottom: 14px; }
.report-detail span { display: block; margin-bottom: 6px; color: var(--text-dim); font-size: 13px; }

.report-detail textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  background: var(--panel-soft);
  color: var(--text);
  font: inherit;
  font-size: 14px;
  resize: vertical;
}

.report-actions { display: flex; gap: 10px; }
.report-actions button { flex: 1 1 0; }
.report-foot { margin: 14px 0 0; color: var(--text-dim); font-size: 12px; text-align: center; }

/* --- reporting somebody after the round is over ----------------------------------------------
   The list of games you played, and who was in them. RecentGames.razor is the twin.

   Deliberately plain, like the sheet it opens (brand guide 8): whoever reached this screen is
   upset, and neon and italics are the wrong register for "tell us what happened". Nothing here
   glows, nothing pulses, and the only colour that is not text is the brand accent on a press. */

.recent-games-panel { gap: 12px; }

.recent-games-intro,
.recent-games-note {
  margin: 0;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.45;
}

/* The guest sentence is an aside rather than a warning: nothing has gone wrong and nothing is
   being withheld. An account changes reach, never permission. */
.recent-games-note.is-aside {
  padding-top: 10px;
  border-top: 1px solid var(--panel-border);
  font-size: 13px;
}

.recent-games-list {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The whole row is the button, not a chevron inside it (brand guide 7). 22px because a row is a
   row, and the same 44pt floor every other target in this client takes. */
.recent-game {
  display: grid;
  gap: 4px;
  width: 100%;
  min-height: 44px;
  padding: 12px 14px;
  overflow-wrap: anywhere;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-button);
  background: var(--control-surface);
  color: var(--text);
  font: inherit;
  text-align: left;
}

.recent-game:hover {
  border-color: var(--ui-accent);
  background: var(--control-hover);
}

/* Drop, darken, light the border (brand guide 6). */
.recent-game:active {
  transform: scale(0.99);
  border-color: var(--ui-accent);
  background: var(--control-hover);
}

.recent-game:focus-visible {
  outline: none;
  border-color: var(--ui-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-accent) 34%, transparent);
}

.recent-game-when {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  font-size: 15px;
  font-weight: 800;
}

.recent-game-ago {
  color: var(--text-dim);
  font-size: 12px;
  font-weight: 600;
}

/* The names, as they were worn in that match. Two lines at most: a twenty-snake Royale is a
   paragraph of strangers' names, and the row is a way in rather than the roster itself. */
.recent-game-who {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.4;
}

.recent-game-go {
  color: var(--ui-accent);
  font-size: 12px;
  font-weight: 800;
}

.recent-games-empty {
  display: grid;
  gap: 8px;
  padding: 16px;
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-card);
  background: var(--panel-soft);
}

.recent-games-empty h2 {
  margin: 0;
  font-size: 17px;
  font-weight: 850;
}

.recent-games-empty p {
  margin: 0;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.45;
}

/* The links inside a sentence are underlined and take the text colour rather than the muted one.
   `.privacy-link` is written for a footer row, where being a row of links is the whole context;
   dropped into a paragraph it renders "contact support" as slightly paler grey and nothing else,
   which is state by colour alone (brand guide 7) — and this is the screen where the alternative
   route out has to be findable. */
.recent-games-empty a,
.recent-games-note a {
  color: var(--text);
  font-size: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.recent-games-empty a:hover,
.recent-games-note a:hover {
  color: var(--ui-accent);
}

/* Take away the movement, not the feedback (brand guide 6). */
@media (prefers-reduced-motion: reduce) {
  .recent-game:active { transform: none; }
}

/* --- the season pass ------------------------------------------------------------------------
   The browser twin of SeasonPass.razor.css, ported rung for rung after the phone's redesign, so
   that the two clients are recognisably the same screen. Same layout, same states, same words.

   BRAND TOKENS THROUGHOUT, never theme tokens. This is a menu, not the arena: the player's chosen
   theme decides what a snake looks like, and the fixed brand palette decides what a button looks
   like (brand guide §1). Nothing on this screen shows what a theme looks like, so nothing on it
   has any business reading one — which is why the muted type here is --brand-text-secondary and
   --brand-text-muted rather than the theme's own greys, which go dark on the eight pastel themes.

   Gold appears on the paid column, on the price and on a reward waiting to be collected, and
   nowhere else. That is the brand rule rather than a preference — gold means money and reward, and
   using it decoratively is how a player learns to stop reading it. */

.season-panel {
  /* ---- the two tracks, named by what they mean --------------------------------------------
     The whole information design of this screen is one column you have and one you buy, so the
     pair is a token pair rather than two colours typed in forty places.

     --season-paid is the brand gold, unchanged. A bought track is money and reward, which is
     exactly what gold is reserved for.

     --season-free is a colour the brand did not have, and the choice was deliberate — please do
     not "fix" it back. The guide reserves green (#63FF48) for "Classic mode" and "live now". This
     mint sits about fifty degrees of hue away from it (≈160° against ≈110°): it reads as aqua
     rather than as acid green, and it never shares a screen with either reserved meaning, because
     the season pass shows no mode card and no live dot. The alternative considered was giving FREE
     no colour at all and letting gold-against-plain carry the distinction; it was rejected because
     the free track is half of this screen and a track with no identity is a track nobody reads.

     It is a fixed value here and is never read off the arena's own accent, for the reason the
     brand guide opens with — a player on Blush would get a pink FREE label. And the colour is
     never the only thing carrying the state: every cell spells out FREE or PASS in words, and a
     locked one carries a padlock.

     The phone's SeasonPass.razor.css carries this same pair with this same value.
     SeasonRewardArtParityTests holds the two spellings equal, because a colour shared between two
     clients that is not pinned by a test is a colour that agrees today. */
  --season-free: #42e8b4;
  --season-free-ink: color-mix(in srgb, var(--season-free) 68%, var(--brand-text));
  --season-paid: var(--brand-gold);

  /* Brand accents stay fixed; surfaces follow the selected theme's tone. A literal navy here was
     the dark-box-on-pastel bug shared with the iOS season pass. */
  --season-surface: var(--brand-surface);
  --season-line: var(--brand-border);

  gap: 14px;
}

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

/* Back, name and balance on one line with the dates under them, rather than four things in a row.
   The name is the biggest word on the screen and the balance is the number a player checks before
   reading a price, so they are the two that get the top line. The refresh control is the browser's
   own — the phone reloads the season on the way in and has nothing to press — and it is the last
   thing in the row because it is the least of them. */
.season-head {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 8px;
}

.season-head h1 {
  margin: 0;
  font-size: 30px;
  font-weight: 900;
  font-style: italic;
  letter-spacing: 0.01em;
  color: var(--brand-text);

  /* A season's name is a game word, and a game word never wraps: broken over two lines it is not
     the name, it is the name with a fault in it. Something else gives way instead. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.season-when {
  grid-column: 2 / -1;
  grid-row: 2;
  margin: 2px 0 0;

  /* Room kept on the right for the refresh control, which shares this row. Reserved by padding
     rather than by a column, so the line can use the whole width the header has and the two never
     have to negotiate: a subtitle boxed into the title's own column wrapped "ends 10/13/2026" onto
     a second line at 375px. */
  padding-right: 42px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--brand-text-secondary);
}

/* The deadline, and the only part of that line a player does anything about. The brand accent
   rather than gold: gold is money and a date is not a price, and a season ending is not a reward
   (brand guide §1). The word "ends" already says what it is, so nothing is carried by the colour
   alone. SeasonPass.razor.css's twin. */
.season-ends {
  color: var(--ui-accent);
  font-weight: 800;
}

.season-back {
  /* 44px, and the whole square is the target rather than the chevron drawn in it. */
  min-width: 44px;
  min-height: 44px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--season-line);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.04);
  font: inherit;
  font-size: 24px;
  line-height: 1;
  color: var(--ui-accent);
  cursor: pointer;
}

.season-back:focus-visible,
.season-coin-pill:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }

/* On the second row, under the balance. It is the browser's own control — the phone reloads the
   season on the way in and has nothing to press — and a fourth thing in the top row is what took
   the season's NAME down to "First…" on a 375px phone: the row is a back target, a game word, a
   balance and this, in 311px of panel. Demoted rather than dropped, because it works. */
.season-refresh {
  grid-column: 3;
  grid-row: 2;
  justify-self: end;
  width: 34px;
  height: 34px;
  min-height: 34px;
}

/* The balance, in the panel's own header, at every width.

   The lobby's `.top-coin-pill` used to live inside the account toggle and vanish below 700px, so on
   every phone this screen — the one with the biggest price in the game on it — had no balance
   anywhere on it, and above 700px this one went away instead because the chrome pill answered the
   same question. Both halves of that arrangement are gone: the pill is its own control in the top
   corner now, and it shows only on the four screens the navigation bar covers, of which this is not
   one. So this is the only balance on the season panel at any width, which is what it has to be —
   a screen for spending with no balance on it is the fault this was written to fix.
   AccountPill.razor is its twin, ShowAdd and all. */
.season-coin-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  padding: 6px 12px 6px 9px;
  border: 1px solid var(--brand-border);
  border-radius: 999px;
  background: var(--brand-surface);
  color: var(--brand-gold);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .34), inset 0 1px 0 rgba(255, 255, 255, .05);
  font: inherit;
  cursor: pointer;
}

.season-coin-mark {
  width: 18px;
  height: 18px;
  flex: 0 0 18px;
  border-radius: 50%;
  /* The gradient is the coin; there is no glow around it. ShopParityTests holds gold to meaning
     money, and a glow is gold spreading past the thing that is money — which is the habit the
     rule exists to stop. The coin reads as gold without it. */
  background: radial-gradient(circle at 35% 30%, #fff3a6, var(--brand-gold) 46%, #c78305);
}

.season-coin-count { font-size: 15px; font-weight: 850; font-variant-numeric: tabular-nums; }

/* The top-up affordance, shown only because this screen is about spending. Inside the pill's own
   button, so it adds an invitation and not a second target. */
.season-coin-add {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: rgba(255, 200, 61, .16);
  font-size: 15px;
  font-weight: 900;
  font-style: normal;
  line-height: 1;
}

/* An account in debt after a refund. Gold is reserved for money and reward and a debt is neither —
   a gold "0" reads as a balance somebody simply spent, which is the one wrong thing this state
   could say. The coin goes darker than the text, not lighter: a mid-tone disc becomes the heaviest
   thing in the badge and the eye lands on it instead of the number. */
.season-coin-pill.is-owing { color: #aaa5c5; }
.season-coin-pill.is-owing .season-coin-add { background: rgba(255, 255, 255, .07); }
.season-coin-pill.is-owing .season-coin-mark {
  background: radial-gradient(circle at 35% 30%, #4a4560, #322e49 46%, #232036);
  box-shadow: none;
}

/* ---- where the player is -------------------------------------------------------------------- */

.season-standing {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 14px;
  margin: 0;
  padding: 14px 16px;
  border: 1px solid var(--season-line);
  border-radius: 26px;
  background: var(--season-surface);
}

.season-standing-copy { display: grid; gap: 9px; min-width: 0; }

.season-standing-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.season-tier-label { font-size: 19px; font-weight: 850; color: var(--brand-text); }
.season-xp { font-size: 14px; font-weight: 900; color: var(--brand-gold); white-space: nowrap; }

.season-bar {
  height: 9px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.10);
  overflow: hidden;
}

.season-bar > span {
  display: block;
  height: 100%;
  background: var(--brand-gradient);
}

.season-next { font-size: 12.5px; color: var(--brand-text-secondary); }

/* The tier crest. Two hexagons rather than a border, because a CSS border follows the box and not
   the clip — a bordered element that is then clip-pathed loses three of its edges. The outer one is
   the brand gradient and the inner one is the ground, so what shows between them is a 3px rim. */
.season-badge {
  position: relative;
  display: block;
  width: 62px;
  height: 70px;
  flex: 0 0 62px;
}

.season-badge-ring,
.season-badge-face {
  position: absolute;
  clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}

.season-badge-ring {
  inset: 0;
  background: linear-gradient(160deg, var(--brand-magenta), var(--ui-accent) 45%, var(--brand-violet));
}

.season-badge-face {
  inset: 3px;
  display: grid;
  place-items: center;
  align-content: center;
  background: linear-gradient(180deg, #241243, #140a28);
}

.season-badge-face b {
  font-size: 26px;
  font-weight: 900;
  line-height: 1;
  color: #fff;

  /* Tabular, so 4 and 44 share a centre line. A crest that shifts as the season goes on is a crest
     that looks broken at tier 10. */
  font-variant-numeric: tabular-nums;
}

/* A pip above the number, so the crest reads as a rank insignia rather than as a numbered tile.
   Clipped rather than a glyph: an emoji star is a font the browser may not have. */
.season-badge-star {
  width: 9px;
  height: 9px;
  margin-bottom: 2px;
  background: var(--brand-gold);
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}

/* The glow belongs to the ring, not to the badge, or the shadow is clipped away with everything
   else. Restrained and static — the arena behind every menu is already moving (guide §6). */
.season-badge::after {
  position: absolute;
  z-index: -1;
  inset: -6px;
  content: "";
  border-radius: 50%;
  background: radial-gradient(circle, rgba(216, 60, 255, 0.34), transparent 68%);
}

/* ---- the offer ------------------------------------------------------------------------------ */

.season-offer {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 14px;
  padding: 14px 16px;
  border-radius: 26px;
  background: var(--season-surface);

  /* Gold, and a full stop rather than a hairline. This is the only card on the page that is asking
     for money, and the border is what says so before a word is read. */
  border: 1.5px solid rgba(255, 200, 61, 0.55);
  box-shadow: 0 0 26px rgba(255, 200, 61, 0.10), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

.season-ticket { display: block; width: 50px; flex: 0 0 50px; }
.season-ticket svg { display: block; width: 100%; height: auto; }
.season-ticket .stub { fill: #f0b429; stroke: #7a4d05; stroke-width: 1.5; }
.season-ticket .crown { fill: #fff3d6; }
.season-ticket .gem { fill: #7a4d05; }

.season-offer-copy { display: grid; gap: 3px; min-width: 0; }
.season-offer-copy strong { font-size: 17px; font-weight: 900; color: var(--brand-gold); }
.season-offer-copy small { font-size: 12px; line-height: 1.35; color: var(--brand-text-secondary); }

/* The price is the largest thing on the card, and the call to action is the small print under it.
   The other way round is a button people press to find out what it costs. */
.season-buy {
  display: grid;
  justify-items: center;
  gap: 1px;
  min-width: 118px;
  min-height: 56px;
  padding: 8px 16px;
  border: 0;
  border-radius: 20px;
  background: var(--brand-gradient);
  color: var(--ui-accent-ink);
  font: inherit;
  cursor: pointer;
  box-shadow: 0 0 26px rgba(216, 60, 255, 0.34), inset 0 1px 0 rgba(255, 255, 255, 0.26);
  transition: transform 60ms ease, box-shadow 160ms ease;
}

.season-buy-price {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 25px;
  font-weight: 900;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.season-buy-coin {
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #fff3a6, var(--brand-gold) 46%, #c78305);
}

.season-buy-call {
  font-size: 10.5px;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  opacity: 0.9;
}

.season-buy:active { transform: translateY(1px); box-shadow: 0 0 14px rgba(216, 60, 255, 0.22); }
.season-buy:disabled { filter: saturate(0.35) brightness(0.7); cursor: default; }
.season-buy:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 3px; }

/* A pass already bought gets a state, not a greyed-out purchase control. Said in a word as well as
   in gold and a tick, because a colour is not a state a colour-blind player can read. */
.season-owned {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin: 0;
  min-height: 48px;
  padding: 0 18px;
  border: 1.5px solid var(--season-paid);
  border-radius: 999px;
  background: rgba(255, 200, 61, 0.14);
  color: var(--season-paid);
  font-size: 14px;
  font-weight: 900;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}

.season-owned-tick {
  position: relative;
  width: 15px;
  height: 15px;
}

.season-owned-tick::after {
  position: absolute;
  left: 5px;
  top: 0;
  width: 5px;
  height: 11px;
  border: 2.5px solid currentColor;
  border-top: 0;
  border-left: 0;
  content: "";
  transform: rotate(40deg);
}

.season-offer.is-owned {
  box-shadow: 0 0 26px rgba(255, 200, 61, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

/* A refusal and a receipt, told apart. app.js has toggled `is-bad` on this line since the panel
   shipped and nothing in this sheet ever styled it, so "You need more coins" was drawn in the same
   muted grey as "The pass is yours" — the phone has said them in two colours all along. Never
   colour alone: the sentence itself is the message, and this only makes it easier to find. */
.season-status { margin: 0; font-size: 13px; color: var(--brand-text-secondary); }
.season-status.is-bad { color: #ff6b81; }
.season-status.is-good { color: var(--brand-green); }

/* ---- the two halves ------------------------------------------------------------------------- */

.season-tabs { display: flex; gap: 10px; }

.season-tab {
  display: flex;
  flex: 1;
  align-items: center;
  justify-content: center;
  gap: 9px;
  min-height: 48px;
  border: 1px solid var(--season-line);
  border-radius: 18px;
  background: rgba(255, 255, 255, 0.03);
  color: var(--brand-text-secondary);
  font: inherit;
  font-size: 15px;
  font-weight: 800;
  cursor: pointer;
}

/* State is never colour alone: the current tab is tinted AND heavier AND outlined AND underlined.
   Neon is not a state a colour-blind player can read, and aria-pressed is not one a sighted player
   can. */
.season-tab.is-active {
  position: relative;
  border-color: var(--ui-accent);
  background: rgba(216, 60, 255, 0.18);
  color: var(--brand-primary);
  font-weight: 900;
  box-shadow: 0 0 18px rgba(216, 60, 255, 0.24);
}

.season-tab.is-active::after {
  position: absolute;
  left: 18%;
  right: 18%;
  bottom: -1px;
  height: 2px;
  border-radius: 999px;
  background: var(--ui-accent);
  content: "";
}

.season-tab:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }

/* Two marks rather than two words in different colours. Drawn rather than set in an emoji, which
   is a font the browser is not guaranteed to have. */
.season-tab-mark { display: block; width: 16px; height: 16px; flex: none; }

.season-tab-mark.is-track {
  background:
    linear-gradient(currentColor, currentColor) 0 100% / 3px 8px no-repeat,
    linear-gradient(currentColor, currentColor) 50% 100% / 3px 15px no-repeat,
    linear-gradient(currentColor, currentColor) 100% 100% / 3px 11px no-repeat;
}

.season-tab-mark.is-goals {
  border: 2px solid currentColor;
  border-radius: 50%;
  box-shadow: inset 0 0 0 2px transparent, inset 0 0 0 4px currentColor;
}

.season-track,
.season-challenges {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
}

.season-tier {
  position: relative;
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr) minmax(0, 1fr);
  gap: 6px;
  padding: 6px;
  border: 1px solid var(--season-line);
  border-radius: 22px;
  background: var(--season-surface);
}

/* The row keeps the class name it shipped with — the phone calls the same element
   `.season-tier-row` and its own tier LABEL `.season-tier`, so the two files differ here by a
   suffix. Renaming this one was tried and reverted: `.season-tier.is-current` is named in
   BrandTokenParityTests' hand-written list of browser selectors, which lives in the app's test
   project, and a rename here turns another client's guard red for nothing a player can see. Worth
   unifying, in one change that moves both. */

/* A tier still out of reach is drawn quieter, never faded. The whole track has to be readable
   before anybody pays — that is what "knowable before purchase" means in a stylesheet — and the
   flat `opacity: .62` this row used to carry put every reward past the player's own tier, which is
   most of the season, behind a veil. What recedes is the SURFACE and the rung number; the reward
   names and their artwork stay at full strength, because those are the thing being sold. */
.season-tier.is-ahead { background: color-mix(in srgb, var(--season-surface) 84%, transparent); border-color: var(--season-line); }
.season-tier.is-ahead .season-tier-number { background: color-mix(in srgb, var(--brand-text) 4%, transparent); }
.season-tier.is-ahead .season-tier-number b { color: var(--brand-text-secondary); }

/* Exactly one row wears this, and it has to be findable in a hundred: a neon rim, a glow the other
   rows have none of, and an arrow pointing at it from outside the card. The arrow sits in the
   panel's own left padding, which is scrollable area rather than overflow, so it cannot introduce a
   sideways scroll on the narrowest phone. */
.season-tier.is-current {
  border: 2px solid var(--ui-accent);
  padding: 5px;
  background: linear-gradient(180deg, rgba(216, 60, 255, 0.14), rgba(216, 60, 255, 0.04));
  box-shadow: 0 0 22px rgba(216, 60, 255, 0.34);
}

.season-tier.is-current::before {
  position: absolute;
  left: -13px;
  top: 50%;
  width: 0;
  height: 0;
  border-left: 9px solid var(--ui-accent);
  border-top: 7px solid transparent;
  border-bottom: 7px solid transparent;
  content: "";
  transform: translateY(-50%);
}

.season-tier-number {
  /* align-content, not place-items. Under the narrow rule at the bottom of this block the cell
     spans two rows and grows to about 104px, and implicit grid rows STRETCH — which put the number
     centred in the top half and "tier" centred in the bottom, 38px apart, reading as two unrelated
     labels. A screenshot is what found it, in this client. */
  display: grid;
  align-content: center;
  justify-items: center;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.06);
}

.season-tier-number b { font-size: 18px; font-weight: 900; font-variant-numeric: tabular-nums; color: var(--brand-text); }
.season-tier-number small { font-size: 9px; font-weight: 800; text-transform: uppercase; color: var(--brand-text-muted); }

/* ---- one reward ------------------------------------------------------------------------------ */

.season-reward {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;

  /* Comfortably past the 44px minimum, because the artwork needs the height and because this is
     the control a player presses a hundred times in a season. */
  min-height: 66px;
  padding: 6px 6px 6px 8px;
  border: 1px solid var(--season-line);
  border-radius: 16px;
  background: var(--panel-soft);
  color: var(--brand-text);
  font: inherit;
  text-align: left;
  cursor: default;
}

.season-reward-copy { display: grid; flex: 1 1 auto; gap: 2px; min-width: 0; }

.season-reward em {
  font-style: normal;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.10em;
  text-transform: uppercase;
}

.season-reward strong {
  font-size: 13px;
  font-weight: 700;
  line-height: 1.12;
}

/* The two tracks. Free is mint, paid is gold, and the word is spelled out on both — see the note
   at the top of this block for why FREE is not the brand's reserved green. */
.season-reward.is-free em { color: var(--season-free-ink); }
.season-reward.is-paid em { color: var(--season-paid); }
.season-reward.is-paid { border-color: rgba(255, 200, 61, 0.26); }

/* Ready to take, in the track's own colour rather than in one colour for both — the point of the
   pair is that a player can tell at a glance which column just paid out. */
.season-reward.is-claimable { cursor: pointer; }

.season-reward.is-free.is-claimable {
  border-color: var(--season-free);
  background: rgba(66, 232, 180, 0.13);
  box-shadow: 0 0 16px rgba(66, 232, 180, 0.20);
}

.season-reward.is-paid.is-claimable {
  border-color: var(--season-paid);
  background: rgba(255, 200, 61, 0.14);
  box-shadow: 0 0 16px rgba(255, 200, 61, 0.20);
}

/* Collected. This is the one state that is allowed to recede, because it is the only one that is
   finished — and it still keeps its name at readable weight and carries a tick, so it is never read
   by colour alone. */
.season-reward.is-claimed {
  border-color: var(--season-line);
  background: transparent;
  box-shadow: none;
}

.season-reward.is-claimed strong { color: var(--brand-text-secondary); font-weight: 600; }
.season-reward.is-claimed .season-art { opacity: 0.45; }

/* Behind the pass: desirable, and unavailable. NOT dimmed — a locked reward that fades out is a
   reward nobody is being sold, and `opacity: .55` is what this cell used to carry. It keeps its
   gold label at full strength, keeps its artwork, gains a violet ground that says "this belongs to
   the other column", and carries a padlock. A change of metal, never a dimming. */
.season-reward.is-shut {
  border-color: rgba(216, 60, 255, 0.32);
  background: linear-gradient(180deg, rgba(122, 40, 255, 0.20), rgba(122, 40, 255, 0.07));
}

.season-reward:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }

/* The padlock and the tick sit in the cell's own corner rather than in the row of content, and that
   is a width decision as much as a visual one: two reward columns beside a tier number leave about
   fifty pixels for a name on a 6.1" phone, and a mark taking sixteen of them is the difference
   between "1 Super Boost" on one line and on three. Out of flow, on a disc of the cell's own ground
   so it reads against artwork as well as against the surface. */
.season-reward-lock,
.season-reward-tick {
  position: absolute;
  right: 3px;
  top: 50%;
  display: block;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--panel-strong);
  color: var(--brand-text-secondary);
  transform: translateY(-50%);
}

/* The mark gets a gutter of its own rather than being laid over the artwork. A padlock sitting on
   top of the thing it is locking is the one place this row cannot afford to be ambiguous. */
.season-reward.is-shut,
.season-reward.is-claimed { padding-right: 21px; }

.season-reward-lock::before {
  position: absolute;
  left: 5px;
  top: 3px;
  width: 6px;
  height: 5px;
  border: 1.6px solid currentColor;
  border-bottom: 0;
  border-radius: 4px 4px 0 0;
  content: "";
}

.season-reward-lock::after {
  position: absolute;
  left: 3.5px;
  top: 7.5px;
  width: 10px;
  height: 7.5px;
  border-radius: 2px;
  background: currentColor;
  content: "";
}

/* Collected reads gold rather than grey. A reward that has been taken is still a reward — the
   receding is done by the cell around it, and the tick is the receipt. */
.season-reward-tick { color: var(--season-paid); }
.season-reward.is-free .season-reward-tick { color: var(--season-free); }

.season-reward-tick::after {
  position: absolute;
  left: 6px;
  top: 3px;
  width: 4px;
  height: 8px;
  border: 2px solid currentColor;
  border-top: 0;
  border-left: 0;
  content: "";
  transform: rotate(40deg);
}

/* A press must look pressed, and it must do more than move — under prefers-reduced-motion the
   movement goes and the colour stays, because a press that says nothing is worse for that reader
   rather than better.

   It DARKENS where the phone's copy of this rule brightens (`filter: brightness(1.25)`), and that
   is a deliberate divergence rather than a slip in the port. Every press in this client darkens, it
   is asserted — BrandTokenParityTests fails any `:active` rule with a brightness above 1 and says
   in its own message that this is the rule "in both clients" — and the phone's season pass is the
   one press in either client that does not. Copying it here would have moved the odd one out into
   two files instead of one. The app's rule is the one worth changing. */
.season-reward.is-claimable:active {
  transform: translateY(1px);
  border-color: var(--brand-text);
  filter: brightness(.88);
  transition: 60ms;
}

/* A pointer gets the lift instead, which is a hover and not a press. */
@media (hover: hover) {
  .season-reward.is-claimable:hover { background: rgba(255, 255, 255, 0.07); }
}

@media (prefers-reduced-motion: reduce) {
  .season-reward.is-claimable:active,
  .season-buy:active { transform: none; }

  .season-buy { transition: none; }
}

/* ---- what a tier hands over, drawn ------------------------------------------------------------
   RewardArt.razor.css's twin. Two colour sets, and which one is used is the only thing "locked"
   changes. Gold is money and reward (brand guide §1) and an unlocked coin is exactly that; a coin
   still behind the pass is drawn in the brand violet instead, because gold on something you cannot
   have teaches a player to stop reading gold. It is a change of metal, not a dimming: nothing here
   drops below full opacity, which is the whole difference between "desirable but unavailable" and
   "switched off". */

.season-art {
  --coin-face: #ffd766;
  --coin-edge: #b57d10;
  --coin-pip: #fff6d2;

  --cell-body: var(--brand-violet);
  --cell-window: #4a1d9e;
  --cell-cap: #241040;
  --cell-bolt: var(--brand-gold);

  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  flex: 0 0 46px;
}

/* Still a prize, still lit — a different metal rather than a lower opacity. */
.season-art.is-locked {
  --coin-face: #c79bff;
  --coin-edge: #4d2a8a;
  --coin-pip: #f0e2ff;
}

/* No `overflow: visible` here, and that is not a tidy-up. The face chip draws a head 52 units
   across inside a 116-unit canvas, and the phone's SVG twin spilled a pair of goggles out of this
   tile and over the reward name in the column beside it before it was clipped. */
.season-art > svg,
.season-art > canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* ---- coins ---------------------------------------------------------------------------------- */

.season-art .coins .top { fill: var(--coin-face); }
.season-art .coins .rim { fill: var(--coin-edge); }
.season-art .coins .pip { fill: var(--coin-pip); }

/* The back stack sits behind and reads as further away. Scaled down rather than drawn a second time
   at another size — two sets of coordinates for one pile is two things to keep in step. */
.season-art .coins .back { transform: translate(1px, 3px) scale(0.86); transform-origin: 18px 28px; }

/* ---- a Super Boost charge ------------------------------------------------------------------- */

.season-art .canister .body { fill: var(--cell-body); }
.season-art .canister .window { fill: var(--cell-window); }
.season-art .canister .cap { fill: var(--cell-cap); }
.season-art .canister .sheen { fill: rgba(255, 255, 255, 0.22); }
.season-art .canister .bolt { fill: var(--cell-bolt); }

/* Locked keeps the violet — the canister is already violet, so what changes is that the gold bolt
   stops promising money it cannot pay. */
.season-art.is-locked .canister .bolt { fill: #e6d2ff; }

/* ---- a body pattern -------------------------------------------------------------------------- */

.season-art .skin path {
  fill: none;
  stroke-width: 11;
}

/* Butt caps on the bands so a colour change is a hard edge, exactly as the arena draws it; round
   caps on the ground pass underneath, so the ends of the length are ends of a snake rather than a
   cut-off ribbon. Both are the same path, so they cannot drift apart. */
.season-art .skin .band { stroke-linecap: butt; }
.season-art .skin .ground { stroke-linecap: round; }

/* The catalogue's own keyline, the one all three renderers outline an accessory with. Without it a
   dark band vanishes into the row it is drawn on. */
.season-art .skin .keyline {
  stroke: #12121a;
  stroke-width: 13.5;
  stroke-linecap: round;
}

/* ---- nothing to draw -------------------------------------------------------------------------- */

/* A reward whose art does not exist yet says so, rather than borrowing a picture that belongs to
   something else. The name beside it is still correct, so the row still keeps its promise. */
.season-art-unknown {
  width: 30px;
  height: 30px;
  border: 2px dashed rgba(255, 255, 255, 0.30);
  border-radius: 8px;
}

/* ---- the objectives -------------------------------------------------------------------------- */

.season-challenge {
  display: grid;
  gap: 6px;
  padding: 12px 14px;
  border: 1px solid var(--season-line);
  border-radius: 22px;
  background: var(--season-surface);
}

.season-challenge-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.season-challenge-head strong { font-size: 15px; font-weight: 850; color: var(--brand-text); }
.season-challenge-head span { font-size: 11px; font-weight: 800; color: var(--brand-gold); white-space: nowrap; }
.season-challenge p { margin: 0; font-size: 12px; color: var(--brand-text-secondary); }
.season-challenge > span { font-size: 11px; color: var(--brand-text-muted); }
.season-challenge.is-finished { border-color: var(--brand-green); }
.season-challenge.is-missed { border-style: dashed; }
.season-challenge.is-missed strong { color: var(--brand-text-secondary); }

/* Not var(--panel-strong), which is the same colour as the card this sits on — measured contrast
   1.000, so an empty bar was not a bar, it was a gap. The same white wash .season-bar uses, which
   is why that one has always been visible and these forty-two were not. Found in a screenshot;
   nothing about it fails a test. */
.season-challenge-bar { height: 6px; border-radius: 999px; background: rgba(255, 255, 255, .09); overflow: hidden; }
.season-challenge-bar > span { display: block; height: 100%; background: var(--ui-accent); }

.season-footnote { margin: 0; font-size: 11.5px; line-height: 1.45; color: var(--brand-text-muted); }

/* The way in, in the lobby. Shaped like the rounds strip beside it on purpose — a player has
   already learnt what that row does, and a second row that behaves the same way needs no
   learning. */
.season-strip {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%; min-height: 44px; padding: 12px 14px;
  border: 1px solid var(--lobby-border); border-radius: var(--lobby-radius-button);
  background: var(--lobby-surface-1); color: var(--lobby-text);
  font: inherit; font-weight: 700; text-align: left; cursor: pointer;
}

.season-strip-label { display: flex; align-items: center; gap: 8px; min-width: 0; }
.season-strip-chevron { font-size: 11px; text-transform: uppercase; color: var(--lobby-text-muted); }
.season-strip:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }

/* Gold, because it means a reward is waiting, which is the only thing it ever means here. */
.season-pip { width: 9px; height: 9px; border-radius: 999px; background: var(--gold); flex: none; }

/* Two columns of reward do not fit beside a tier number on a 320px phone without a name becoming an
   ellipsis, so the tracks stack under it instead. Nothing scales to fit. */
@media (max-width: 420px) {
  .season-tier { grid-template-columns: 40px minmax(0, 1fr); }
  .season-tier-number { grid-row: span 2; }
  .season-reward { min-height: 54px; }
  .season-offer { grid-template-columns: auto minmax(0, 1fr); }
  .season-buy,
  .season-owned { grid-column: 1 / -1; width: 100%; }
  .season-owned { justify-content: center; }
  .season-head h1 { font-size: 25px; }
}

/* The narrowest phone the lobby is measured at. A season's name is a game word and a game word is
   not allowed to become "Firs…", so the two things beside it give way first: the type comes down a
   step and the balance loses its padding rather than its digits. The panel's own 18px and the
   shell's 14px are 64px this header does not have and the phone's page does. */
@media (max-width: 360px) {
  .season-head h1 { font-size: 21px; }
  .season-coin-pill { gap: 6px; padding: 5px 9px 5px 7px; }
  .season-coin-mark { width: 16px; height: 16px; flex-basis: 16px; }
  .season-coin-count { font-size: 14px; }

  /* And the top-up affordance goes, which is the last 26px the name needs. The pill is still the
     one button it always was and its label still reads "Open your wallet", so what is lost here is
     an invitation rather than a door — and a season called "Firs…" is a worse trade. */
  .season-coin-add { display: none; }
}

/* The season panel keeps its own wallet at every width now, and the premise that took it away
   above 700px is what changed. The lobby's coin pill used to ride inside the account capsule, which
   was on screen behind every panel — so a second balance here was a second answer to one question.
   The pill lives in the corner cluster now, and that cluster shows only on the four screens the
   navigation bar covers. This is not one of them: hiding this one would leave the season panel,
   which is a screen for spending, with no balance on it anywhere. */
@media (min-width: 701px) {
  .season-head h1 { font-size: 34px; }
  .season-reward strong { font-size: 14px; }
  .season-reward { min-height: 72px; }
  .season-tier { grid-template-columns: 52px minmax(0, 1fr) minmax(0, 1fr); }
}

/* ---- the shop -----------------------------------------------------------------------------
   The browser twin of Shop.razor.css. Same discipline, same words, and the same one rule that
   matters most on this screen:

   GOLD IS MONEY HERE AND NOWHERE ELSE. --brand-gold appears on exactly three things — the wallet,
   a price on a card, and a coin bundle. It is deliberately absent from the aisle tabs, from the
   "Owned" tag and from every border and heading, because a shop is precisely where a player learns
   whether gold means anything. Decorate with it once and the price stops reading as a price.

   The aisle tabs reuse .builder-tab rather than restyling a second row of pills, so the two panels
   scroll and highlight identically. */

.shop-panel { display: flex; flex-direction: column; min-height: 0; }

/* The snake and the way out stay on screen while you shop, for the reason the builder pins the
   same band: the preview IS the product here, and a shelf of ninety cards scrolls it away. */
.shop-top { position: sticky; top: 0; z-index: 2; }

.shop-wallet {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid rgba(255, 200, 61, .35);
  background: rgba(255, 200, 61, .10);
  color: var(--brand-gold, #ffc83d);
  font-weight: 850;
  font-variant-numeric: tabular-nums;
}

.shop-fitting {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  flex-wrap: wrap;
  min-height: 48px;
  padding: 4px 12px 10px;
}

.shop-fitting-name { font-weight: 850; font-size: 17px; }

.shop-fitting-hint {
  color: var(--text-dim);
  font-size: 13px;
  text-align: center;
  max-width: 40ch;
}

/* Primary, so 48 rather than 44. */
.shop-fitting-action {
  min-height: 48px;
  padding: 0 20px;
  border-radius: 22px;
  border: 1px solid var(--ui-accent);
  background: var(--ui-accent);
  color: var(--accent-ink, #03100d);
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

#shop-wear {
  background: transparent;
  color: var(--text);
  border-color: var(--panel-border);
}

.shop-aisles {
  flex: none;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  grid-template-columns: none;
}

.shop-aisles::-webkit-scrollbar { display: none; }

.shop-aisles .builder-tab {
  flex: 0 0 auto;
  overflow: visible;
  padding-inline: 14px;
  text-overflow: clip;
}

.shop-blurb {
  margin: 0 0 14px;
  color: var(--text-dim);
  font-size: 13px;
}

.shop-pages { overflow-y: auto; min-height: 0; }

/* Two columns on a phone, matching the app's shop after its redesign. auto-fill at 104px gave four
   or five cramped tiles on a wide screen, so the artwork — which is the thing being sold — came out
   SMALLEST where there was most room for it. Wider screens get more columns, but never at the cost
   of the art: 200px is the floor, not 104. */
.shop-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 14px;
  align-items: stretch;
}

@media (max-width: 460px) {
  .shop-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.shop-card {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  padding: 12px;
  border: 1px solid var(--panel-border);
  border-radius: 22px;
  background: var(--panel-soft);
  transition: border-color 150ms, background 150ms, box-shadow 150ms;
}

/* On the snake right now, and louder than owned — which is the question somebody scanning a grid
   of things they already own is actually asking. The app draws the same distinction. */
.shop-card.is-equipped {
  border-color: var(--brand-secondary, #3bf5c8);
  box-shadow: 0 0 0 1px var(--brand-secondary, #3bf5c8);
}

/* What is being tried on, marked by a border rather than a fill, so the snake art keeps its own
   colours. */
.shop-card.is-trying {
  border-color: var(--brand-purple, #d83cff);
  background: var(--control-hover);
}

.shop-card-try {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 44px;
  appearance: none;
  border: 0;
  background: none;
  color: inherit;
  padding: 0;
  cursor: pointer;
}

.shop-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-height: 32px;
  padding: 0 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 850;
  font-variant-numeric: tabular-nums;
}

/* Owned says so in a word and a tick as well as in a colour, because a colour-blind player reading
   a grid of ninety cards has only the word to go on. Deliberately not gold: owning is not money. */
.shop-tag-owned {
  border: 1px solid rgba(255, 255, 255, .14);
  background: var(--control-hover);
  color: var(--text-dim);
}

/* A price, and the buy button, so 44. */
.shop-tag-buy {
  min-height: 44px;
  width: 100%;
  appearance: none;
  border: 1px solid rgba(255, 200, 61, .35);
  background: rgba(255, 200, 61, .10);
  color: var(--brand-gold, #ffc83d);
  cursor: pointer;
}

/* The other way to have it, which is the whole model: levels unlock, coins skip ahead. */
.shop-tag-earn { font-size: 11px; color: var(--text-dim); }

/* Equipped: the teal the app uses for the same state, and a word rather than only a colour. */
.shop-tag-equipped {
  border: 1px solid var(--brand-secondary, #3bf5c8);
  background: color-mix(in srgb, var(--brand-secondary, #3bf5c8) 14%, transparent);
  color: var(--brand-secondary, #3bf5c8);
}

.shop-offers { display: flex; flex-direction: column; gap: 12px; }

.shop-offer {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px;
  border: 1px solid var(--panel-border);
  border-radius: 28px;
  background: var(--panel-soft);
}

.shop-offer.is-quiet { opacity: .72; }

.shop-offer-art { font-size: 28px; flex: none; }

.shop-offer-words { flex: 1; min-width: 0; }

.shop-offer-words h3 { margin: 0 0 4px; font-size: 17px; font-weight: 850; }

.shop-offer-words p { margin: 0; font-size: 13px; color: var(--text-dim); }

.shop-offer-held { margin-top: 4px !important; color: var(--brand-green, #63ff48) !important; font-weight: 700; }

.shop-offer-action {
  flex: none;
  min-height: 48px;
  padding: 0 20px;
  border-radius: 22px;
  border: 1px solid var(--ui-accent);
  background: var(--ui-accent);
  color: var(--accent-ink, #03100d);
  font-weight: 900;
  cursor: pointer;
}

.shop-offer-quiet {
  flex: none;
  font-size: 12px;
  font-weight: 700;
  color: var(--text-dim);
  text-align: right;
  max-width: 12ch;
}

.shop-note { margin: 0; font-size: 13px; color: var(--text-dim); }

.shop-bundles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 12px;
}

.shop-bundle {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-height: 96px;
  padding: 16px 8px;
  appearance: none;
  border: 1px solid rgba(255, 200, 61, .35);
  border-radius: 22px;
  background: rgba(255, 200, 61, .08);
  color: var(--text);
  cursor: pointer;
}

.shop-bundle-coins {
  font-size: 24px;
  font-weight: 900;
  font-style: italic;
  color: var(--brand-gold, #ffc83d);
  font-variant-numeric: tabular-nums;
}

.shop-bundle-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--text-dim);
}

.shop-bundle-price { margin-top: 6px; font-size: 14px; font-weight: 800; }

@media (prefers-reduced-motion: reduce) {
  .shop-card { transition: none; }
}

/* A trail's card art was a `.shop-card-streak` span with a flat left-to-right gradient in it, and
   it is gone: the card draws the real wake on a `.trail-canvas` now, through the same
   `drawTrailChip` the builder's picker uses. See shopCard() in app.js for why a gradient could not
   stand in for one. */

/* ---------------------------------------------------------------------------------------------
   The feedback sheet. Twin of the app's FeedbackSheet.razor.css.

   Same plain register and the same surface colour as .report-sheet above, because this hands
   straight over to that one and two dialogs a tap apart that do not match read as two different
   apps. The rating half may be lighter — it is asked of somebody who has just had a game — but
   "lighter" is one warm colour on a chosen star, not a different design. The moment this sheet
   looks jolly, the report row inside it looks jolly too.
   --------------------------------------------------------------------------------------------- */

.feedback-sheet {
  width: min(100%, 460px);
  max-height: 100%;
  padding: 18px;
  overflow-y: auto;
  border: 1px solid var(--panel-border);
  border-radius: 18px;
  background: var(--panel-strong);
}

/* ---- the rating ---- */

.feedback-rating {
  display: grid;
  gap: 6px;
  grid-template-columns: repeat(5, 1fr);
}

.feedback-rating button {
  min-height: 52px;
  padding: 8px 0;
  border: 1px solid var(--panel-border);
  border-radius: 14px;
  background: var(--panel-soft);
  /* Dim rather than absent when unchosen: five invisible stars read as a decoration rather than
     as a question, and this one wants answering. */
  color: var(--text-dim);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  transition: transform 60ms ease-out, background-color 60ms ease-out, border-color 60ms ease-out,
              color 60ms ease-out;
}

/* Gold rather than the menu purple: purple is the arcade's accent and wearing it here would pull
   the whole sheet back into the register the report half has to stay out of. State is never colour
   alone — the border lifts too, and aria-pressed carries it for a screen reader. */
.feedback-rating button.on {
  border-color: rgba(255, 200, 64, .5);
  background: rgba(255, 200, 64, .1);
  color: #ffc83d;
}

.feedback-rating button:active { transform: translateY(1px) scale(.985); }

.feedback-scale {
  display: flex;
  margin: 6px 2px 4px;
  justify-content: space-between;
  color: var(--text-dim);
  font-size: 12px;
}

/* ---- the rows ---- */

.feedback-list { display: grid; gap: 8px; margin-top: 14px; }

.feedback-row {
  display: block;
  width: 100%;
  min-height: 48px;
  padding: 12px 14px;
  border: 1px solid var(--panel-border);
  border-radius: 14px;
  background: var(--panel-soft);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform 60ms ease-out, background-color 60ms ease-out, border-color 60ms ease-out;
}

.feedback-row strong { display: block; font-size: 15px; font-weight: 750; }
.feedback-row small { display: block; margin-top: 3px; color: var(--text-dim); font-size: 12.5px; line-height: 1.35; }
.feedback-row:hover { border-color: rgba(255, 255, 255, .3); background: rgba(255, 255, 255, .06); }
.feedback-row:active { transform: translateY(1px) scale(.985); }

/* Reporting somebody, set apart by a rule and a little air rather than dressed up. No colour, no
   icon, no urgency: it is last because it is rarest, and separated because it is not the same KIND
   of thing as "the sound was too loud". Making it louder puts us back where we started, with a
   serious act reading as a routine control. */
.feedback-row.report {
  margin-top: 6px;
  padding-top: 16px;
  border-color: transparent;
  border-top: 1px solid rgba(255, 255, 255, .1);
  border-radius: 0;
  background: transparent;
}

.feedback-row.report:hover { border-color: transparent; border-top-color: rgba(255, 255, 255, .24); background: transparent; }
.feedback-row.report strong { font-weight: 700; }

/* ---- the trouble step ---- */

.feedback-reasons { display: grid; gap: 8px; margin-bottom: 14px; }

.feedback-reasons button {
  display: block;
  width: 100%;
  min-height: 48px;
  padding: 12px 14px;
  border: 1px solid rgba(255, 255, 255, .12);
  border-radius: 14px;
  background: rgba(255, 255, 255, .03);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.feedback-reasons button:hover { border-color: rgba(255, 255, 255, .3); background: rgba(255, 255, 255, .06); }
.feedback-reasons button.is-chosen { border-color: #d83cff; background: rgba(216, 60, 255, .12); }
.feedback-reasons strong { display: block; font-size: 15px; }
.feedback-reasons small { display: block; margin-top: 3px; color: var(--text-dim); font-size: 12.5px; line-height: 1.35; }

/* Take away the movement, not the feedback: colour and border still change, the element no longer
   moves to do it. A press that says nothing is worse for this player, not better. */
@media (prefers-reduced-motion: reduce) {
  .feedback-open:active,
  .feedback-rating button:active,
  .feedback-row:active { transform: none; }
}

/* ---- friends, and the global board -----------------------------------------------------------
   The browser twin of Friends.razor.css and Leaderboard.razor.css. Two rules from the brand guide
   do most of the work here, and both are easy to break on exactly these two screens.

   GREEN MEANS LIVE NOW, and a friends list is the one screen where that reservation earns its
   keep: the dot beside a name is the only decorative green on the page, and it says one thing —
   this person is here. It deliberately does NOT distinguish "in a lobby" from "playing"; the
   sentence under the name does that, because a colour is not a state a colour-blind player can
   read (§7, state is never colour alone), and three shades of green would be exactly that mistake.

   THE PIP IS NOT GOLD. It is --ui-accent, because gold means money or reward and a friend request
   is neither. The season strip's pip beside it is gold and means a reward is waiting; two pips of
   the same colour meaning two different things is how a player learns to stop reading either. */

.friends-pip {
  width: 9px; height: 9px; border-radius: 999px;
  background: var(--ui-accent); flex: none;
  box-shadow: 0 0 10px color-mix(in srgb, var(--ui-accent) 60%, transparent);
}

.friends-panel { display: flex; flex-direction: column; min-height: 0; }

.friends-signed-out { color: var(--lobby-text-muted); }

/* The sentence that stands in for the list when there is no list, and sits above it when there is
   one that could not be refreshed. Muted rather than red on purpose: none of the four things it
   says is the player's fault and none of them is an emergency — a server without a database, a
   connection that dropped, a first read still in flight. A red line for a poll that will very
   likely succeed in three seconds is the thing this screen already decided not to do. */
.friends-notice { margin: 0; color: var(--lobby-text-muted); font-size: 13px; }

.friends-section { display: grid; gap: 8px; }
.friends-section h2 {
  margin: 0;
  font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .08em;
  color: var(--lobby-text-muted);
}
.friends-section small { color: var(--lobby-text-muted); font-size: 12px; }

/* The answer to a redeemed code, in the browser's half of the same fix the app got.

   The plate itself is new; the position is not — this line was already directly under the Add form
   here, which is where the app's had to be moved to. What was missing was the DIFFERENCE between
   the three things that can happen: "you are now friends" rendered as the same dim 13px line as
   "that code does not work".

   The tone class is set from the wire token, never from the sentence. See askFriend(). */
#friends-add-status {
  display: grid;
  gap: 3px;
  margin: 0;
  min-height: 0;
}

#friends-add-status:not(:empty) {
  padding: 10px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 14px;
}

#friends-add-status strong {
  color: var(--lobby-text, var(--text));
  font-size: 13px;
  font-weight: 700;
  line-height: 1.4;
}

/* Where the row went. Quieter than the verdict, and still a sentence somebody is about to act on —
   so it keeps the muted token rather than dropping below it. */
#friends-add-status small {
  color: var(--lobby-text-muted);
  font-size: 11.5px;
  line-height: 1.4;
}

/* A friendship that exists NOW. Green is what this panel already spends on "live" for a presence
   dot, and this is the other thing on it that is true this second rather than pending. The colour
   carries none of the meaning on its own — the sentence inside says what happened. */
#friends-add-status.is-friends {
  border-color: color-mix(in srgb, var(--brand-green) 55%, transparent);
  background: color-mix(in srgb, var(--brand-green) 13%, transparent);
}

/* A request that went out: marked, not celebrated. Nobody has answered it. */
#friends-add-status.is-good {
  border-color: color-mix(in srgb, var(--ui-accent) 45%, transparent);
}

/* And a refusal, deliberately not red: a mistyped code is a typo, and dressing it as an alarm puts
   the loudest colour on the panel on the most ordinary thing that happens on it. */
#friends-add-status.is-refused {
  border-color: var(--panel-border);
}

/* The granted crown beside a name in a list. Sized against the text it sits beside rather than in
   pixels, so the same element is right in a 15px friend row and a 13px board row — a fixed 16px
   badge is a decal on one and a smudge on the other. The app's twin is NameCrown.razor.css.

   Inline-block with a baseline nudge rather than a flex child: it is drawn inside a name's own line
   in five different row builders, several of which are already flex containers doing something
   else. */
.name-crown {
  display: inline-block;
  width: 1.15em;
  height: 1.15em;
  /* Never squeezed by a long name beside it in a flex row. */
  flex: 0 0 auto;
  /* Sits on the text's own optical middle. Aligned to the baseline it hangs below the lower-case
     letters beside it and reads as having fallen off the row. */
  vertical-align: -0.22em;
  margin-right: 0.18em;
}

/* What just happened to a row, beside the rows. Sized and coloured like the add form's answer, so
   the panel has one kind of receipt rather than two. */
.friends-action-status {
  margin: 0;
  padding: 9px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  font-size: 12.5px;
  line-height: 1.4;
}

.friends-action-status.is-good {
  border-color: color-mix(in srgb, var(--brand-green) 55%, transparent);
  background: color-mix(in srgb, var(--brand-green) 13%, transparent);
}

/* Deliberately not red. "You have already hissed at them" is a fact about timing, not a fault, and
   the loudest colour on this panel belongs to nothing that happens this often. */
.friends-action-status.is-refused {
  border-color: var(--panel-border);
  opacity: 0.85;
}

/* Hiss, on a friend row. Outlined rather than filled: it is the one control here that makes
   somebody ELSE's phone make a noise, so it is findable without being the loudest thing on a row
   that also carries Join and Remove. */
.friend-hiss {
  border-color: color-mix(in srgb, var(--ui-accent) 55%, transparent);
}

.friends-list { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }

.friends-empty { margin: 0; color: var(--lobby-text-muted); font-size: 13px; }

/* Every button on a friend row is 44pt, the app's .row-action value, and the row wraps rather than
   shrinking them. The guide's "the whole row is the target" is about a row with ONE destination;
   these carry two or three separate decisions — Join, Invite, Remove — and each is its own target,
   so each has to clear the minimum on its own. A row that fitted three of them onto a 320px phone
   by making them 32px would be a row nobody with large hands can use accurately, on the screen
   where a mis-tap unfriends somebody. */
.friend-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  min-height: 44px; padding: 8px 12px;
  border: 1px solid var(--lobby-border); border-radius: var(--lobby-radius-button);
  background: var(--lobby-surface-1);
}

.friend-dot {
  width: 9px; height: 9px; border-radius: 999px; flex: none;
  background: var(--lobby-text-muted);
}
.friend-dot.is-online,
.friend-dot.is-in-party,
.friend-dot.is-in-round {
  background: var(--brand-green);
  box-shadow: 0 0 10px rgba(99, 255, 72, .7);
}

.friend-text { display: grid; gap: 1px; min-width: 0; flex: 1 1 auto; }
.friend-text strong { font-size: 15px; font-weight: 800; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.friend-text small { font-size: 12px; color: var(--lobby-text-muted); }

.friend-actions { display: flex; align-items: center; gap: 6px; flex: none; margin-left: auto; }
.friend-actions .plain-button { min-height: 44px; padding: 0 13px; border-radius: 22px; }

/* An invitation is the one row on this screen addressed to the reader by name, so it is the one
   row that is lit. Anything else lit alongside it stops it meaning "this one is for you". */
.friend-invite { border-color: color-mix(in srgb, var(--ui-accent) 55%, var(--lobby-border)); }

/* Not red. Removing somebody is an ordinary thing to do and mostly not a conflict; painting it as
   a danger makes an unfriend feel like an accusation and makes the real refusals read as louder
   than they are. Weight and position do the work instead. */
.friend-remove { color: var(--lobby-text-muted); }

.friends-code-block { display: grid; gap: 6px; }
.friends-code-row { display: flex; gap: 8px; }
.friends-code-row input { flex: 1 1 auto; min-width: 0; min-height: 44px; }
.friends-code-row button { min-height: 44px; padding: 0 16px; }

/* ---- the friends screen, redesigned ---------------------------------------------------------
   Twin of the app's Friends.razor.css. Every value here has a counterpart there: it is one screen
   drawn by two clients, and the parity is the point.

   Tokens throughout, because eight of the themes are pastel and a dark comp punched into one of
   them is the fault the shop's hero shipped with once already.
   --------------------------------------------------------------------------------------------- */

.friends-panel { position: relative; }

/* Scenery. It clips ITSELF rather than hanging off the panel — the app's first attempt let the
   image overhang and made the whole screen draggable sideways, which `overflow-x: clip` did not
   fix because the spec downgrades it to `hidden` whenever the other axis scrolls. */
.friends-snake {
  position: absolute;
  z-index: 0;
  top: -18px;
  right: 0;
  width: clamp(150px, 30vw, 230px);
  aspect-ratio: 700 / 651;
  overflow: hidden;
  opacity: .24;
  filter: hue-rotate(212deg) saturate(1.35) brightness(.92);
  pointer-events: none;
  user-select: none;
}

.friends-snake img { display: block; width: 127%; max-width: none; height: auto; }

/* The panel's own content sits over it. */
.friends-panel > *:not(.friends-snake) { position: relative; z-index: 1; }

:root[data-theme="light"] .friends-snake,
.is-light .friends-snake { opacity: .13; filter: hue-rotate(212deg) saturate(1.1) brightness(.72); }

.friends-section-copy { margin: 0 0 2px; color: var(--text-dim); font-size: 13.5px; line-height: 1.45; }

/* The code's own field: one frame holding the mark, the code and both actions. */
.code-mark {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  flex: 0 0 40px;
  border-radius: 14px;
  background: color-mix(in srgb, var(--ui-accent, #d83cff) 16%, transparent);
  color: var(--ui-accent, #d83cff);
}

.code-mark svg,
.code-act-icon {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.code-mark svg { width: 22px; height: 22px; }
.code-act-icon { width: 16px; height: 16px; flex: 0 0 16px; }

.code-act {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 40px;
  padding: 0 14px;
  border: 1px solid var(--line, rgba(255, 255, 255, .14));
  border-radius: 999px;
  background: rgba(255, 255, 255, .06);
  color: var(--text);
  font: inherit;
  font-size: 13.5px;
  font-weight: 750;
  white-space: nowrap;
  cursor: pointer;
}

/* Copy is the likelier of the two and carries the accent; Share stays quiet beside it. */
.code-act.is-copy {
  border-color: transparent;
  background: linear-gradient(135deg, #ea42f5, #7a28ff);
  color: #fff;
}

.code-act:focus-visible { outline: 2px solid var(--ui-accent, #d83cff); outline-offset: 2px; }

/* The labels cannot fit beside the code on a narrow screen — the code's min-content alone is
   142px — so below that width the icons carry it. Both buttons keep their aria-label. */
@media (max-width: 560px) {
  .code-act span {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .code-act { padding: 0 13px; }
}

/* Full width and quiet: it is considered and slightly destructive — it stops every code already
   handed out — so it gets prominence without the accent that says "do this". */
.code-renew {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  width: 100%;
  min-height: 52px;
  margin-top: 10px;
  border: 1px solid var(--line, rgba(255, 255, 255, .14));
  border-radius: 20px;
  background: rgba(255, 255, 255, .05);
  color: var(--text);
  font: inherit;
  font-size: 15.5px;
  font-weight: 700;
  cursor: pointer;
}

.code-renew:focus-visible { outline: 2px solid var(--ui-accent, #d83cff); outline-offset: 2px; }

.friends-code-mine {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  min-height: 44px; padding: 8px 12px;
  border: 1px solid var(--lobby-border); border-radius: var(--lobby-radius-button);
  background: var(--lobby-surface-1);
}
.friends-code-mine code {
  font-size: 18px; font-weight: 800; letter-spacing: .14em;
  font-variant-numeric: tabular-nums;
  color: var(--ui-accent);
}

/* A real switch. The checkbox is still the control — it holds the state, takes the keyboard and
   carries the label — and is simply not painted; the track and knob are drawn from :checked. */
.friends-toggle {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  min-height: 56px;
  padding: 8px 12px;
  border: 1px solid var(--line, rgba(255, 255, 255, .14));
  border-radius: 22px;
  background: rgba(255, 255, 255, .04);
  cursor: pointer;
}

.friends-toggle input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* The eye says the state a second way, so it is never colour alone. */
.friends-toggle .toggle-mark {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 13px;
  background: color-mix(in srgb, #63ff48 15%, transparent);
  color: #63ff48;
  transition: background 180ms ease, color 180ms ease;
}

.friends-toggle .toggle-mark svg {
  width: 21px;
  height: 21px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.friends-toggle .toggle-slash { display: none; }

.friends-toggle .toggle-copy { font-size: 14.5px; font-weight: 750; }

.friends-toggle .toggle-switch {
  position: relative;
  width: 58px;
  height: 32px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--text-dim) 34%, transparent);
  transition: background 180ms ease, box-shadow 180ms ease;
}

.friends-toggle .toggle-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(0, 0, 0, .35);
  transition: transform 180ms cubic-bezier(.2, .8, .3, 1);
}

.friends-toggle input:checked ~ .toggle-switch {
  background: linear-gradient(100deg, #7a28ff, #ea42f5);
  box-shadow: 0 0 16px color-mix(in srgb, var(--ui-accent, #d83cff) 34%, transparent);
}

.friends-toggle input:checked ~ .toggle-switch .toggle-knob { transform: translateX(26px); }

.friends-toggle input:checked ~ .toggle-mark {
  background: color-mix(in srgb, var(--ui-accent, #d83cff) 17%, transparent);
  color: var(--ui-accent, #d83cff);
}

.friends-toggle input:checked ~ .toggle-mark .toggle-slash { display: block; }

.friends-toggle input:focus-visible ~ .toggle-switch { outline: 2px solid var(--ui-accent, #d83cff); outline-offset: 3px; }

/* ---- the inbox ------------------------------------------------------------------------------

   The lobby strip's pip, and its own class rather than .friends-pip: the two counts are different
   facts — one is people waiting on you, the other is the game talking to you — and sharing a rule
   is how one gets restyled and silently takes the other with it. */
.inbox-pip {
  width: 9px; height: 9px; border-radius: 999px;
  background: var(--ui-accent); flex: none;
  box-shadow: 0 0 10px color-mix(in srgb, var(--ui-accent) 60%, transparent);
}

.inbox-panel { display: flex; flex-direction: column; min-height: 0; }

/* One control, on the right, on its own line — see the note in index.html. Nowrap because the
   label is three words and the button is the width of its text: "Mark all / read" over two lines
   inside a pill reads as two controls. */
.inbox-actions { display: flex; justify-content: flex-end; }
.inbox-actions .plain-button { min-height: 44px; padding: 0 16px; white-space: nowrap; }

/* Signed out, loading and empty all land here, one at a time. */
.inbox-note { margin: 0; color: var(--lobby-text-muted); font-size: 13px; }

.inbox-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }

/* A row and its action side by side, and the action never nested inside the row's button — that is
   invalid HTML and a browser resolving it drops one of the two, which is how a control vanishes on
   one engine and not another. The row wraps rather than shrinking the action, so a long title on a
   320px phone pushes "Join" onto its own line at full size instead of squeezing it below the
   minimum target. */
.notice {
  display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap;
  padding: 10px 12px;
  border: 1px solid var(--lobby-border); border-radius: var(--lobby-radius-button);
  background: var(--lobby-surface-1);
}

/* Unread carries in three things at once — the ground, the weight of the title and the dot — plus
   the word behind the dot for a reader who gets none of the three. */
.notice.is-unread { border-color: color-mix(in srgb, var(--ui-accent) 42%, var(--lobby-border)); }
.notice.is-read .notice-mark { opacity: .68; }

/* The whole row is the target for "I have read this", which is the one decision it carries. */
.notice-body {
  display: flex; align-items: flex-start; gap: 10px;
  flex: 1 1 220px; min-width: 0; min-height: 44px;
  padding: 0; border: 0; background: none; color: inherit; text-align: left; cursor: pointer;
}

.notice-mark {
  width: 30px; height: 30px; flex: none;
  display: grid; place-items: center;
  border-radius: 999px;
  font-size: 15px; line-height: 1;
  background: color-mix(in srgb, var(--ui-accent) 16%, transparent);
}
.notice-mark.is-coin   { background: color-mix(in srgb, var(--brand-gold) 20%, transparent); }
.notice-mark.is-refund { background: color-mix(in srgb, var(--lobby-text-muted) 26%, transparent); }
.notice-mark.is-gift   { background: color-mix(in srgb, var(--granted-beta) 22%, transparent); }
.notice-mark.is-warn   { background: color-mix(in srgb, var(--danger) 22%, transparent); }

.notice-words { display: grid; gap: 2px; min-width: 0; flex: 1 1 auto; }

.notice-title {
  display: flex; align-items: center; gap: 7px;
  font-size: 14.5px; font-weight: 700; color: var(--lobby-text);
}
.notice.is-unread .notice-title { font-weight: 850; }

.notice-dot {
  width: 8px; height: 8px; flex: none; border-radius: 999px;
  background: var(--ui-accent);
}

/* The body wraps in full. A notice truncated to one line is a notice that has to be opened to be
   read, and there is nothing here to open it into — this screen IS the notice. */
.notice-text { font-size: 13px; line-height: 1.4; color: var(--lobby-text-muted); }

.notice-when {
  font-size: 11px; font-weight: 700; letter-spacing: .03em;
  color: var(--lobby-text-muted);
}

/* 44px, and nowrap. "Wear it" wrapped to two lines inside a 60px button on the phone before
   somebody looked at a screenshot of it. */
.notice-go {
  flex: none; min-height: 44px; padding: 0 15px;
  border: 1px solid color-mix(in srgb, var(--ui-accent) 55%, transparent);
  border-radius: var(--lobby-radius-button);
  background: color-mix(in srgb, var(--ui-accent) 15%, transparent);
  color: var(--lobby-text);
  font-weight: 800; white-space: nowrap; cursor: pointer;
}
/* Support is the last thing anybody wants to press, so it does not compete with a Join above it. */
.notice-go.is-quiet {
  border-color: var(--lobby-border);
  background: transparent;
  color: var(--lobby-text-muted);
}

@media (max-width: 420px) {
  /* The action takes the full width on its own line rather than sharing one with a title that has
     become an ellipsis. Same rule the season tiers follow: the screen gives things up in order. */
  .notice-go { flex: 1 1 100%; }
}

.leaderboard-panel { display: flex; flex-direction: column; min-height: 0; }

/* Where the reader stands, above everybody else. It is a card rather than a row because it is not
   one of them: the list below is other people, and this is the answer to the question that brought
   somebody to the page. */
.leaderboard-me {
  display: grid; gap: 8px; padding: 14px;
  border: 1px solid color-mix(in srgb, var(--ui-accent) 40%, var(--lobby-border));
  border-radius: var(--lobby-radius-card, 28px);
  background: var(--lobby-surface-1);
}
.leaderboard-me-head { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.leaderboard-me-rank {
  font-size: 26px; font-weight: 900; font-style: italic;
  font-variant-numeric: tabular-nums; color: var(--ui-accent);
}
.leaderboard-me-name { font-size: 16px; font-weight: 800; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.leaderboard-me-note { margin: 0; font-size: 12px; color: var(--lobby-text-muted); }

.leaderboard-me-stats {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px 10px; margin: 0;
}
.leaderboard-me-stats dt {
  font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .07em;
  color: var(--lobby-text-muted);
}
.leaderboard-me-stats dd {
  margin: 0; font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums;
}

/* Wider than the winners board's rank column: this one counts every player rather than every
   recorded win, so it reaches four figures where that one reaches three. */
/* Five columns now: rank, cobra, who, score, chevron. This rule is here rather than on
   .leaderboard-row because it is what the three-column version was, and a second rule further down
   the file loses to it on specificity — which is exactly what happened: the row grew two children
   and went on being laid out for three. */
.leaderboard-list .scoreboard-row {
  grid-template-columns: 40px auto minmax(0, 1fr) auto 14px;
}
.leaderboard-row .scoreboard-rank { font-variant-numeric: tabular-nums; }

@media (max-width: 420px) {
  /* Six metrics in three columns is two characters per number on a 320px phone. Two columns and
     three rows instead — the screen gives things up in order rather than scaling to fit. */
  .leaderboard-me-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ================================================================================================
   THE LOBBY'S NAVIGATION, AND THE CHROME THAT MOVED TO MAKE ROOM FOR IT

   The browser lobby had drifted a long way behind the phone's. Its five destinations were a stack
   of full-width rows at the bottom of the dashboard, each with an "OPEN" chevron — a list of doors
   dressed as content, three of which sat below the fold on every phone. The app answers the same
   question with a fixed bar of five items, and this is that bar.

   Three things moved to make it work, and each of them is the app's arrangement rather than a new
   one: the account capsule is hidden wherever the bar shows (PROFILE is the drawer's door, and two
   doors to one room is what the capsule became the day the bar arrived); the balance and the inbox
   bell went to the corner, because a number you glance at is not a place you go; and the wordmark
   became a real lockup with the cobra under the crown.

   Everything here is last in the file on purpose. The lobby block above it overrides the older
   chrome with !important, and this has to win against that.
   ============================================================================================= */

:root {
  /* What the bar reserves at the bottom of the page. Spent through --shell-bottom, never as a
     margin on anything: .lobby-body's cap and every .menu-panel's cap already read --shell-bottom,
     so ONE number moves all of them together. A second number that hoped to match it is the exact
     mistake --shell-top carries a comment about, and it put a panel's last row 30px below the
     bottom of the screen. */
  --lobby-nav-h: 67px;
}

/* ---- where the bar is, and what it displaces --------------------------------------------------
   The four screens it covers are the app's: Home, Leaderboard, Friends and Shop. Written as one
   forgiving :has() list rather than four selectors, and repeated for each of the three things it
   decides, because CSS has no name for a state.

   Not the party lobby, the builder, the season or the how-to-play panel: those are places you go
   INTO and come back from, they carry their own way out, and the app puts no bar on their twins
   either. The account capsule is still their door to the drawer, which is why it is hidden here
   rather than deleted. */
body:not(.is-playing):has(#menu:not(.is-hidden)):has(#home-panel:not(.is-hidden), #friends-panel:not(.is-hidden), #leaderboard-panel:not(.is-hidden), #shop-panel:not(.is-hidden)) .lobby-nav,
body:not(.is-playing):has(#menu:not(.is-hidden)):has(#home-panel:not(.is-hidden), #friends-panel:not(.is-hidden), #leaderboard-panel:not(.is-hidden), #shop-panel:not(.is-hidden)) .lobby-top-actions {
  display: grid;
}

body:not(.is-playing):has(#menu:not(.is-hidden)):has(#home-panel:not(.is-hidden), #friends-panel:not(.is-hidden), #leaderboard-panel:not(.is-hidden), #shop-panel:not(.is-hidden)) .account-menu-toggle {
  display: none !important;
}

/* The room the bar takes, said once, in the shell's own bottom padding. Custom properties resolve
   on the element, so redeclaring --shell-bottom here rewrites the base rule's `padding` AND both
   caps that read it — the lobby's scroller and every panel's — with nothing else to keep in step. */
body:not(.is-playing):has(#menu:not(.is-hidden)):has(#home-panel:not(.is-hidden), #friends-panel:not(.is-hidden), #leaderboard-panel:not(.is-hidden), #shop-panel:not(.is-hidden)) .menu-shell {
  --shell-bottom: calc(14px + var(--lobby-nav-h));
}

/* Browsing rounds takes the whole page — the wordmark, the capsule and the cog all go, and the
   shell starts scrolling. The bar goes with them: it is page chrome, and a long list of matches
   should not have to scroll under it. */
body:has(#rounds-drawer:not([hidden])) .lobby-nav,
body:has(#rounds-drawer:not([hidden])) .lobby-top-actions { display: none !important; }

/* ---- the bar itself ---------------------------------------------------------------------------
   Fixed to the bottom edge and carrying the home indicator's inset in its OWN padding, exactly as
   BottomNavigation.razor.css does: the glass reaches the bottom of the screen the way a system bar
   does while every target stays above the indicator. A margin instead would leave a strip of moving
   arena under it, which reads as a bar floating half a centimetre off the glass — and in Safari on
   any modern iPhone, an item you cannot press. */
.lobby-nav {
  position: fixed;
  z-index: 7;
  right: 0;
  bottom: 0;
  left: 0;
  display: none;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  padding:
    0 max(10px, calc(var(--safe-right) + 10px))
    max(10px, var(--safe-bottom))
    max(10px, calc(var(--safe-left) + 10px));
  border-top: 1px solid var(--lobby-border);
  background: linear-gradient(rgba(6, 14, 13, .86), rgba(3, 8, 8, .97));
  backdrop-filter: blur(22px) saturate(1.3);
  -webkit-backdrop-filter: blur(22px) saturate(1.3);
  pointer-events: auto;
}

.lobby-nav-item {
  position: relative;
  display: grid;
  min-height: 48px;
  padding: 9px 2px 7px;
  justify-items: center;
  gap: 3px;
  border: 0;
  background: transparent;
  color: var(--lobby-text-muted);
  font: inherit;
  cursor: pointer;
  transition: color 180ms ease, transform 180ms ease;
}

.lobby-nav-icon { position: relative; display: grid; place-items: center; }

.lobby-nav-item svg {
  width: 23px;
  height: 23px;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 1.8;
}

/* "LEADERBOARD" is the label that decides this size; the other four have room to spare. It comes
   down a step rather than ellipsising, because a nav item reading "LEADERBO…" is worse than a
   small one — the app's bar records measuring exactly that at 402pt. */
.lobby-nav-label {
  overflow: hidden;
  max-width: 100%;
  color: inherit;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
}

/* Current is said THREE ways — colour, weight and the underline — because neon on its own is not a
   state a colour-blind player can read, and aria-current is not one a sighted player can.

   Purple, and that is a deliberate exception to the mode tinting a few hundred lines above: the bar
   is site chrome, not part of the round you are about to play, and an underline that turned purple
   because you glanced at the Royale card would be teaching the colour to mean nothing. Brand purple
   is the app bar's colour on the phone too.

   Read off the panels themselves rather than a class this file writes, so the bar cannot say Home
   while the leaderboard is on screen. app.js sets the matching aria-current; it cannot set this,
   and this cannot set that. */
body:has(#home-panel:not(.is-hidden)) #lobby-nav-home,
body:has(#leaderboard-panel:not(.is-hidden)) .lobby-nav #leaderboard-open,
body:has(#friends-panel:not(.is-hidden)) .lobby-nav #friends-open,
body:has(#shop-panel:not(.is-hidden)) .lobby-nav #shop-open { color: var(--ui-accent); }

body:has(#home-panel:not(.is-hidden)) #lobby-nav-home .lobby-nav-label,
body:has(#leaderboard-panel:not(.is-hidden)) .lobby-nav #leaderboard-open .lobby-nav-label,
body:has(#friends-panel:not(.is-hidden)) .lobby-nav #friends-open .lobby-nav-label,
body:has(#shop-panel:not(.is-hidden)) .lobby-nav #shop-open .lobby-nav-label { font-weight: 900; }

body:has(#home-panel:not(.is-hidden)) #lobby-nav-home::after,
body:has(#leaderboard-panel:not(.is-hidden)) .lobby-nav #leaderboard-open::after,
body:has(#friends-panel:not(.is-hidden)) .lobby-nav #friends-open::after,
body:has(#shop-panel:not(.is-hidden)) .lobby-nav #shop-open::after {
  position: absolute;
  right: 26%;
  bottom: 0;
  left: 26%;
  height: 3px;
  border-radius: 3px 3px 0 0;
  background: var(--ui-accent);
  box-shadow: 0 0 10px color-mix(in srgb, var(--ui-accent) 85%, transparent);
  content: "";
}

.lobby-nav-item:active { transform: scale(.94); }
.lobby-nav-item:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: -3px; border-radius: 12px; }

@media (hover: hover) {
  .lobby-nav-item:hover { color: var(--lobby-text); }
}

/* Two counts, drawn as counts. The friends mark rides on its icon and the bell's on the bell; both
   were 9px dots that could only say "something", and both now say how many. Red, and red only
   here: this is the one place in the lobby that means "somebody is waiting on you", and it is
   neither of the two brand colours that already mean Classic and money. */
.lobby-nav .friends-pip,
.lobby-bell .inbox-pip {
  position: absolute;
  top: -4px;
  right: -8px;
  display: grid;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  place-items: center;
  border: 1.5px solid #030808;
  border-radius: 999px;
  background: #ff3b5c;
  color: #fff;
  font-size: 9.5px;
  font-style: normal;
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

/* ---- the corner: what you have, and what is waiting -------------------------------------------
   Where the account capsule stands, using the capsule's own offsets so the two can never be beside
   each other by half a pixel. Never both on screen: this shows exactly where that is hidden. */
.lobby-top-actions {
  position: fixed;
  z-index: 7;
  top: calc(20px + var(--safe-top));
  right: max(88px, calc(50% - 384px + var(--safe-right)));
  display: none;
  grid-auto-flow: column;
  align-items: center;
  gap: 8px;
}

.lobby-bell {
  position: relative;
  display: grid;
  width: 46px;
  height: 46px;
  place-items: center;
  border: 1px solid var(--lobby-border);
  border-radius: 50%;
  background: var(--lobby-surface-1);
  color: var(--lobby-text-secondary);
  cursor: pointer;
}

.lobby-bell-mark { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
.lobby-bell:focus-visible, .lobby-top-actions .top-coin-pill:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 3px; }
.lobby-bell:active, .lobby-top-actions .top-coin-pill:active { transform: scale(.94); }

@media (hover: hover) {
  .lobby-bell:hover { border-color: var(--lobby-border-strong); color: var(--lobby-text); }
  .lobby-top-actions .top-coin-pill:hover { border-color: var(--brand-gold, #ffc83d); }
}

/* GOLD, because this is money — the one thing --brand-gold is for, and the reason it still means
   anything anywhere else on the page. The debt state keeps its own rule further up: a balance a
   refund has taken below zero drops the gold in both clients, because a gold zero reads as a purse
   somebody merely emptied rather than one with nothing to spend from. */
.lobby-top-actions .top-coin-pill {
  min-height: 46px;
  padding: 0 15px 0 12px;
  gap: 8px;
  border: 1px solid color-mix(in srgb, var(--brand-gold, #ffc83d) 34%, transparent);
  border-radius: var(--lobby-radius-pill);
  background: var(--lobby-surface-1);
  font-size: 17px;
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

/* :not(.is-owing) on both of these, and it is load-bearing rather than tidy. The size and the shape
   belong to the pill; the COLOUR belongs to the state, and a gold declaration that applied to both
   would beat `.top-coin-pill.is-owing` and leave a debt painted as money. That exact override,
   with that exact !important, is a bug this file already records above. */
.lobby-top-actions .top-coin-pill:not(.is-owing) { color: var(--brand-gold, #ffc83d) !important; }

.lobby-top-actions .top-coin-pill:not(.is-owing) .coin-dot {
  width: 19px;
  height: 19px;
  border: 0;
  border-radius: 50%;
  /* No glow, for the reason the season pill has none: gold means money, and a halo is gold
     spreading past the thing that IS the money. ShopParityTests holds the line. */
  background: radial-gradient(circle at 35% 30%, #fff3a6, var(--brand-gold, #ffc83d) 46%, #c78305);
}

/* And the debt state gets the same geometry with none of the gold: the disc recedes with the
   number, because a state that recedes has to recede everywhere. */
.lobby-top-actions .top-coin-pill.is-owing { border-color: var(--lobby-border); }
.lobby-top-actions .top-coin-pill.is-owing .coin-dot { width: 19px; height: 19px; }

/* ---- the lockup: emblem, crown, word, line ----------------------------------------------------
   The crown used to float beside the C with nothing under it, which is a stray gold glyph rather
   than a mark. It sits on the cobra now — the same PNG the Classic card draws and the same crown
   the mascot wears — and the three pieces are one lockup, as they are in the app's header.

   Every number is a ratio of --wordmark, for the reason the block above states at length: the type
   shrinks across four breakpoints, and a literal offset is how the crown came adrift the first
   time. The 24px is the padding already reserved above the type for exactly this. */
.brand-emblem {
  position: relative;
  display: grid;
  width: calc(var(--wordmark) * 1.28);
  height: calc(var(--wordmark) * 1.28);
  flex: 0 0 calc(var(--wordmark) * 1.28);
  place-items: center;
  border: 1px solid rgba(216, 60, 255, .45);
  border-radius: calc(var(--wordmark) * .42);
  background:
    radial-gradient(circle at 50% 35%, rgba(99, 255, 72, .16), transparent 70%),
    rgba(10, 8, 26, .85);
  box-shadow: 0 0 20px rgba(216, 60, 255, .18), inset 0 0 18px rgba(99, 255, 72, .06);
}

.brand-cobra {
  width: calc(var(--wordmark) * .95);
  height: calc(var(--wordmark) * .95);
  object-fit: contain;
  filter: drop-shadow(0 0 8px rgba(99, 255, 72, .35));
}

/* Centred on the emblem and clear of the artwork rather than resting in it: lower, and the band
   crosses the cobra's own head so the two pieces of gold read as one shape with a fault in it. */
.brand-crown {
  top: calc(var(--wordmark) * -.46) !important;
  right: auto !important;
  left: 50% !important;
  width: calc(var(--wordmark) * .8) !important;
  height: auto !important;
  animation: brand-crown-lift 3s ease-in-out infinite;
  transform-origin: 50% 80%;
  filter: drop-shadow(0 3px 8px rgba(0, 0, 0, .5));
}

/* Its own keyframe, because the old one animates `transform` and would drop the -50% that centres
   the crown on the emblem — a transform in a keyframe replaces the property, it does not compose
   with it. Same lift, same period, so nothing about the personality changes. */
@keyframes brand-crown-lift {
  0%, 100% { transform: translateX(-50%) translateY(0) rotate(-8deg); }
  50% { transform: translateX(-50%) translateY(-4px) rotate(-2deg); }
}

/* Core's line for the game, under the word, in the app's own treatment. Nowrap for the reason the
   logotype is: a tagline broken over two lines is not a tagline, it is a sentence. */
.brand-lockup small {
  margin: 2px 0 0;
  color: var(--lobby-text-secondary);
  font-size: clamp(8.5px, 2.5vw, 11px);
  font-style: italic;
  font-weight: 800;
  letter-spacing: .13em;
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
}

/* ---- the cobra on the CTA ---------------------------------------------------------------------
   Decoration, and outside the flow of the label so it can never push the word around or shrink the
   target: absolutely positioned in the button's right-hand margin, taking no pointer events. The
   button already has `position: relative` and `overflow: hidden` for its travelling band, so the
   artwork is clipped to the same 22px radius and cannot run off the corners. */
.play-cobra {
  position: absolute;
  z-index: 0;
  right: -10px;
  bottom: -4%;
  /* Sized off the button's HEIGHT, where the app sizes the same artwork off its width — and that is
     not a stylistic difference, it is the same proportion arrived at from the other side. The app's
     CTA is clamp(84px, 15vh, 126px) tall and this one is 78, so 31% of the width came out 134px
     against 78px of room and the artwork was sliced through the cobra's own head. The app's asset
     stands at 89% of its button's height; anchoring to that keeps the animal whole at every
     breakpoint this button has, and there is no width at which it can be cropped again. */
  height: 92%;
  width: auto;
  max-width: 42%;
  pointer-events: none;
  filter: var(--cta-cobra);
  opacity: .97;
}

/* ---- the live-rounds row ----------------------------------------------------------------------
   Two lines now, as the app's live-toggle has: the count, and what pressing it is for. */
.rounds-strip-copy { display: grid; gap: 2px; min-width: 0; text-align: left; }
.rounds-strip-copy strong { color: var(--lobby-text); font-size: 14px; font-weight: 850; }
.rounds-strip-copy small { color: var(--lobby-text-muted); font-size: 11.5px; font-weight: 700; letter-spacing: 0; text-transform: none; }
.rounds-strip { min-height: 72px; }

/* ---- narrow phones ---------------------------------------------------------------------------- */
@media (max-width: 700px) {
  /* The corner cluster follows the capsule's phone offsets, the ones it is standing in for. */
  .lobby-top-actions { top: calc(16px + var(--safe-top)); right: max(72px, calc(var(--safe-right) + 72px)); gap: 6px; }
  .lobby-bell { width: 42px; height: 42px; }
  .lobby-bell-mark { width: 20px; height: 20px; }
  .lobby-top-actions .top-coin-pill { min-height: 42px; padding: 0 13px 0 10px; font-size: 15px; }
  .lobby-top-actions .top-coin-pill .coin-dot { width: 17px; height: 17px; }
  .lobby-top-actions .top-coin-pill.is-owing .coin-dot { width: 17px; height: 17px; }
  .rounds-strip-copy strong { font-size: 13px; }
  .rounds-strip-copy small { font-size: 10.5px; }
}

/* 440, not 400: a 402pt iPhone sat just the wrong side of the older breakpoint and ellipsised
   anyway. Measured on the app's bar, which is the same five words at the same weight. */
@media (max-width: 440px) {
  .lobby-nav-label { font-size: 8px; }
  .lobby-nav-item svg { width: 21px; height: 21px; }
}

/* The artwork goes, and the room reserved for it goes with it — otherwise the label keeps sitting
   a hundred pixels left of centre in a button with nothing on its right. The app drops it at the
   same width for the same reason. */
@media (max-width: 400px) {
  .play-cobra { display: none; }
  .lobby-play { padding-inline: 20px; }
}

/* A phone held sideways is barely 400pt tall and the whole lobby has to fit in it. The bar gives up
   the height it can while every target stays above 44. */
@media (max-height: 560px) {
  :root { --lobby-nav-h: 60px; }
  .lobby-nav-item { min-height: 44px; padding: 6px 2px 5px; }
  .lobby-nav-item svg { width: 19px; height: 19px; }
  .brand-lockup small { display: none; }
}

/* ---- wide viewports: a dock, not a strip ------------------------------------------------------
   A five-item tab bar spanning a 1440px window is a phone idiom in the wrong place. But the answer
   is not to move it to the top either: the top row here is already the wordmark on the left and the
   account controls on the right at EVERY width, and a nav squeezed between them collides somewhere
   around 1000px — which is precisely the range a desktop browser window actually gets resized to.

   So the bar keeps its edge and gives up its width. Above 860px it narrows to the dashboard's own
   column, lifts clear of the bottom edge, closes into a rounded dock and lays each item out as an
   icon beside its label rather than above it. It then belongs to the 760px dashboard it navigates,
   lines up with the cards, and stops reading as a phone bar that happens to be very long.
   860 is a breakpoint this file already uses; it is not a fifth one invented here. */
@media (min-width: 860px) {
  /* 86 is the dock measured, not guessed: 18px of lift plus 52 of item plus 6 of padding top
     and bottom is 82 from the bottom edge, and --shell-bottom spends 14 more on top of this,
     which leaves the same 18px between the dashboard's last row and the dock at every width. */
  :root { --lobby-nav-h: 86px; }

  .lobby-nav {
    right: auto;
    bottom: calc(18px + var(--safe-bottom));
    left: 50%;
    width: min(760px, calc(100vw - 28px - var(--safe-left) - var(--safe-right)));
    padding: 6px;
    border: 1px solid var(--lobby-border);
    border-radius: var(--lobby-radius-button);
    box-shadow: 0 18px 44px rgba(0, 0, 0, .42);
    transform: translateX(-50%);
  }

  .lobby-nav-item {
    min-height: 52px;
    padding: 8px 6px;
    grid-auto-flow: column;
    align-items: center;
    justify-content: center;
    gap: 9px;
    border-radius: calc(var(--lobby-radius-button) - 8px);
  }

  .lobby-nav-item svg { width: 21px; height: 21px; }
  .lobby-nav-label { font-size: 11.5px; }

  /* The underline becomes a fill: a 3px rule under an item inside a rounded dock points at the
     dock's own border rather than at the item. Colour, weight and a lit ground — still three. */
  body:has(#home-panel:not(.is-hidden)) #lobby-nav-home::after,
  body:has(#leaderboard-panel:not(.is-hidden)) .lobby-nav #leaderboard-open::after,
  body:has(#friends-panel:not(.is-hidden)) .lobby-nav #friends-open::after,
  body:has(#shop-panel:not(.is-hidden)) .lobby-nav #shop-open::after {
    z-index: -1;
    inset: 0;
    right: 0;
    left: 0;
    height: auto;
    border-radius: inherit;
    background: color-mix(in srgb, var(--ui-accent) 16%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-accent) 40%, transparent);
  }

  .lobby-nav-item { isolation: isolate; }
  .lobby-top-actions { top: calc(20px + var(--safe-top)); }
}

@media (prefers-reduced-motion: reduce) {
  /* Take away the MOVEMENT, not the FEEDBACK: killing a transition does not kill a transform, so
     without this the reader who asked for less animation is the only one still being moved. */
  .lobby-nav-item, .lobby-bell, .lobby-top-actions .top-coin-pill { transition: none; }
  .lobby-nav-item:active, .lobby-bell:active, .lobby-top-actions .top-coin-pill:active { transform: none; }
  .brand-crown { animation: none; transform: translateX(-50%) rotate(-8deg); }
}

/* The Freepik credit for the cobra on the CTA, in the same footer as the independence line and set
   the same way — it is a sentence, not a destination.

   Its own rule for the link inside it, because `.lobby-legal a` gives every link in that footer
   12px of vertical padding and 12.5px semibold type to make a 44px target of it. That is right for
   Terms and Privacy, which are things you press, and wrong for two words inside a sentence: it made
   the credit read as a fifth nav link with prose either side. Underlined instead, which is what
   marks a link inside running text. */
.lobby-credit {
  flex-basis: 100%;
  margin: 0 0 2px;
  color: var(--text-dim, #94a0b8);
  font-size: 11px;
  line-height: 1.45;
  text-align: center;
}

.lobby-credit a {
  padding: 0;
  color: inherit;
  font-size: inherit;
  font-weight: 600;
  text-decoration: underline;
}

/* =================================================================================================
   WIDE VIEWPORTS: A PANEL IS A WINDOW, NOT A PHONE COLUMN

   Reported as "the windows don't scale right, such as Leaderboard", and measured in Chromium at
   1440x900 and 1920x1080 before touching anything. Every .menu-panel was `min(520px, …)` at every
   width, so on a 1920 window the leaderboard was a 520px card with 1400px of arena either side, the
   shop was a shelf of ninety items four tiles across, and the season track was a fifty-rung ladder
   in a phone column — 5249px of content scrolling inside 908px of panel.

   The number is not invented. This page already has a desktop column and the whole lobby is built
   on it: the home dashboard is 760px (`.lobby`), the winners board is 760px, and — since the
   navigation bar arrived — THE DOCK IS 760px TOO. So a 520px panel was the one thing on the screen
   that did not line up with anything else: open the leaderboard and the card sat 120px narrower
   than the bar underneath it, which is most of what "doesn't scale right" looks like. Above 860px,
   which is where the bar becomes that dock, the lobby's screens join the column they navigate.

   Not the builder (780px, its own rule and its own reason) and not the party lobby (980px with a
   two-column body above 900px) — both already chose a desktop width deliberately. Not
   `.inner-window` either: How to play is PROSE, and prose at 560px is the right measure at any
   window size. Widening a paragraph is not the same favour as widening a table.

   Not the end or pause panels, which are dialogs over a running round rather than screens; they
   keep the 520 the shared rule gives them, which is why this lists the panels rather than saying
   `.menu-panel`. ================================================================================= */
@media (min-width: 860px) {
  .leaderboard-panel,
  .recent-games-panel,
  .friends-panel,
  .inbox-panel,
  .season-panel,
  .shop-panel {
    width: min(760px, calc(100vw - 56px - var(--safe-left) - var(--safe-right)));
  }
}

/* The winners board is not in that list because it already chose this exact width, in this exact
   expression, hundreds of lines above — it is where the 760 was measured. It is here as a comment
   rather than as a seventh selector so that nobody adds it and quietly creates a second place the
   number lives. */

/* The shop is the one screen where the extra room is the POINT rather than a courtesy: it is a
   shelf, and a shelf's job is to show you what is on it. At 520px the featured aisle was two rows
   of four 78px tiles; the room here is spent on bigger art rather than on more of it, because the
   art is what somebody is deciding from. 1180 rather than 860 so the wider panel only appears when
   the window can hold it without crowding the arena behind. */
@media (min-width: 1180px) {
  .shop-panel {
    width: min(980px, calc(100vw - 56px - var(--safe-left) - var(--safe-right)));
  }
}

/* A 2x desktop screenshot can report a ~1024px CSS viewport. Give the shop its desktop shelf
   layout from 900px rather than making it fall back to the phone column and crop its cards. */
@media (min-width: 900px) {
  .leaderboard-panel, .recent-games-panel, .friends-panel, .inbox-panel, .season-panel,
  .shop-panel, .scoreboard-panel { width: min(1100px, calc(100vw - 40px - var(--safe-left) - var(--safe-right))); }
  .shop-panel {
    max-height: calc(100dvh - 32px - var(--safe-top) - var(--safe-bottom));
    grid-template-rows: auto auto auto minmax(260px, 1fr);
  }
  .shop-top { display: grid; grid-template-columns: 420px minmax(0, 1fr); align-items: center; column-gap: 24px; }
  .shop-top .panel-head { grid-column: 1 / -1; }
  .shop-panel .snake-preview { grid-column: 1; margin-inline: 0; }
  .shop-fitting { grid-column: 2; justify-content: flex-start; text-align: left; padding-block: 0; }
  .shop-fitting-hint { text-align: left; }
  .shop-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
  .shop-card { min-height: 170px; padding: 14px 10px; }
  .shop-aisles .builder-tab { flex: 1 1 auto; min-width: 92px; }
}

@media (min-width: 860px) {
  .shop-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
}

/* The preview stops being a letterbox.

   `.snake-preview` is `width: 100%; height: 132px` at every width, and the snake inside it is
   drawn by `drawPreviewCanvas` at `min(width / 320, height / 150)` — the artwork is 320x150 in its
   own units, so the HEIGHT is what binds, at every width, always. Widening the panel therefore made
   the box wider and the snake not one pixel bigger: measured in the builder at 1440, a 740x130 box
   with a 255px snake in the middle of it, two thirds of the hero being black. That is the "looks a
   bit unusual on desktop" — the picture was authored for a 430px phone and stretched into a band.

   So above 860 the box takes the artwork's own proportions instead of the panel's: 420x190 is
   2.21:1 against the artwork's 2.13:1, and the snake comes out at 373x175 inside it. Centred rather
   than full-bleed, because a hero is a framed picture and a band is not. Phones are untouched —
   below 860 the box is still 132px tall, which is the vertical room a phone actually has. */
@media (min-width: 860px) {
  .snake-preview {
    width: min(100%, 420px);
    height: 190px;
    margin-inline: auto;
  }
}

/* -------------------------------------------------------------------------------------------
   "It's not clear the user can scroll on the home page."

   The dashboard has had a scroller and a cap since the day it stopped fitting (see the block
   above about the transform ladder that used to hide the overflow). What it has never had is
   anything that SAYS so. Measured at 1440x900: ~950px of content in a 658px box, with the fold
   landing halfway down the three quick-action cards and the floating dock over them — so the one
   visual clue on the page, a sliced row, read as "the bar is covering the end" rather than "there
   is more". Both platforms this ships on hide the scrollbar until something is already scrolling,
   so there was nothing else to go on.

   Two marks, and neither of them is a decoration:

   The FADE is on `.has-more-below`, which app.js writes from the box's own measurement and takes
   off the moment the last row is reached. A fade under a lobby that already fits would be a
   promise of content that is not there. A mask rather than an overlay strip because the thing
   behind this column is a live arena — a gradient to any solid colour would paint a grey band over
   a moving game, and fading the CONTENT to transparent is the only version of this that reveals
   what is actually underneath.

   The SCROLLBAR is for the desktop specifically, and it is the half the owner was missing: on a
   mouse, a visible track is what tells you a box scrolls before you have tried. Declaring
   ::-webkit-scrollbar is what turns a macOS overlay scrollbar into a permanent one. Pointer-fine
   only, because on a phone a permanent 10px gutter is 10px of column taken to say something a
   thumb finds out by touching. -------------------------------------------------------------- */
#home-panel .lobby-body.has-more-below {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 58px), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 58px), transparent);
}

@media (pointer: fine) {
  /* Deliberately NOT `scrollbar-width` / `scrollbar-color`. The standard pair and the -webkit-
     pseudo-elements are not additive: Chromium honours the standard properties when both are
     present and drops the pseudo-element styling entirely, and `scrollbar-width: thin` on macOS is
     still an OVERLAY scrollbar — invisible until something is already moving, which is the whole
     problem. Measured: with both declared, offsetWidth - clientWidth came back 0. Styling the
     pseudo-element ALONE is what turns it into a permanent track, in Chromium and in WebKit, which
     between them is nearly every desktop visitor. Firefox has no way to force one, and keeps the
     fade above, which is why the fade is not the optional half of this. */
  #home-panel .lobby-body::-webkit-scrollbar { width: 10px; }
  #home-panel .lobby-body::-webkit-scrollbar-track { background: transparent; }

  /* The border is a transparent inset rather than a margin: a scrollbar thumb has no box model of
     its own to inset with, and `background-clip: padding-box` is what makes the border read as
     padding around a 4px rail. */
  #home-panel .lobby-body::-webkit-scrollbar-thumb {
    border: 3px solid transparent;
    border-radius: 999px;
    background: rgba(255, 255, 255, .3);
    background-clip: padding-box;
  }

  #home-panel .lobby-body::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, .46); background-clip: padding-box; }
}

/* The "More" control that goes with the fade above. Hidden until the lobby's own measurement says
   there is something under the fold — see `.has-more-below`, and `syncLobbyScrollCue` in app.js.

   Placed against the lobby column rather than the viewport so it lands ON the fade, at the bottom
   of the thing that scrolls, and moves with the column at every width. `.lobby` is
   `overflow: visible !important` and had no position of its own; it gets one here, which is also
   what keeps this above the faded row rather than behind it. */
.lobby { position: relative; }

.lobby-more {
  position: absolute;
  z-index: 3;
  bottom: 2px;
  left: 50%;
  display: none;
  align-items: center;
  gap: 6px;
  min-height: 34px;
  padding: 0 14px;
  border: 1px solid var(--lobby-border, rgba(255, 255, 255, .12));
  border-radius: 999px;
  background: rgba(9, 16, 15, .88);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--lobby-text, #fff);
  font: inherit;
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  cursor: pointer;
  transform: translateX(-50%);
}

/* Never neon, and never green: green is Classic and "live now" on this page, and a scroll
   affordance is neither. It is chrome, so it is the chrome's own muted text. */
.lobby-more svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2.2;
}

/* Pointer-fine only, and that is the whole reason it exists: the complaint was a desktop one, and
   a mouse is the input with no way to discover an overlay scrollbar short of trying to drag
   something. A thumb scrolls a page by reflex and does not need to be told; on a 500px phone this
   pill would only stand on the very row it is pointing at, in the little vertical room a phone has
   to spare. The fade is not restricted — that one helps everybody. */
@media (pointer: fine) {
  body:has(#lobby-body.has-more-below) .lobby-more { display: inline-flex; }
}

/* Browsing rounds takes the whole page: the wordmark, the account capsule, the cog and the
   navigation bar all go, and the shell itself starts scrolling instead of the dashboard's own box.
   So this goes with them. Left in, it was measured standing on "Paste the ID shown in your
   friend's warm-up lobby" and pointing at a scroller that is no longer the one moving — which is a
   worse answer to "can I scroll here" than saying nothing. Same condition as the bar's, one line
   below it in spirit; kept here rather than added to that rule because the element is this
   block's. */
body:has(#rounds-drawer:not([hidden])) .lobby-more { display: none !important; }

@media (hover: hover) {
  .lobby-more:hover { border-color: rgba(255, 255, 255, .3); background: rgba(16, 26, 24, .94); }
}

.lobby-more:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }

/* =================================================================================================
   ABOVE 1180: A LOBBY SCREEN IS THE PAGE, NOT A CARD ON IT

   "Really don't like that the web app looks to have an iframe, just feels unnatural on a bigger
   screen, the shop is cropped etc."

   Measured in Chromium at 1920x1080 before anything here was written. Every screen was a card
   floating in the middle of a live arena: the leaderboard 760px wide with 580px of moving game
   either side, the friends list a 132px-tall box at 1440 with its sixth row sliced through by the
   panel's bottom edge, the season track 5233px of content scrolling inside 908px of panel, and the
   shop 980px wide with 470 either side. An earlier pass widened the panels from 520 to 760 and the
   shop to 980. It treated the symptom: a wider card is still a card.

   THE MEASUREMENT THAT DECIDED THE SHAPE OF THIS. The home dashboard, at the same 760px and the
   same 1920 window, does NOT read as an iframe — and it is the one screen that runs from under the
   wordmark down to the dock. The panels stopped somewhere in the middle. So the thing that reads as
   an embedded frame is not the width, it is a box with a live game visible above it, below it and
   on both sides at once. Height is the fix and width is the finish.

   Four changes, and each answers one of the four complaints:

   1. THE PANELS FILL THE PAGE'S HEIGHT. `align-self: stretch` against the shell, which is
      `position: fixed; inset: 0` — so the row is already exactly the room between the wordmark and
      the dock, and no new number is introduced. `.menu-panel`'s existing `max-height` is that same
      room written out, so the two agree by construction. `align-content: start` keeps the contents
      at the top of the taller box rather than letting the panel's own auto rows share out the
      slack, which would space a header away from the list under it.

   2. THE CROP BECOMES THE PAGE'S EDGE. A row cut in half by a panel's bottom border reads as
      content that has been cut off; the same row cut by the bottom of the window reads as a page
      you scroll. Same pixels, opposite meaning. That is the whole of "the shop is cropped".

   3. THE ROOM IS SPENT ON MORE COLUMNS, NOT A WIDER ROW. The column is 1280 rather than the
      window, because a leaderboard row 1800px wide with a name at one end and a score at the other
      is worse than the 760 it replaced. What the extra 520px buys is a second column: twenty ranks
      in two columns of ten, each column 616px — still wider than the 560 this page treats as a
      readable measure — and the season's fifty rungs in two columns of twenty-five instead of
      5233px of ladder scrolling through 908px of panel. The shop is the one screen where the room
      goes to the grid itself: seven tiles across instead of five, which is what a shelf is for.

   4. THE ARENA STOPS BEING A GAME RUNNING AROUND A BOX. A scrim under the panel, so the page has a
      ground and the arena is a field behind it rather than a second thing happening at the same
      time. Not a filter on `#game`: that canvas repaints every frame and a filter would be
      re-applied on every one of them. A composited translucent quad costs one layer.

   The breakpoint is 1180 and not 860, because 1180 is the narrowest window in which a list can be
   split into two columns that are each still a real column: at 860 the halves come out under 400px
   and the meta on a leaderboard row stops fitting on one line. Phones and tablets are untouched —
   everything in this pass is inside this query, and the panels measure the same at 500px wide
   before it and after it, to the pixel.

   NOT the how-to-play window: it is prose, 560px is the right measure at any width, and widening a
   paragraph is not the same favour as widening a table. NOT the end or pause panels, which are
   dialogs over a running round and are supposed to look like dialogs.
   ================================================================================================= */

@media (min-width: 1180px) {
  :root {
    /* One column for the lobby's SCREENS. 96 is the page's own margin, doubled.

       The home dashboard, the dock and the wordmark are deliberately NOT in it, and that was
       measured rather than assumed: with `.lobby` widened to 1500 at 1920 the two mode cards became
       740x210 hollow rectangles with a line of type in the middle of each, PLAY ONLINE a 1500px
       banner, and the three quick-action cards 490px wide and empty. The dashboard is a menu of
       eight controls and 760 is the width those controls want; there is nothing there to spend the
       room on. So home keeps its column and the screens get their own, concentric with it — a page
       with a header, a wide content area and a floating toolbar, all on one axis.

       1280 rather than the window, because the point is more COLUMNS and not wider rows: at 1280 a
       two-column list gives each column 616px, which is still wider than the 560 this page already
       treats as a readable measure for prose, and the shop's shelf reaches seven tiles across
       against five at 980. */
    --page-column: min(1280px, calc(100vw - 96px - var(--safe-left) - var(--safe-right)));
  }

  .leaderboard-panel,
  .recent-games-panel,
  .friends-panel,
  .inbox-panel,
  .season-panel,
  .shop-panel,
  .scoreboard-panel {
    width: var(--page-column);
  }

  /* Full height, and the contents held at the top of it.

     `stretch` rather than a height of its own: the shell is fixed to all four edges, so its row is
     already the space between the wordmark and the dock, and asking for that space by name would be
     the sixth number `--shell-top` carries a comment about. */
  .leaderboard-panel,
  .recent-games-panel,
  .friends-panel,
  .inbox-panel,
  .season-panel,
  .shop-panel,
  .scoreboard-panel {
    align-self: stretch;
    align-content: start;
  }

  /* The party lobby and the builder are deliberately NOT in that list, and it is the same reason
     they are not in the width list: both already chose their own desktop shape.

     The builder never needed it — measured at 1920 it is 8381px of content in a 910px panel, so it
     is at the cap already and `stretch` would be a no-op. The party lobby is the case that says why
     the rule is not "every panel": its first screen is a heading, one button and one code box, and
     stretched to 1080 that is 500px of empty glass. A screen becomes the page when it has a page
     of things on it; a form does not, and a form in a tall empty box is a worse answer than the
     card it replaced. */

  /* The shop's shelf is the part that scrolls, and the snake you are dressing stays put.

     Four rows: the pinned top (heading, the snake, what you are trying on), the aisles, the aisle's
     one line of copy, and the shelf. Only the last one takes the slack, so a tall window shows more
     of the shelf rather than pushing the tabs down the page. `.shop-pages` was already
     `overflow-y: auto; min-height: 0` for exactly this and had never had a row to be flexible in.

     A floor of 220px on that last row rather than `minmax(0, 1fr)`, because a wide window can also
     be a short one: at 1280x600 the room left after the band, the aisles and the blurb comes to
     eighteen pixels, and a shelf eighteen pixels tall is a shelf nobody can shop from. Below the
     floor the rows overflow and `.menu-panel`'s own scroller takes over, which is what every one of
     these panels did before this rule existed. */
  .shop-panel {
    grid-template-rows: auto auto auto minmax(220px, 1fr);
  }

  /* Seven tiles across instead of five. The tile floor is unchanged — the art is the thing somebody
     is deciding from and it does not get smaller — so the room buys more of them. */
  .shop-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }

  /* The snake you are dressing stops being a 400px-tall column on top of the shelf.

     Stacked, `.shop-top` spent the heading, a 190px picture, the caption under it and the fitting
     line on four separate lines — measured at 1440, 400px of a 658px panel, which left room for one
     and a bit rows of tiles and put the crop through the middle of the second row. That IS "the
     shop is cropped": the shelf was being squeezed by the hero above it, not by the panel.

     Side by side, the picture keeps its full 420x190 and the fitting line moves beside it into room
     that was empty, so the whole band costs about 250px and the shelf gets the rest. The heading
     spans both, because the close button belongs at the top right of the panel and nowhere else. */
  .shop-top {
    display: grid;
    grid-template-columns: 420px minmax(0, 1fr);
    align-items: center;
    column-gap: 24px;
  }

  .shop-top .panel-head { grid-column: 1 / -1; }
  .shop-panel .snake-preview { grid-column: 1; margin-inline: 0; }

  .shop-fitting {
    grid-column: 2;
    justify-content: flex-start;
    text-align: left;
    padding-block: 0;
  }

  .shop-fitting-hint { text-align: left; }

  /* The shelf says it is a shelf.

     A row of tiles sliced through by the bottom of the box is what "the shop is cropped" looked
     like, and it looks the same whether the box scrolls or not — which is the whole problem, since
     it now does. The dashboard already answered this exact question a few hundred lines above: fade
     the CONTENT to transparent rather than laying a strip over it, because what is behind this
     column is a live arena and a gradient to any solid colour would paint a band across a moving
     game. Only while there is something below — `has-more-below` is written by the shelf's own
     measurement in syncScrollCue and comes off at the last row, and a fade under a shelf that
     already fits is a promise of tiles that are not there.

     Desktop only, because a phone's shop is not being changed by this pass at all. */
  .shop-pages.has-more-below {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 54px), transparent);
    mask-image: linear-gradient(to bottom, #000 calc(100% - 54px), transparent);
  }

  /* Six stats, each a label over its number.

     `repeat(3, 1fr)` put twelve children — six <dt> and six <dd> — into three tracks, so the labels
     and the numbers interleaved: the row read "SCORE 41,230 ROUNDS" with "96" underneath SCORE and
     belonging to ROUNDS. It is wrong at every width and merely unmissable at this one, where the
     three tracks are 400px each. Flowing down columns instead puts each <dd> under its own <dt>.
     Left alone below 1180: six columns do not fit a phone, and this pass is not allowed to move
     anything a phone renders. */
  .leaderboard-me-stats {
    grid-template-columns: repeat(6, minmax(0, 1fr));
    grid-template-rows: auto auto;
    grid-auto-flow: column;
  }

  /* ---- the lists become two columns ---------------------------------------------------------
     Multi-column and not a two-track grid, and the difference matters: a grid flows 1,2 across then
     3,4, so a leaderboard would read 1 2 / 3 4 down the page. Columns flow DOWN the first and then
     down the second, which is ranks 1–10 beside ranks 11–20 — the way a printed table of results is
     set. `break-inside: avoid` because a row split across a column boundary is a row nobody can
     read, and the gap moves to a margin because multi-column has no `gap` between siblings. */
  #leaderboard-list,
  #friends-list,
  #inbox-list,
  #recent-games-list,
  #season-track {
    display: block;
    column-count: 2;
    column-gap: 16px;
  }

  #leaderboard-list > *,
  #friends-list > *,
  #inbox-list > *,
  #recent-games-list > *,
  #season-track > * {
    break-inside: avoid;
    margin-bottom: 8px;
  }

  /* ---- the arena goes quiet ------------------------------------------------------------------
     Only while one of the screens is open, and never during a round: the home dashboard's live
     arena is the page's own character and this would take it away. Absolutely positioned so it is
     not a second grid item, and behind the panel because the panel's own backdrop blur has to have
     something to blur.

     How to play is on the list even though its width is not: it is the one screen that keeps its
     560px measure at every size, so it is also the one most obviously a small box over a running
     game. Prose does not get wider, but it does get a ground to stand on. `.inner-window` is
     z-index 8 against a shell with none, so it stands above this without being told to. */
  body:not(.is-playing):has(.menu-panel:not(.is-hidden)) .menu-shell::before,
  body:not(.is-playing):has(.inner-window:not(.is-hidden)) .menu-shell::before {
    position: absolute;
    z-index: 0;
    inset: 0;
    background: rgba(3, 8, 10, .62);
    pointer-events: none;
    content: "";
  }

  .menu-panel { position: relative; z-index: 1; }
}
/* Pastel lobby override. These selectors sit at the end because several legacy desktop rules
   intentionally use fixed dark glass; a light theme must win over those rules unambiguously. */
html[data-theme-tone="light"] .team-deathmatch,
html[data-theme-tone="light"] .lobby-mode,
html[data-theme-tone="light"] .lobby-card .live-games,
html[data-theme-tone="light"] .lobby-chip,
html[data-theme-tone="light"] .lobby-nav,
html[data-theme-tone="light"] .lobby-solo {
  background: rgba(255, 255, 255, .78) !important;
  color: #17202A !important;
}
html[data-theme-tone="light"] .team-deathmatch strong,
html[data-theme-tone="light"] .team-deathmatch p,
html[data-theme-tone="light"] .lobby-card .live-games,
html[data-theme-tone="light"] .lobby-chip,
html[data-theme-tone="light"] .lobby-nav .lobby-nav-label {
  color: #17202A;
}
html[data-theme-tone="light"] body:not(.is-playing):has(.menu-panel:not(.is-hidden)) .menu-shell::before {
  background: rgba(255, 255, 255, .08) !important;
}
html[data-theme-tone="light"] .quick-settings-toggle,
html[data-theme-tone="light"] .account-menu-toggle,
html[data-theme-tone="light"] .rounds-strip,
html[data-theme-tone="light"] .lobby-mode-icon {
  background: rgba(255, 255, 255, .82) !important;
  color: #17202A !important;
}
html[data-theme-tone="light"] .lobby-mode .lobby-mode-badge,
html[data-theme-tone="light"] .lobby-mode-icon {
  background: rgba(255, 255, 255, .72) !important;
}
html[data-theme-tone="light"] .rounds-strip-copy strong,
html[data-theme-tone="light"] .rounds-strip-copy small { color: #17202A !important; }
html[data-theme-tone="light"] .lobby-more {
  background: rgba(255, 255, 255, .86) !important;
  color: #17202A !important;
  border-color: rgba(23, 32, 42, .18);
}

/* The browser lobby is a page, not an embedded-looking 760px viewport.  Keep the
   controls on one readable axis, but let that axis use the desktop room so the
   arena is not visible as a tall strip on either side of every button. */
@media (min-width: 1180px) {
  /* Leave a clear breathing band below the wordmark and slogan. The lobby used
     to start beneath the logo's nominal box, while the lockup's slogan extends
     visually below it and collided with the mode cards. */
  body:not(.is-playing):has(#home-panel:not(.is-hidden)) .menu-shell {
    --lobby-top: 214px;
    --shell-top: var(--lobby-top);
    padding-top: calc(var(--lobby-top) + var(--safe-top));
  }

  #home-panel.lobby {
    width: min(1100px, calc(100vw - 96px - var(--safe-left) - var(--safe-right))) !important;
    max-width: 1100px !important;
  }

  #home-panel.lobby .lobby-controls {
    width: 100%;
  }
}

@media (min-width: 1180px) and (max-height: 850px) {
  body:not(.is-playing):has(#home-panel:not(.is-hidden)) .menu-shell {
    --lobby-top: 170px;
    --shell-top: var(--lobby-top);
    padding-top: calc(var(--lobby-top) + var(--safe-top));
  }
}

/* The home dashboard has individual themed controls; the old encompassing glass
   pseudo-element makes the top of the Classic card look like a dark overlay. */
#home-panel.lobby::before {
  display: none !important;
  content: none !important;
}

/* Keep the mode group from reintroducing the retired dark glass through an
   earlier `.lobby-modes` rule. Only the two cards themselves should paint a
   surface. */
#home-panel.lobby .lobby-modes {
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

/* The lobby body is its own scroll viewport. Give its first row a little
   breathing room so the top edge of a mode card cannot be clipped by that
   viewport when the page is opened or after returning from another tab. */
#home-panel.lobby .lobby-body {
  padding-top: 18px !important;
}

@media (min-width: 900px) {
  /* The builder is a focused editor. Keep it high on desktop; its panel is layered above the
     fixed wordmark below so the logo becomes background decoration instead of an overlay. */
  body:not(.is-playing):has(#builder-panel:not(.is-hidden)) .menu-shell {
    --lobby-top: 78px;
    --shell-top: var(--lobby-top);
    padding-top: calc(var(--lobby-top) + var(--safe-top));
    align-items: start;
  }

  body:not(.is-playing):has(#builder-panel:not(.is-hidden)) .site-brand {
    z-index: 0;
  }

  body:not(.is-playing):has(#builder-panel:not(.is-hidden)) #builder-panel {
    position: relative;
    z-index: 2;
    margin-top: -32px;
  }

  body:not(.is-playing):has(#builder-panel:not(.is-hidden)) .account-menu-toggle,
  body:not(.is-playing):has(#builder-panel:not(.is-hidden)) .quick-settings-toggle {
    display: none !important;
  }

  .shop-panel {
    width: min(1500px, calc(100vw - 40px - var(--safe-left) - var(--safe-right))) !important;
    max-width: 1500px !important;
    min-height: min(760px, calc(100dvh - 170px - var(--safe-top) - var(--safe-bottom)));
    max-height: calc(100dvh - 130px - var(--safe-top) - var(--safe-bottom)) !important;
  }

  .builder-panel {
    width: min(1500px, calc(100vw - 40px - var(--safe-left) - var(--safe-right))) !important;
    max-width: 1500px !important;
    min-height: min(620px, calc(100dvh - 170px - var(--safe-top) - var(--safe-bottom)));
    max-height: calc(100dvh - 130px - var(--safe-top) - var(--safe-bottom)) !important;
    /* The editor is a catalogue, so use the same generous desktop reading width as Shop and
       keep its own header clear of the scroll viewport. */
    padding: 0 24px 24px;
    grid-template-columns: minmax(360px, 0.78fr) minmax(0, 1.22fr);
    align-content: start;
    position: relative;
    overflow: hidden;
    align-self: start;
    height: min(720px, calc(100dvh - 220px - var(--safe-top) - var(--safe-bottom)));
  }

  .builder-panel::after {
    content: "";
    position: absolute;
    z-index: 1;
    top: 0;
    bottom: 0;
    left: 39.5%;
    width: 1px;
    background: var(--panel-border);
    pointer-events: none;
  }

  /* Desktop builder: the thing being dressed stays visible on the left while the catalogue is
     worked through on the right. The panel itself remains the one scroll container, so keyboard
     and wheel users still have a single predictable surface. */
  .builder-panel > .builder-top {
    grid-column: 1;
    grid-row: 1;
    align-self: start;
    position: sticky;
    top: 0;
    margin: 0;
    padding: 18px 24px 24px 0;
  }

  .builder-panel > .standing,
  .builder-panel > .roster-row {
    grid-column: 1;
    min-width: 0;
  }

  .builder-panel > .standing { grid-row: 2; }
  .builder-panel > .roster-row { grid-row: 3; }
  .builder-panel > .builder-tabs,
  .builder-panel > .builder-pages {
    grid-column: 2;
    min-width: 0;
  }

  .builder-panel > .builder-tabs {
    position: absolute;
    z-index: 2;
    top: 18px;
    right: 24px;
    left: calc(39.5% + 24px);
    margin: 0;
  }

  .builder-panel > .builder-pages {
    position: absolute;
    top: 88px;
    right: 24px;
    bottom: 24px;
    left: calc(39.5% + 24px);
    margin: 0;
    max-height: none;
  }

  /* Only the catalogue scrolls. Preview, progress/sign-in and the add-snake roster stay in the
     left pane as an always-visible anchor while somebody browses dozens of flags or accessories. */
  .builder-panel > .builder-pages {
    overflow-y: auto;
    max-height: calc(100dvh - 190px - var(--safe-top) - var(--safe-bottom));
    padding-right: 10px;
    scrollbar-gutter: stable;
  }

  .builder-panel > .builder-tabs {
    align-self: start;
    height: max-content;
    margin-inline: 8px;
    padding: 8px;
    gap: 6px;
  }

  /* Flags are artwork, not tiny text labels. Let the catalogue use additional columns on a wide
     screen and scale the flag mark/bands with the tile so a 2x desktop viewport does not produce
     three enormous cards with a postage-stamp emoji in each one. */
  .builder-panel .skin-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 14px;
  }

  .builder-panel .skin-art {
    font-size: clamp(42px, 3vw, 72px);
    aspect-ratio: 2.4;
  }

  .builder-panel .skin-option {
    min-height: 130px;
  }

  .builder-panel .skin-bands {
    height: 10px;
  }

  /* Give the shop a proper shelf rhythm: fewer, larger cards are easier to
     scan and leave the artwork and price room to breathe. */
  .shop-panel .shop-grid {
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)) !important;
    gap: 18px !important;
  }

  .shop-panel .shop-card {
    min-height: 200px;
    padding: 18px 14px;
    gap: 9px;
  }

  .shop-panel .shop-aisles {
    gap: 10px;
  }

  .shop-panel .shop-aisles .builder-tab {
    padding-inline: 18px;
  }
}

/* Keep the Snake heading and preview in a solid, slightly tighter band. */
.builder-top {
  margin: 0 -18px;
  padding: 10px 18px 14px;
  isolation: isolate;
  overflow: clip;
}

.builder-panel {
  scroll-padding-top: 170px;
}

@media (min-width: 900px) {
  .builder-top {
    margin: 0;
    padding: 18px 24px 24px 0;
  }
}

/* Final builder layout contract. The two panes are explicit so the preview/loadout column cannot
   determine the height of the catalogue column, and the catalogue is the only scroll surface. */
@media (min-width: 900px) {
  body:not(.is-playing):has(#builder-panel:not(.is-hidden)) {
    overflow: hidden;
  }

  .builder-panel {
    display: grid;
    grid-template-columns: minmax(0, 40%) minmax(0, 60%);
    grid-template-rows: minmax(0, 1fr);
    width: min(1500px, calc(100vw - 40px - var(--safe-left) - var(--safe-right))) !important;
    height: min(720px, calc(100dvh - 170px - var(--safe-top) - var(--safe-bottom)));
    min-height: 0;
    max-height: calc(100dvh - 170px - var(--safe-top) - var(--safe-bottom)) !important;
    padding: 18px 24px 24px;
    gap: 0;
    overflow: hidden;
  }

  .builder-panel::after { display: none; }

  .builder-left-pane,
  .builder-customisation-pane {
    min-width: 0;
    min-height: 0;
  }

  .builder-left-pane {
    display: grid;
    grid-template-rows: auto auto auto;
    align-content: start;
    gap: 14px;
    padding-right: 24px;
    border-right: 1px solid var(--panel-border);
    overflow: hidden;
  }

  .builder-customisation-pane {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    gap: 12px;
    padding-left: 24px;
    overflow: hidden;
  }

  .builder-left-pane > .builder-top,
  .builder-left-pane > .standing,
  .builder-left-pane > .roster-row,
  .builder-customisation-pane > .builder-tabs,
  .builder-customisation-pane > .builder-pages {
    grid-column: auto;
    grid-row: auto;
    position: static;
    margin: 0;
  }

  .builder-left-pane > .builder-top {
    padding: 0 0 4px;
    overflow: visible;
  }

  .builder-customisation-pane > .builder-tabs {
    align-self: start;
    width: 100%;
    height: max-content;
    margin: 0;
    padding: 8px;
  }

  .builder-customisation-pane > .builder-pages {
    min-height: 0;
    max-height: none;
    height: auto;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 0 10px 28px 0;
    scrollbar-gutter: stable;
    overscroll-behavior: contain;
  }

  .builder-customisation-pane > .builder-pages .finder {
    position: sticky;
    top: 0;
    z-index: 3;
    padding-block: 6px;
    background: var(--panel-strong);
  }

  .builder-customisation-pane .builder-page {
    min-width: 0;
  }

  .builder-panel .snake-preview {
    width: min(100%, 560px);
    height: auto;
    aspect-ratio: 680 / 190;
    margin-inline: auto;
  }
}

/* The builder's escape hatch is a labelled control, not an unlabeled glyph. Keep it aligned with
   the sound control while reserving the randomiser for the loadout row where it acts. */
.builder-top .close-button {
  width: auto;
  min-width: 74px;
  padding-inline: 12px;
  gap: 6px;
}

.builder-top .close-button::before {
  display: none;
}

.builder-top .close-label {
  display: inline;
  font-size: 12px;
  font-weight: 800;
}

.builder-surprise {
  flex: 0 0 auto;
  min-height: 40px;
  padding-inline: 14px;
  white-space: nowrap;
  font-size: 12px;
}

@media (max-width: 899px) {
  .builder-surprise {
    min-height: 36px;
    padding-inline: 11px;
  }
}

@media (max-width: 899px) {
  .builder-left-pane,
  .builder-customisation-pane {
    min-width: 0;
  }

  .builder-customisation-pane {
    display: grid;
    gap: 12px;
    min-height: 0;
  }

  .builder-customisation-pane > .builder-pages {
    min-height: 0;
    overflow-y: auto;
    padding-bottom: calc(24px + var(--safe-bottom));
  }

  .builder-customisation-pane > .builder-pages .finder {
    position: sticky;
    top: 0;
    z-index: 3;
    padding-block: 6px;
    background: var(--panel-strong);
  }
}

@media (hover: hover) {
  /* Hover must repaint the complete mode card, not only its background colour.
     The previous colour-only rule left the card's upper gradient untouched, so
     a deselected card appeared to have a missing/dark top section. */
  #home-panel.lobby .lobby-mode:hover:not(.is-active) {
    border-color: color-mix(in srgb, var(--mode-accent) 72%, transparent) !important;
    background: linear-gradient(
      145deg,
      color-mix(in srgb, var(--mode-accent) 11%, var(--theme-mode-surface, var(--panel))),
      var(--theme-mode-surface, var(--panel))
    ) !important;
    box-shadow: 0 0 18px color-mix(in srgb, var(--mode-accent) 16%, transparent);
  }
}

/* Final light-theme cascade: keep pastel chrome opaque and readable. */
:root[data-theme-tone="light"] {
  --panel: rgba(255,255,255,.90);
  --panel-strong: rgba(255,255,255,.96);
  --panel-soft: rgba(255,255,255,.72);
  --panel-border: rgba(23,32,42,.18);
  --control-surface: rgba(255,255,255,.86);
  --control-hover: rgba(23,32,42,.06);
  --accent-ink: #17202A;
  --lobby-surface-1: rgba(255,255,255,.88);
  --lobby-surface-2: rgba(255,255,255,.94);
  --lobby-surface-3: rgba(255,255,255,.97);
  --lobby-border: rgba(23,32,42,.18);
  --lobby-text: #17202A;
  --lobby-text-secondary: #59647D;
  --lobby-text-muted: #6D7890;
}
:root[data-theme-tone="light"] .season-strip,
:root[data-theme-tone="light"] .rounds-strip,
:root[data-theme-tone="light"] .lobby-nav,
:root[data-theme-tone="light"] .lobby-solo,
:root[data-theme-tone="light"] .team-deathmatch,
:root[data-theme-tone="light"] .lobby-chip {
  background: var(--lobby-surface-1) !important;
  color: var(--lobby-text) !important;
  border-color: var(--lobby-border) !important;
  box-shadow: 0 10px 28px rgba(23,32,42,.08);
}
:root[data-theme-tone="light"] .lobby-nav-item,
:root[data-theme-tone="light"] .season-strip-chevron { color: var(--lobby-text-secondary) !important; }


/* The moment a rename lands, matching the app's .rename-said.is-good. One pulse, not a loop: a box
   that keeps breathing has stopped being an event and become a decoration. */
#account-rename-status.is-good {
  color: var(--lobby-green, #63ff48);
  animation: rename-landed 900ms ease-out 1;
}

@keyframes rename-landed {
  0% { text-shadow: 0 0 14px color-mix(in srgb, var(--lobby-green, #63ff48) 70%, transparent); }
  45% { text-shadow: 0 0 22px color-mix(in srgb, var(--lobby-green, #63ff48) 45%, transparent); }
  100% { text-shadow: none; }
}

/* The pulse goes for a reader who asked not to be moved; the colour, which carries the news, stays. */
@media (prefers-reduced-motion: reduce) {
  #account-rename-status.is-good { animation: none; }
}

/* The Handling dials — a development panel, and labelled as one so nobody mistakes it for a
   normal setting. It exists because how a snake feels cannot be reasoned to: three attempts were
   made at it from measurements and the turn rate was tuned wrong in both directions before the
   dials went in. The person holding the controls is the one who can tell. */
/* The tag is an <em> so that the heading beside it can stay a plain <h3>: SettingsParityTests
   reads headings as their bare text, and the app spells this section the same way. Not italic —
   <em> is chosen for the markup shape, and the shape it wears is this pill. */
.handling-tag {
  font-style: normal;
  margin-left: 0;
  margin-right: 6px;
  padding: 2px 7px;
  border: 1px solid color-mix(in srgb, var(--ui-accent) 45%, transparent);
  border-radius: 999px;
  color: var(--ui-accent);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  vertical-align: middle;
}

.handling-actions {
  display: flex;
  margin-top: 12px;
  flex-wrap: wrap;
  gap: 8px;
}

/* The readouts under each dial say what the number MEANS rather than what it is, so they are the
   part worth reading and want to be legible rather than incidental. */
#handling-block .slider-row small {
  color: var(--lobby-text-secondary, var(--text-dim));
  font-variant-numeric: tabular-nums;
}

/* The line under a drawer heading that explains what the section is for, rather than labelling a
   control. Used by the Handling panel to say, once, that it is development-only and solo-only. */
.drawer-note {
  margin: 0 0 10px;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1.45;
}

/* ---- the shop's wallet and its coin promo -------------------------------------------------- */

/* The wallet is one of the three things gold is allowed on in a shop — it is money. The plus lives
   inside it because "I cannot afford this" and "get me more" are one thought. */
.shop-wallet {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 4px 4px 12px;
  border: 1px solid rgba(255, 200, 61, .42);
  border-radius: 999px;
  background: rgba(255, 200, 61, .10);
}

.shop-wallet-amount {
  color: var(--brand-gold, #ffc83d);
  font-size: 15px;
  font-weight: 850;
  font-variant-numeric: tabular-nums;
}

/* 32 painted inside a 44 target: the pill would look clumsy at full size, and a tap that misses
   because the control was drawn honestly is still a missed tap. */
.shop-wallet-more {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  margin: -6px;
  padding: 6px;
  box-sizing: content-box;
  appearance: none;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--brand-gold, #ffc83d);
  font-size: 19px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}

.shop-wallet-more:active { transform: scale(.94); }

/* A door to the coin shelf, under the prices it answers. Purple, so the only gold on it is the
   button that leads to spending. */
.shop-coin-promo {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 14px;
  margin-top: 20px;
  padding: 14px 16px;
  border: 1px solid color-mix(in srgb, var(--brand-purple, #d83cff) 38%, transparent);
  border-radius: 22px;
  background:
    radial-gradient(90% 140% at 8% 50%,
      color-mix(in srgb, var(--brand-violet, #7a28ff) 22%, transparent), transparent 62%),
    var(--panel-soft);
}

@media (max-width: 420px) {
  .shop-coin-promo { grid-template-columns: auto minmax(0, 1fr); }
  .shop-coin-promo .promo-cta { grid-column: 1 / -1; justify-content: center; }
}

.shop-coin-promo .promo-art {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 13px;
  background: rgba(255, 200, 61, .16);
  color: var(--brand-gold, #ffc83d);
  font-size: 19px;
}

.shop-coin-promo .promo-words { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.shop-coin-promo .promo-title { margin: 0; font-size: 15px; font-weight: 800; }
.shop-coin-promo .promo-blurb { margin: 0; font-size: 12.5px; line-height: 1.35; color: var(--text-dim); }

/* Gold: this is the way to buying coins. */
.shop-coin-promo .promo-cta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 10px 15px;
  appearance: none;
  border: 0;
  border-radius: 999px;
  background: linear-gradient(135deg, var(--brand-gold, #ffc83d), #ffb020);
  color: #241800;
  font-size: 14px;
  font-weight: 800;
  white-space: nowrap;
  cursor: pointer;
}

.shop-coin-promo .promo-cta:active { transform: scale(.98); }

/* ---- the shop's featured hero and its sort control ----------------------------------------- */

/* The one large thing at the front of the shop. Dressed in the panel's own surface rather than a
   fixed dark card: the browser has the same eight themes the app does, and a near-black hero on a
   pastel page is the fault that was found on the phone. */
.shop-hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  padding: 16px;
  border: 1px solid color-mix(in srgb, var(--brand-secondary, #3bf5c8) 34%, var(--panel-border));
  border-radius: 24px;
  background:
    radial-gradient(120% 90% at 80% 45%,
      color-mix(in srgb, var(--brand-secondary, #3bf5c8) 12%, transparent), transparent 62%),
    var(--panel-soft);
}

@media (max-width: 380px) {
  .shop-hero { grid-template-columns: minmax(0, 1fr); }
  .shop-hero .hero-art { justify-self: center; }
}

.shop-hero .hero-words { display: flex; flex-direction: column; align-items: flex-start; gap: 7px; min-width: 0; }

.shop-hero .hero-badge {
  padding: 4px 9px;
  border-radius: 8px;
  background: linear-gradient(135deg, var(--brand-violet, #7a28ff), var(--brand-purple, #d83cff));
  color: #fff;
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.shop-hero .hero-title { margin: 0; font-size: clamp(20px, 5vw, 26px); font-weight: 900; line-height: 1.05; }
.shop-hero .hero-blurb { margin: 0; font-size: 13px; line-height: 1.4; color: var(--text-dim); }

/* Gold: it carries a price, and this is the button that spends. Solid rather than a tint over
   black — the tint turned olive on the pastel themes when the app tried it. */
.shop-hero .hero-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  margin-top: 2px;
  padding: 10px 15px;
  appearance: none;
  border: 0;
  border-radius: 999px;
  background: linear-gradient(135deg, var(--brand-gold, #ffc83d), #ffb020);
  color: #241800;
  font-size: 14.5px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.shop-hero .hero-cta:active { transform: scale(.98); }
.shop-hero .hero-art { width: 96px; height: 96px; }

.shop-blurb-line { display: flex; align-items: baseline; gap: 12px; }
.shop-blurb-line .shop-blurb { flex: 1; min-width: 0; }

.shop-sort {
  flex: 0 0 auto;
  min-height: 44px;
  padding: 8px 10px;
  appearance: none;
  border: 0;
  background: none;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}

.shop-sort:active { transform: scale(.97); }

/* ---- the reports somebody has filed --------------------------------------------------------- */

/* A list rather than cards: a record to scan, not a set of things to act on — there is nothing to
   do to a report once it is in. Matches the app's, which shows the same three words. */
.report-list { display: flex; flex-direction: column; gap: 8px; margin: 10px 0 0; padding: 0; list-style: none; }

.report-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 14px;
  background: var(--panel-soft);
}

.report-empty { color: var(--text-dim); font-size: 13px; }
.report-who { flex: 1; min-width: 0; font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.report-when { flex: 0 0 auto; font-size: 12px; color: var(--text-dim); }

.report-state {
  flex: 0 0 auto;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 700;
}

/* Three states, three words, and none of them names what was done. The word carries the meaning;
   colour is only a second signal. */
.report-state.is-open,
.report-state.is-closed { background: rgba(255, 255, 255, .10); color: var(--text-dim); }

.report-state.is-actioned {
  background: color-mix(in srgb, var(--brand-secondary, #3bf5c8) 16%, transparent);
  color: var(--brand-secondary, #3bf5c8);
}

/* ---- the leaderboard podium ----------------------------------------------------------------- */

/* Three places with the winner in the middle and raised. A list cannot answer "who is winning" at a
   glance, because rank 1 and rank 7 are the same shape; a podium can. The app draws the same three
   cards from the same data. */
.podium {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: end;
  gap: 10px;
  margin: 4px 0 18px;
}

/* The winner on its own line on a narrow screen, rather than three cards squeezed until none of
   them can be read. */
@media (max-width: 460px) {
  .podium { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .podium .is-first { grid-column: 1 / -1; }
}

.podium-place {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 14px 10px 16px;
  border: 1px solid var(--panel-border);
  border-radius: 22px;
  background: var(--panel-soft);
  text-align: center;

  /* Depth, so the three places sit ON the page rather than being drawn onto it. Two shadows: a
     tight one that reads as a lifted edge, and a wide soft one that is the light the card blocks.
     One shadow alone looks like a border somebody blurred. */
  box-shadow:
    0 1px 0 rgba(255, 255, 255, .05) inset,
    0 10px 24px rgba(0, 0, 0, .38);
  transition: transform 140ms ease, box-shadow 140ms ease;
  cursor: pointer;
}

.podium-place:active { transform: translateY(1px) scale(.99); }

/* Gold is spent here and almost nowhere else on this screen, which is what makes it mean champion
   rather than decoration. */
/* First is raised further and lit from its own gold rather than from the page. The halo is kept
   small deliberately: a card that glows over its neighbours makes THEM look broken, and gold means
   champion only while it is rare. */
.podium-place.is-first {
  padding-top: 30px;
  padding-bottom: 22px;
  margin-top: -10px;
  border-color: rgba(255, 200, 61, .6);
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(255, 200, 61, .16), transparent 62%),
    var(--panel-soft);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, .07) inset,
    0 14px 34px rgba(0, 0, 0, .46),
    0 0 26px rgba(255, 200, 61, .16);
}

/* Sits above the card rather than inside it, so the crown reads as worn rather than printed. */
/* The brand's crown, sitting on the card's edge as though worn. Big enough to be identifiable —
   at 22px the glyph it replaced was a smudge, and an ornament nobody can identify is noise on the
   one card that is supposed to be the clearest thing on the screen. No disc behind it: the crown
   has its own silhouette, and a plate would only mute it. */
.podium-crown {
  position: absolute;
  top: -22px;
  left: 50%;
  width: 46px;
  height: 38px;
  transform: translateX(-50%);
  filter: drop-shadow(0 3px 10px rgba(255, 200, 61, .45));
}

.podium-place.is-second { border-color: rgba(255, 255, 255, .28); }
.podium-place.is-third { border-color: rgba(205, 127, 50, .55); }

.podium-rank {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .12);
  font-size: 13px;
  font-weight: 900;
}

.podium-place.is-first .podium-rank {
  width: 30px;
  height: 30px;
  background: var(--brand-gold, #ffc83d);
  color: #241800;
  font-size: 15px;
}

.podium-name { font-size: 15px; font-weight: 800; overflow-wrap: anywhere; }
.podium-place.is-first .podium-name { font-size: 17px; }

.podium-score { font-size: 22px; font-weight: 900; font-variant-numeric: tabular-nums; }
.podium-place.is-first .podium-score { font-size: 28px; color: var(--brand-gold, #ffc83d); }

.podium-meta { font-size: 11.5px; color: var(--text-dim); }

/* A handle for recognising a row, not a portrait: the colour comes from the name. See
   boardAvatarColour for why it is not the player's cosmetic. */
.player-avatar {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  padding: 3px;
  border: 2px solid color-mix(in srgb, var(--ui-accent) 45%, transparent);
  border-radius: 999px;
  background: rgba(255, 255, 255, .06);
  overflow: hidden;
}

.player-avatar canvas,
.player-cobra { width: 100%; height: 100%; object-fit: contain; }

/* One cobra, recoloured per player. The turn comes from the same name hash the app uses, so a
   player is the same colour on the phone and in the browser. */
.player-cobra { filter: hue-rotate(var(--cobra-turn, 0deg)) saturate(1.15); }

.player-avatar.is-first { width: 76px; height: 76px; border-width: 3px; border-color: var(--brand-gold, #ffc83d); }
.player-avatar.is-second { width: 62px; height: 62px; border-color: rgba(255, 255, 255, .45); }
.player-avatar.is-third { width: 62px; height: 62px; border-color: #cd7f32; }

/* ---- leaderboard rows and the player sheet -------------------------------------------------- */

/* Five columns: rank, avatar, who, score, chevron. The score has its OWN column because it was
   buried mid-sentence in "1,769 · 24 rounds · 0 won" — the one number the board ranks on was the
   hardest thing on the row to find. */
.leaderboard-row {
  gap: 10px;
  min-height: 62px;
  padding: 8px 12px;
  cursor: pointer;
}

.leaderboard-row:active { transform: scale(.995); }

/* Just enough to separate a row from the panel behind it. Rows are scanned rather than admired, so
   they get a fraction of the podium's depth. */
.leaderboard-list .scoreboard-row {
  box-shadow: 0 4px 12px rgba(0, 0, 0, .22);
}

.player-avatar {
  box-shadow: 0 4px 12px rgba(0, 0, 0, .35);
}

.player-avatar.is-first { box-shadow: 0 6px 20px rgba(255, 200, 61, .28); }

.leaderboard-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.leaderboard-meta { font-size: 12px; color: var(--text-dim); }

.leaderboard-score {
  font-size: 18px;
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  color: var(--ui-accent);
}

.leaderboard-chevron { color: var(--text-dim); font-size: 16px; }

/* One player, opened from a row. The old board put a whole achievement section inside the page,
   which pushed the rankings off the screen for anybody not reading it. */
/* The panel, not just the contents. There is no shared .sheet card style in this stylesheet —
   .report-sheet carries its own — so a sheet that only said "flex column" drew its text straight
   onto the blurred page behind it, with no card at all. */
.board-player-sheet {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: min(100%, 460px);
  max-height: 100%;
  padding: 18px;
  overflow-y: auto;
  border: 1px solid var(--panel-border);
  border-radius: 18px;
  background: var(--panel-strong);
}

.board-player-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 10px;
  margin: 0;
}

/* Each figure is a block of its own — label above, number below — rather than a definition list
   read as two columns. Stacked, they scan as five statistics; side by side they scanned as a form. */
.board-player-stats > div { display: flex; flex-direction: column; }
.board-player-stats dt { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .05em; }
.board-player-stats dd { margin: 3px 0 0; font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; }

.board-player-awards { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }

/* The board's search, as one control rather than a labelled field and a button beside it. */
.board-search {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 14px;
  padding: 2px 14px;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  background: var(--panel-soft);
}

.board-search .search-mark { flex: 0 0 auto; color: var(--text-dim); font-size: 15px; }

.board-search input {
  flex: 1;
  min-width: 0;
  min-height: 44px;
  border: 0;
  background: none;
  color: var(--text);
  font-size: 15px;
}

.board-search input:focus { outline: none; }

/* ---- sharing a player's card ------------------------------------------------------------------
   Sits at the foot of the player sheet. The note is always in the DOM and usually empty, because
   it is an aria-live region: one created at the moment it has something to say is announced by
   nothing.
   ----------------------------------------------------------------------------------------------- */

.board-player-share-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: stretch;
  margin-top: 18px;
}

.board-player-share-note {
  margin: 0;
  min-height: 1.2em;
  font-size: 13px;
  text-align: center;
  color: var(--text-dim);
  word-break: break-all;
}

/* ---- the coin statement folds away -----------------------------------------------------------
   Every coin this account has gained or spent is hundreds of rows on a busy one, and it sat open
   in the middle of the profile drawer pushing everything under it off the bottom. Shut by default;
   the heading is the control.
   ----------------------------------------------------------------------------------------------- */

.statement-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.statement-toggle h3 { margin: 0; font: inherit; color: inherit; }

.statement-chev {
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1;
  transition: transform 160ms ease;
}

.coin-statement.is-open .statement-chev { transform: rotate(180deg); }

/* Hidden rather than unbuilt: the list is written by the same render that fills the rest of the
   drawer, and making it conditional would mean the rows arriving late the first time it is
   opened. */
.coin-statement:not(.is-open) #coin-statement-list,
.coin-statement:not(.is-open) #coin-statement-status { display: none; }

.statement-toggle:focus-visible { outline: 2px solid var(--ui-accent, #d83cff); outline-offset: 3px; }

/* The bin on a notice. Quiet and last: deleting is the least likely thing anybody came to the bell
   to do, and a full-size Delete beside the action button is a mis-tap that removes the message
   somebody opened it to read. */
.notice-bin {
  flex: 0 0 auto;
  align-self: start;
  width: 32px;
  height: 32px;
  border: 1px solid var(--line, rgba(255, 255, 255, .14));
  border-radius: 50%;
  background: transparent;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

.notice-bin:hover { color: var(--text); border-color: var(--text-dim); }
.notice-bin:focus-visible { outline: 2px solid var(--ui-accent, #d83cff); outline-offset: 2px; }
