/* ============================================================
   Emoji Match — "Arcade Night"
   Deep indigo night; white cards glow like game pieces.
   One accent (coral→amber) does all the work.
   ============================================================ */

@font-face {
  font-family: "Baloo 2";
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url("/fonts/baloo2-latin.woff2") format("woff2");
}

:root {
  --night: #15122b;
  --night-2: #221d49;
  --night-3: #2e2763;
  --edge: #3b3378;
  /* Raised chip surface for secondary controls. It exists so "quiet" does not
     have to mean "hollow outline" -- an outline-only button on this background
     reads as disabled, which is why every control had drifted to the accent
     gradient just to look enabled. */
  --chip: #2a2456;
  --chip-hover: #363073;
  --edge-soft: #4a4190;
  --cloud: #efecff;
  --dim: #a29ad1;
  --card-face: #fffdf6;
  --ink: #241f4d;
  --accent: #ff5e5b;
  --accent-2: #ffb03a;
  --good: #3ddc97;
  --bad: #ff4d6d;
  --flash-duration: 300ms;
  --display: "Baloo 2", "Trebuchet MS", sans-serif;
  --body: -apple-system, "Segoe UI", Roboto, sans-serif;

  /* Every "screen change" that leaves the current document — landing -> room,
     any exit to the main menu, Capacitor's location.reload() — hands the
     browser a moment with no styled page to paint. It fills that moment with
     the canvas colour, which defaults to white. Claiming the canvas here (plus
     <meta name="color-scheme"> in index.html, and backgroundColor in
     capacitor.config.ts for the WKWebView underneath) makes that moment night
     coloured instead of a white flash. Setting a background on the root also
     stops body's background from propagating to the canvas, so this same
     colour is what shows through an overscroll bounce. */
  color-scheme: dark;
  background-color: var(--night);
}

* {
  box-sizing: border-box;
}

/* WebKit fires double-tap-to-zoom on any surface that hasn't opted out, and a
   speed-tapping game manufactures accidental double taps constantly: two quick
   stabs at neighbouring emoji, or a miss followed by a retry, land close enough
   in time and space to read as one double tap and zoom the board.

   The selector is `*` because narrower ones do not work here. Per spec the
   effective touch-action is the intersection of the hit element and its
   ancestors, so `#gameView { touch-action: manipulation }` should have covered
   the cards inside it -- on an iPhone it did not, and double tap still zoomed
   the card. WebKit honours the property on the element actually hit, so every
   element has to carry it. Testing this on desktop is useless: Chrome has no
   double-tap-zoom gesture to suppress, and it reports the composed value the
   spec promises rather than the one WebKit acts on.

   `manipulation` drops double tap (and the legacy 300ms click delay) while
   leaving scrolling and pinch alone. Capacitor separately disables PINCH
   natively (zoomEnabled defaults to false), so the app ends up with no zoom at
   all -- an accessibility cost accepted deliberately, because accidental zoom
   mid-round made the game unplayable. The static document pages (privacy.html,
   support.html) use legal.css and are intentionally left zoomable. */
* {
  touch-action: manipulation;
}

body {
  margin: 0;
  font-family: var(--body);
  color: var(--cloud);
  background:
    radial-gradient(ellipse 90% 55% at 50% -10%, #372e7a55 0%, transparent 60%),
    radial-gradient(circle at 85% 95%, #ff5e5b14 0%, transparent 40%),
    var(--night);
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 18px;
  padding-top: max(18px, env(safe-area-inset-top));
  padding-bottom: max(18px, env(safe-area-inset-bottom));
  padding-left: max(18px, env(safe-area-inset-left));
  padding-right: max(18px, env(safe-area-inset-right));
}

.app {
  width: min(1040px, 100%);
}

.hidden {
  display: none !important;
}

/* ---------- headings & labels ---------- */

h2 {
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(1.7rem, 1rem + 2.2vw, 2.4rem);
  letter-spacing: 0.01em;
  margin: 0 0 10px;
}

h3 {
  font-family: var(--body);
  font-weight: 700;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--dim);
  margin: 22px 0 10px;
}

/* ---------- panels ---------- */

.view {
  background: linear-gradient(180deg, #262051 0%, var(--night-2) 100%);
  border: 1px solid var(--edge);
  border-radius: 20px;
  padding: clamp(20px, 3vw, 30px);
  margin-top: 12px;
  /* The inset hairline is the whole trick: a 1px light top edge makes the panel
     read as a surface catching light rather than a darker rectangle painted on
     a dark page. */
  box-shadow: 0 18px 40px #00000038, inset 0 1px 0 #ffffff12;
}

/* The board needs the full 1040px; a lobby holding 400px of controls inside it
   just reads as a fragment pinned to the left of a void. */
#nameView,
#lobbyView,
#resultsView {
  width: min(640px, 100%);
  margin-left: auto;
  margin-right: auto;
}

/* In-app view changes are display:none swaps, which land with no easing at
   all. A short fade-and-rise on the arriving panel is enough to read as a
   transition. It replays on every swap because unhiding a display:none
   element restarts its animations — and setView() in client.js is now
   idempotent, so it only replays on an actual change of view. */
@keyframes viewIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

.view,
.game-stage {
  animation: viewIn 200ms ease-out both;
}

/* Landing hero: no panel, pure night. Width-constrained so the settings gear in
   the header sits at the top-right OF THE HERO -- across the full 1040px app
   column it floated alone in the dark, anchored to nothing. */
#roomView {
  width: min(520px, 100%);
  margin-left: auto;
  margin-right: auto;
  background: transparent;
  border: 0;
  box-shadow: none;
  text-align: center;
  padding-top: 4vh;
}

/* id-qualified so it beats .view-header's space-between regardless of where
   these two rules sit relative to each other */
#roomView .room-header {
  justify-content: flex-end;
  margin-bottom: 0;
}

.hero-emojis {
  font-size: clamp(2.4rem, 1.6rem + 3.5vw, 3.6rem);
  line-height: 1;
  margin-bottom: 6px;
  user-select: none;
}

.hero-emojis span {
  display: inline-block;
  animation: bob 2.6s ease-in-out infinite;
}

.hero-emojis span:nth-child(2) {
  animation-delay: 0.3s;
  font-size: 1.25em;
}

.hero-emojis span:nth-child(3) {
  animation-delay: 0.6s;
}

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

#roomView h2 {
  font-size: clamp(2.6rem, 1.6rem + 4.5vw, 4rem);
  margin: 0 0 26px;
  background: linear-gradient(115deg, var(--accent), var(--accent-2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* A grid track, not `width: 100%`, is what makes these fill: grid items stretch
   by default, so the mobile `width: auto` override below cannot flatten the
   stack the way it would fight an explicit width (see LEARNINGS 2026-08-28 on
   the specificity trap this replaces). */
.menu {
  display: grid;
  grid-template-columns: 1fr;
  gap: 10px;
  width: min(320px, 100%);
  margin: 0 auto;
}

/* ---------- rows & inputs ---------- */

.row {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
}

input {
  min-width: 220px;
  border: 1px solid var(--edge-soft);
  border-radius: 999px;
  padding: 11px 16px;
  font-size: 1rem;
  font-family: var(--body);
  background: var(--chip);
  color: var(--cloud);
  transition: border-color 140ms ease, box-shadow 140ms ease;
}

input::placeholder {
  color: var(--dim);
}

input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px #ff5e5b33;
}

/* ---------- buttons: one shape, three ranks ----------
   Split deliberately into SHAPE and SKIN. Shape (size, radius, type, motion) is
   shared by every rank so the ranks read as one family; only skin varies. The
   `:not()` chain is (0,6,1), so anything a bare `.ghost` class needs to
   override has to live in the skin rule -- put `border` or `padding` in the
   shape rule and the ghost silently loses the fight (LEARNINGS 2026-08-28). */
button:not(.emoji):not(.mode-btn):not(.icon-btn):not(.back-btn):not(.switch):not(.plain-btn) {
  border-radius: 999px;
  padding: clamp(10px, 0.5vw + 8px, 14px) clamp(18px, 1.4vw + 12px, 30px);
  min-height: clamp(44px, 1.2vw + 38px, 54px);
  font-size: clamp(1rem, 0.22vw + 0.95rem, 1.12rem);
  font-family: var(--display);
  font-weight: 800;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: transform 120ms cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 120ms ease,
              filter 120ms ease, background 140ms ease, border-color 140ms ease, color 140ms ease;
}

/* Rank 1 — the one gradient on the screen. */
button:not(.emoji):not(.mode-btn):not(.icon-btn):not(.back-btn):not(.switch):not(.plain-btn):not(.ghost) {
  border: 0;
  background: linear-gradient(115deg, var(--accent), var(--accent-2));
  color: #2b1235;
  box-shadow: 0 8px 22px #ff5e5b3d;
}

button:not(.emoji):not(.mode-btn):not(.icon-btn):not(.back-btn):not(.switch):not(.plain-btn):hover:not(:disabled) {
  transform: translateY(-2px) scale(1.02);
}

button:not(.emoji):not(.mode-btn):not(.icon-btn):not(.back-btn):not(.switch):not(.plain-btn):not(.ghost):hover:not(:disabled) {
  filter: brightness(1.05);
}

button:not(.emoji):not(.mode-btn):not(.icon-btn):not(.back-btn):not(.switch):not(.plain-btn):active:not(:disabled) {
  transform: translateY(1px) scale(0.97);
}

button:not(.emoji):not(.mode-btn):not(.icon-btn):not(.back-btn):not(.switch):not(.plain-btn):not(.ghost):active:not(:disabled) {
  box-shadow: 0 4px 10px #ff5e5b2e;
}

button:not(.emoji):not(.icon-btn):not(.back-btn):not(.switch):not(.plain-btn):disabled {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}

button:not(.emoji):not(.icon-btn):not(.back-btn):not(.switch):not(.plain-btn):focus-visible {
  outline: 3px solid #ffb03a;
  outline-offset: 3px;
}

/* Rank 2 — the matched secondary chip.
   The gradient is a promise: "this is the thing that moves you forward." It was
   being spent on Start game, How to play AND the selected game mode at once, so
   nothing outranked anything. A raised chip reads as available without
   shouting; the old hollow outline read as disabled, which is exactly why every
   control had drifted back to the gradient. */
.ghost {
  border: 1px solid var(--edge-soft);
  background: var(--chip);
  color: var(--cloud);
  box-shadow: none;
}

.ghost:hover:not(:disabled) {
  background: var(--chip-hover);
  border-color: var(--edge-soft);
}

/* Rank 3 — help and undo. Present, reachable, never competing. */
.ghost.is-quiet {
  background: transparent;
  border-color: transparent;
  color: #beb6e8;
}

.ghost.is-quiet:hover:not(:disabled) {
  background: #ffffff12;
  border-color: transparent;
  color: var(--cloud);
}

/* A rule, not just a gap: it marks where configuring the game ends and
   committing to it begins. The secondary pair sits at the far end so the eye
   lands on the primary first and finds the rest only when it looks for them. */
.lobby-action,
.results-action {
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--edge);
  gap: 10px;
}

.action-secondary {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  flex-wrap: wrap;
}

/* Results is a centred panel, so its footer is centred too -- the far-end split
   that suits the left-aligned lobby reads as a misalignment here. */
.results-action {
  justify-content: center;
}

.results-action .action-secondary {
  margin-left: 0;
}

/* Compact icon buttons — excluded from the primary gradient below */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex: 0 0 auto;
  padding: 0;
  border: 2px solid var(--edge);
  border-radius: 999px;
  background: transparent;
  color: var(--cloud);
  font-size: 1.05rem;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: border-color 140ms ease;
}

.icon-btn:hover:not(:disabled) {
  border-color: var(--accent);
}

.icon-btn:focus-visible {
  outline: 3px solid #ffb03a;
  outline-offset: 2px;
}

.view-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
}

.back-btn {
  border: 0;
  border-radius: 999px;
  padding: 8px 16px;
  min-height: 40px;
  background: transparent;
  color: var(--dim);
  font-family: var(--body);
  font-weight: 700;
  font-size: 0.9rem;
  cursor: pointer;
  transition: color 140ms ease;
}

.back-btn:hover {
  color: var(--cloud);
}

.back-btn:focus-visible {
  outline: 3px solid #ffb03a;
  outline-offset: 2px;
}

/* ---------- settings modal ---------- */

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 2300;
  display: grid;
  place-items: center;
  padding: 18px;
  padding-top: max(18px, env(safe-area-inset-top));
  padding-bottom: max(18px, env(safe-area-inset-bottom));
  background: #0b0920b8;
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  animation: overlayIn 160ms ease-out both;
}

@keyframes overlayIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.modal-card {
  width: min(420px, 100%);
  max-height: 100%;
  overflow-y: auto;
  background: var(--night-2);
  border: 1px solid var(--edge);
  border-radius: 20px;
  padding: clamp(18px, 3vw, 26px);
  box-shadow: 0 24px 60px #00000073;
  animation: modalIn 200ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes modalIn {
  from { opacity: 0; transform: translateY(14px) scale(0.96); }
  to { opacity: 1; transform: none; }
}

.modal-title {
  font-size: clamp(1.4rem, 1rem + 1.4vw, 1.8rem);
  margin: 0 0 4px;
}

.link-btn {
  background: none;
  border: none;
  color: inherit;
  opacity: 0.75;
  text-decoration: underline;
  cursor: pointer;
  font-size: 0.9rem;
  padding: 4px;
}

.link-btn:hover { opacity: 1; }

.tutorial-text {
  margin: 10px 0 14px;
  line-height: 1.4;
}

/* ---------- tutorial: a second, board-only instance of the game view ---------- */

.tutorial-overlay {
  position: fixed;
  inset: 0;
  z-index: 1500;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right))
           max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
  background: var(--night);
  overscroll-behavior: contain;
}

/* Mirrors body.view-game's app-shell treatment: the stage must fill what is
   left after the coaching sheet, and nothing may scroll. */
.tutorial-overlay .game-stage {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

body.view-tutorial {
  overflow: hidden;
}

/* --night-2, NOT --card-face: the card face is near-white (#fffdf6) and the
   page text is --cloud, so a card-face sheet would be light text on light. */
.tutorial-sheet {
  flex: 0 0 auto;
  border-radius: 18px;
  padding: 14px 16px;
  background: var(--night-2);
  box-shadow: 0 -12px 40px #00000040;
}

.tutorial-sheet-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 10px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.emoji.is-found {
  filter: drop-shadow(0 0 12px var(--good));
}

.setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 14px 0;
  border-bottom: 1px solid var(--edge);
}

.setting-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.setting-name {
  font-weight: 700;
  font-size: 1rem;
}

/* The switches are `flex: 0 0 auto`; the name field instead takes the slack so
   it lines up with them on the right edge of the card. */
.setting-input {
  flex: 0 1 auto;
  min-width: 0;
  width: 56%;
  max-width: 220px;
  padding: 9px 16px;
  font-size: 0.95rem;
  text-align: right;
}

/* Right-hand affordance for rows that navigate rather than toggle (privacy
   policy, ad reporting, UMP privacy options). Sized to sit on the same right
   edge as the switches without pretending to be a primary button. */
.setting-link,
.setting-link-btn {
  flex: 0 0 auto;
  padding: 8px 16px;
  border: 1px solid var(--edge-soft);
  border-radius: 999px;
  background: var(--chip);
  color: inherit;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 160ms ease, border-color 160ms ease;
}

.setting-link:hover,
.setting-link-btn:hover {
  background: var(--chip-hover);
}

/* iOS-style switch. `.switch` is deliberately outside the primary-button
   selector's exclusion list below, so it needs every reset the other opt-outs
   get spelled out here. */
.switch {
  position: relative;
  flex: 0 0 auto;
  width: 56px;
  height: 32px;
  padding: 0;
  border: 2px solid var(--edge);
  border-radius: 999px;
  background: var(--night);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 160ms ease, border-color 160ms ease;
}

.switch[aria-checked="true"] {
  border-color: transparent;
  background: linear-gradient(115deg, var(--accent), var(--accent-2));
}

.switch-knob {
  position: absolute;
  top: 50%;
  left: 3px;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  background: var(--cloud);
  transform: translateY(-50%);
  box-shadow: 0 2px 6px #00000059;
  transition: transform 160ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.switch[aria-checked="true"] .switch-knob {
  transform: translateY(-50%) translateX(24px);
}

.switch:focus-visible {
  outline: 3px solid #ffb03a;
  outline-offset: 3px;
}

.modal-action {
  margin-top: 20px;
  justify-content: flex-end;
}

/* ---------- mode segmented control ---------- */

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

.mode-btn {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 3px;
  border: 1px solid var(--edge-soft);
  border-radius: 14px;
  padding: 11px 16px;
  min-height: 62px;
  text-align: left;
  background: var(--chip);
  cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease, transform 140ms ease, box-shadow 140ms ease;
}

.mode-name {
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.02rem;
  color: var(--cloud);
  line-height: 1.1;
}

/* The emoji count was buried in a title tooltip, which is the one place a phone
   can never read. It is the entire difference between the two modes, so it goes
   on the chip. */
.mode-meta {
  font-family: var(--body);
  font-weight: 600;
  font-size: 0.72rem;
  letter-spacing: 0.03em;
  color: var(--dim);
  line-height: 1.1;
}

.mode-btn:not(.is-active):hover:not(:disabled) {
  background: var(--chip-hover);
}

/* The selected mode turns into a card face -- the same cream the game's own
   cards are painted in. You are choosing a deck, so the control says so
   literally, and "chosen" stops looking like a third button waiting to be
   pressed. It is also the one high-contrast note on the screen, which is
   exactly where the eye should land after the primary action. */
.mode-btn.is-active {
  background: var(--card-face);
  border-color: var(--card-face);
  box-shadow: 0 10px 24px #00000047;
}

.mode-btn.is-active .mode-name {
  color: var(--ink);
}

.mode-btn.is-active .mode-meta {
  color: #6f67a6;
}

.mode-btn:disabled {
  cursor: default;
}

.mode-btn:focus-visible {
  outline: 3px solid #ffb03a;
  outline-offset: 2px;
}

/* ---------- player chips ---------- */

.list {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
}

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

.player-pill {
  position: relative;
  margin: 0;
  border-radius: 999px;
  background: var(--chip);
  border: 1px solid var(--edge-soft);
  padding: 9px 16px 9px 32px;
  font-weight: 700;
  font-size: 0.95rem;
  color: var(--cloud);
}

.player-pill::before {
  content: "";
  position: absolute;
  left: 12px;
  top: 50%;
  width: 11px;
  height: 11px;
  border-radius: 999px;
  transform: translateY(-50%);
  background: var(--player-color, #64748b);
  box-shadow: 0 0 8px var(--player-color, #64748b);
}

@keyframes scoreFlashGood {
  0% { background: var(--chip); box-shadow: none; }
  45% { background: #14532d; box-shadow: 0 0 14px #3ddc9766; }
  100% { background: var(--chip); box-shadow: none; }
}

@keyframes scoreFlashBad {
  0% { background: var(--chip); box-shadow: none; }
  45% { background: #5f1a2e; box-shadow: 0 0 14px #ff4d6d66; }
  100% { background: var(--chip); box-shadow: none; }
}

.player-pill.score-flash-good {
  animation: scoreFlashGood var(--flash-duration) ease-in-out;
}

.player-pill.score-flash-bad {
  animation: scoreFlashBad var(--flash-duration) ease-in-out;
}

/* ---------- status & toast ---------- */

.status {
  margin: 8px 0 0;
  font-weight: 600;
  color: var(--dim);
}

.status.ok {
  color: var(--good);
}

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

.toast {
  position: fixed;
  top: max(16px, env(safe-area-inset-top));
  right: 16px;
  z-index: 2400;
  max-width: min(360px, calc(100vw - 32px));
  padding: 12px 16px;
  border-radius: 14px;
  border: 1px solid var(--edge);
  background: var(--night-3);
  color: var(--cloud);
  font-weight: 700;
  box-shadow: 0 14px 30px #00000066;
  opacity: 0;
  transform: translateY(-10px);
  pointer-events: none;
  transition: opacity 180ms ease, transform 180ms ease;
}

.toast.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.toast.is-hidden {
  opacity: 0;
  transform: translateY(-10px);
}

.toast.ok {
  border-color: #3ddc9766;
}

.toast.bad {
  border-color: #ff4d6d66;
}

/* ---------- game view ---------- */

/* ---------- game view fills the viewport, never scrolls ---------- */

body.view-game {
  height: 100vh;        /* fallback: iOS 15.0–15.3 has no dvh */
  height: 100dvh;
  min-height: 0;        /* must beat the base `min-height: 100vh` above */
  overflow: hidden;
  align-items: stretch;
}

body.view-game .app {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

body.view-game #appViews {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.game-stage {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 0;
}

.game-stage .game-bar,
.game-stage .timer-track,
.game-stage .game-scores-heading {
  flex: 0 0 auto;
}

/* a full room would otherwise wrap onto several rows and squeeze the board */
.game-stage .list.player-list {
  flex: 0 0 auto;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
}

.game-stage .board {
  flex: 1 1 auto;
  min-height: 0;
  margin-top: 0;
}

.game-stage .card-col {
  min-height: 0;
  /* lets .emoji-card query this box's resolved height (cqh) below, so the
     card can be sized by whichever axis is tighter and stay square */
  container-type: size;
  /* the card no longer grows to fill leftover height (see flex: 0 0 auto
     below), so center it instead of pinning it to top */
  justify-content: center;
}

/* A content-less box with `aspect-ratio` can only derive its size from ONE
   definite axis (CSS has no built-in two-axis "fit largest square" solve for
   non-replaced elements) — so a plain `width: auto; height: 100%` here goes
   oval on any layout where the column is narrower than the available row
   height (e.g. wide-but-short desktop windows). `min(100%, 99cqh)` picks the
   tighter of the two axes explicitly; the card is now the column's only child
   (the "Your card"/"Match this" labels were removed), so there is nothing to
   reserve height for beyond a hairline of slack.
   Fallback width:100% here is a LAYOUT-BREAKING placeholder, not a safe
   degradation — pre-container-query browsers (Safari < 16 / iOS < 16) drop
   the invalid `cqh` declaration and are left with `width: 100%`, which
   overflows on narrow-but-tall layouts exactly like the bug this file exists
   to fix. The `@supports not (container-type: size)` block near the end of
   this file replaces it with a JS-computed `--card-max` custom property.

   The iOS app raised IPHONEOS_DEPLOYMENT_TARGET to 16.0 on 2026-08-02, so the
   app itself can no longer reach this branch. It stays for the WEBSITE: this
   stylesheet is served to every browser, and devices capped at iOS 15 (iPhone
   6s/7 and similar) still run Safari 15 on emojimatch.us. Deleting it would
   break the board for them. That also means the branch remains untested on a
   real pre-16 engine — see IDEAS.md. */
.game-stage .card-col > .emoji-card {
  flex: 0 0 auto;
  width: 100%;
  width: min(100%, 99cqh);
  height: auto;
  max-width: 100%;
  margin: 0 auto;
}

/* Equal 1fr side columns are what actually centres the score: with `auto`
   sides the middle would sit off-centre by however much the back button is
   wider than the timer pill. */
.game-bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;
}

.game-bar .back-btn {
  justify-self: start;
  min-width: 0;
  padding-left: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.game-bar .game-meta {
  text-align: center;
}

.game-bar .game-timer {
  justify-self: end;
}

.game-bar .status {
  margin: 0;
  font-weight: 700;
}

.game-meta-slot {
  position: relative;
  text-align: center;
}

.streak-badge {
  position: absolute;
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-left: 8px;
  white-space: nowrap;
  font-weight: 800;
  color: #ffb03a;
  transition: transform 0.15s ease-out;
}

.streak-badge.tier-2 {
  transform: translateY(-50%) scale(1.15);
  color: #ff8c3a;
}

.streak-badge.tier-3 {
  transform: translateY(-50%) scale(1.3);
  color: #ff5e5b;
}

.board.board-pulse {
  animation: boardPulse 0.25s ease-out;
}

.board.board-shake {
  animation: boardShake 0.3s ease-in-out;
}

@keyframes boardPulse {
  50% { transform: scale(1.012); }
}

@keyframes boardShake {
  20% { transform: translateX(-4px); }
  40% { transform: translateX(4px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(2px); }
}

.timer-pill {
  margin: 0;
  padding: 6px 16px;
  border-radius: 999px;
  background: var(--night-2);
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.15rem;
  color: var(--cloud);
  font-variant-numeric: tabular-nums;
}

.timer-pill.is-urgent {
  color: var(--bad);
  animation: urgentPulse 1s ease-in-out infinite;
}

@keyframes urgentPulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.07); }
}

.timer-track {
  height: 6px;
  border-radius: 999px;
  background: var(--night-2);
  margin: 10px 0 4px;
  overflow: hidden;
}

.timer-fill {
  height: 100%;
  width: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  transition: width 400ms linear;
}

.timer-fill.is-urgent {
  background: var(--bad);
  animation: urgentGlow 1s ease-in-out infinite;
}

@keyframes urgentGlow {
  0%, 100% { filter: brightness(1); }
  50% { filter: brightness(1.45); }
}

/* ---------- the cards ---------- */

.board {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(14px, 2.5vw, 26px);
}

.card-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.emoji-card {
  position: relative;
  width: 100%;
  margin: 0 auto;
  aspect-ratio: 1;
  border-radius: 26px;
  border: 0;
  background: var(--card-face);
  box-shadow:
    0 24px 60px #00000059,
    0 0 70px #ff5e5b17;
  overflow: hidden;
}

.card-fly {
  position: fixed;
  z-index: 2000;
  pointer-events: none;
  margin: 0;
}

.emoji {
  position: absolute;
  line-height: 1;
  user-select: none;
  border: 0;
  background: transparent;
  padding: 0;
  appearance: none;
  -webkit-appearance: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transform: translate(-50%, -50%) rotate(var(--rot));
}

.emoji.clickable {
  cursor: pointer;
  transition: transform 0.12s ease, filter 0.12s ease;
}

.emoji.clickable:focus {
  outline: none;
}

.emoji.clickable:active {
  background: transparent;
}

@media (hover: hover) and (pointer: fine) {
  .emoji.clickable:hover {
    transform: translate(-50%, -50%) rotate(var(--rot)) scale(1.14);
    filter: drop-shadow(0 2px 3px #00000035);
  }
}

@keyframes cardFlashGood {
  0% { background: var(--card-face); }
  50% {
    background: #d8ffef;
    box-shadow: 0 0 0 5px var(--good), 0 24px 60px #00000059, 0 0 90px #3ddc9744;
  }
  100% { background: var(--card-face); }
}

@keyframes cardFlashBad {
  0% { background: var(--card-face); }
  50% {
    background: #ffe3ea;
    box-shadow: 0 0 0 5px var(--bad), 0 24px 60px #00000059, 0 0 90px #ff4d6d44;
  }
  100% { background: var(--card-face); }
}

.emoji-card.card-flash-good {
  animation: cardFlashGood var(--flash-duration) ease-in-out;
}

.emoji-card.card-flash-bad {
  animation: cardFlashBad var(--flash-duration) ease-in-out;
}

/* ---------- live game box (lobby) ---------- */

.live-game {
  margin-top: 14px;
  background: #1b1740;
  border: 1px solid var(--edge);
  border-radius: 14px;
  padding: 14px 16px;
}

.live-game h3 {
  margin-top: 0;
}

.live-game-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* ---------- results ---------- */

#resultsView {
  position: relative;
  overflow: hidden;
  text-align: center;
}

#resultsView h2 {
  font-size: clamp(2.2rem, 1.4rem + 3.5vw, 3.2rem);
  background: linear-gradient(115deg, var(--accent), var(--accent-2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.personal-best {
  margin: 4px 0 0;
  font-weight: 600;
  opacity: 0.85;
}

.personal-best.is-new {
  opacity: 1;
  font-size: 1.05rem;
  color: var(--good);
}

.results-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 560px;
  margin-left: auto;
  margin-right: auto;
}

.app-cta {
  margin-top: 14px;
  padding: 12px 14px;
  background: linear-gradient(140deg, var(--night-3), var(--night-2));
  border: 1px solid var(--edge);
  border-radius: 12px;
  display: flex;
  align-items: center;
  gap: 12px;
  text-align: left;
  flex-wrap: wrap;
}

.app-cta-icon {
  border-radius: 12px;
  flex: none;
}

.app-cta-copy {
  flex: 1 1 150px;
  min-width: 0;
}

.app-cta-title {
  margin: 0;
  font-family: var(--display);
  font-size: 1.02rem;
  color: var(--cloud);
}

.app-cta-text {
  margin: 2px 0 0;
  font-size: 0.84rem;
  line-height: 1.35;
  color: var(--dim);
}

.app-cta-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
}

.app-cta-get {
  background: var(--accent-2);
  color: #2a2008;
  font-weight: 700;
  font-size: 0.88rem;
  padding: 9px 15px;
  border-radius: 999px;
  text-decoration: none;
  white-space: nowrap;
}

.app-cta-dismiss {
  background: none;
  border: 0;
  color: var(--dim);
  font-size: 0.82rem;
  padding: 6px 2px;
  cursor: pointer;
  text-decoration: underline;
}

.app-cta-dismiss:hover {
  color: var(--cloud);
}

.results-stats {
  margin-top: 14px;
  padding: 12px 14px;
  background: var(--night);
  border-radius: 12px;
  text-align: left;
}

.results-stats-heading {
  margin: 0 0 8px;
  font-size: 0.95rem;
}

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

.results-stat {
  display: flex;
  justify-content: space-between;
  gap: 12px;
}

.stat-label {
  opacity: 0.8;
}

.stat-value {
  font-weight: 700;
}

.result-row {
  position: relative;
  display: grid;
  grid-template-columns: 44px 1fr auto;
  align-items: center;
  gap: 10px;
  background: var(--night-3);
  border-radius: 14px;
  padding: 12px 18px 12px 38px;
  font-weight: 700;
  text-align: left;
  animation: rowIn 420ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

.result-row:nth-child(1) { animation-delay: 0.05s; }
.result-row:nth-child(2) { animation-delay: 0.15s; }
.result-row:nth-child(3) { animation-delay: 0.25s; }
.result-row:nth-child(4) { animation-delay: 0.35s; }
.result-row:nth-child(n+5) { animation-delay: 0.45s; }

@keyframes rowIn {
  from { opacity: 0; transform: translateY(16px) scale(0.96); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

.result-row:first-child:not(.result-empty) {
  background: linear-gradient(115deg, #ff5e5b2b, #ffb03a2b), var(--night-3);
  box-shadow: 0 0 0 2px #ffb03a88, 0 0 26px #ffb03a33;
}

.result-row:first-child:not(.result-empty) .result-place::after {
  content: " 👑";
}

.result-row::before {
  content: "";
  position: absolute;
  left: 14px;
  top: 50%;
  width: 11px;
  height: 11px;
  border-radius: 999px;
  transform: translateY(-50%);
  background: var(--player-color, #64748b);
  box-shadow: 0 0 8px var(--player-color, #64748b);
}

.result-place {
  font-family: var(--display);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--dim);
}

.result-score {
  font-family: var(--display);
  font-weight: 800;
  font-size: 1.2rem;
  font-variant-numeric: tabular-nums;
}

/* Solo runs are a single player, so there is nothing to rank and no one to
   name — the row collapses to just the score. The colour dot and the
   first-place crown come from the shared rules above and are cancelled here. */
.result-solo {
  grid-template-columns: 1fr;
  justify-items: center;
  gap: 2px;
  padding: 20px 18px;
}

.result-solo::before {
  display: none;
}

.result-solo .result-score {
  font-size: 2.6rem;
  line-height: 1.05;
}

.result-solo-label {
  font-size: 0.74rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--dim);
}

.result-empty {
  display: block;
  padding-left: 18px;
}

.result-empty::before {
  display: none;
}

.results-action {
  margin-top: 26px;
  justify-content: center;
}

/* confetti — pure CSS, replays whenever the results view is entered.
   Fixed to the viewport (the element is a direct child of <body>, see
   index.html) so the fall distance and the clip box are the same box. Keeping
   it inside #resultsView meant clipping at that panel's height while the
   keyframe travelled 560px, so pieces were cut off mid-fall at full opacity. */
.confetti {
  position: fixed;
  inset: 0;
  z-index: 1500;
  pointer-events: none;
  overflow: hidden;
  display: none;
}

/* display:none -> block restarts the child animations, which is what makes the
   burst replay on every arrival at the results view. */
body.view-results .confetti {
  display: block;
}

.confetti i {
  position: absolute;
  top: -16px;
  left: calc(var(--x) * 1%);
  width: 9px;
  height: 14px;
  border-radius: 2px;
  background: var(--c);
  opacity: 0;
  animation: confettiFall calc(var(--t, 2.6) * 1s) ease-in calc(var(--d) * 1s) 1 both;
}

/* Travels past the bottom of the viewport, so pieces leave the screen instead
   of blinking out at the end of the keyframe. The fade over the last stretch
   only has to cover the slowest piece; the rest are already off-screen. */
@keyframes confettiFall {
  0% { opacity: 1; transform: translateY(0) rotateZ(0) rotateX(0); }
  85% { opacity: 1; }
  100% { opacity: 0; transform: translateY(106vh) rotateZ(540deg) rotateX(320deg); }
}

/* ---------- responsive ---------- */

@media (max-width: 860px) {
  :root {
    --flash-duration: 420ms;
  }

  body {
    padding: 12px;
    padding-top: max(12px, env(safe-area-inset-top));
    padding-bottom: max(12px, env(safe-area-inset-bottom));
    padding-left: max(12px, env(safe-area-inset-left));
    padding-right: max(12px, env(safe-area-inset-right));
  }

  .board {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr 1fr;
  }

  button:not(.emoji):not(.mode-btn):not(.icon-btn):not(.back-btn):not(.switch) {
    min-width: 0;
    width: auto;
  }

  /* The whole footer stacks: primary on its own line, the quiet pair sharing
     the next one. Left-aligned, because `margin-left: auto` on a wrapped flex
     line strands the pair against the right edge under a left-aligned button. */
  .lobby-action,
  .results-action {
    display: grid;
    grid-template-columns: 1fr;
  }

  .action-secondary {
    margin-left: 0;
  }

  .action-secondary > button {
    flex: 1 1 0;
    min-width: 0;
  }

  .game-stage .game-scores-heading,
  .game-stage .list.player-list {
    display: none;
  }

  /* three items on one line at 320-430px: keep the back label from stealing
     room the centred score and the timer pill need */
  .game-bar {
    gap: 6px;
  }

  .game-bar .back-btn {
    padding-right: 4px;
    font-size: 0.82rem;
  }

  .game-bar .game-meta {
    font-size: 0.92rem;
  }
}

/* Landscape phones: not enough height to stack, so go back to two columns.
   grid-template-rows MUST be reset — the inherited `1fr 1fr` would leave an
   empty second row eating half the board. */
@media (max-width: 860px) and (max-height: 560px) {
  .board {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr;
  }
}

/* ---------- fallback for browsers without container-query support ----------
   Safari < 16 / iOS < 16 (this app's deployment target is iOS 15, so this is
   not a hypothetical) has no `container-type`/`cqh`, which leaves the rules
   above at their unclamped `width: 100%` fallback — square only by luck,
   overflowing whenever the card-col is narrower than the available row
   height (see the rule above). `--card-max` is set from JS in renderCard()
   (client.js) to the actual measured height available to the card (the
   card-col's content-box height) — the exact quantity `cqh` approximates
   above, computed once instead of guessed at a fixed percentage. Equal
   specificity to the rule above; wins because it's later in source order,
   in both portrait and landscape. */
@supports not (container-type: size) {
  .game-stage .card-col > .emoji-card {
    width: min(100%, var(--card-max, 100%));
  }
}

.score-popup {
  position: fixed;
  transform: translate(-50%, -50%);
  z-index: 2100;
  pointer-events: none;
  font-weight: 800;
  font-size: 1.3rem;
  animation: popupFloat 0.7s ease-out forwards;
}

.score-popup.is-good { color: var(--good, #3ddc97); }
.score-popup.is-bad { color: var(--bad, #ff4d6d); }

@keyframes popupFloat {
  to {
    transform: translate(-50%, calc(-50% - 40px));
    opacity: 0;
  }
}

@keyframes popupFade {
  to { opacity: 0; }
}

/* ---------- reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .hero-emojis span,
  .timer-pill.is-urgent,
  .timer-fill.is-urgent,
  .view,
  .game-stage,
  .modal-overlay,
  .modal-card {
    animation: none;
  }

  .switch,
  .switch-knob {
    transition: none;
  }

  .confetti i {
    display: none;
  }

  .result-row {
    animation: none;
  }

  .board.board-pulse,
  .board.board-shake {
    animation: none;
  }

  button:not(.emoji):not(.mode-btn):not(.icon-btn):not(.back-btn):not(.switch):hover:not(:disabled) {
    transform: none;
  }

  .score-popup {
    animation: popupFade 0.7s ease-out forwards;
  }
}
