/* The v2 shell: every screen outside the table. DOM rather than Phaser, so
 * text is crisp at any density, inputs get native keyboards, and screen
 * readers work. SPEC §5. */

#fp-shell {
  position: fixed; inset: 0; z-index: 40;
  display: none;
  font-family: var(--fp-font);
  color: var(--fp-ink);
  background: var(--fp-bg);
  overflow: hidden;
}
#fp-shell.open { display: block; }

/* ---------- shared inline-SVG icon (ui/icons.js) ----------
 * `stroke: currentColor` on the wrapper lets every icon recolor with its
 * surrounding text/button color rather than needing a class per color. */
.fp-icon {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto; line-height: 0; color: currentColor;
}
.fp-icon svg { display: block; }

.fp-screen {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  padding: calc(var(--fp-safe-t) + var(--fp-s3)) calc(var(--fp-safe-r) + var(--fp-s4))
           calc(var(--fp-safe-b) + var(--fp-s3)) calc(var(--fp-safe-l) + var(--fp-s4));
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  animation: fp-in var(--fp-enter);
}
@keyframes fp-in { from { opacity: 0; transform: translateY(8px); } }

/* ---------- header ---------- */
.fp-topbar {
  display: flex; align-items: center; gap: var(--fp-s3);
  margin-bottom: var(--fp-s4); flex: 0 0 auto;
}
.fp-id { display: flex; align-items: center; gap: var(--fp-s2); min-width: 0; }
.fp-avatar {
  width: 40px; height: 40px; border-radius: 50%; flex: 0 0 auto;
  display: grid; place-items: center;
  font-weight: 800; font-size: var(--fp-t-md); color: #fff;
  background: linear-gradient(135deg, var(--fp-accent), #25588f);
  box-shadow: inset 0 -3px 6px rgba(0,0,0,.3), var(--fp-shadow-1);
}
.fp-name {
  font-size: var(--fp-t-sm); font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fp-sub { font-size: var(--fp-t-xs); color: var(--fp-ink-faint); }
.fp-spacer { flex: 1 1 auto; }

.fp-pill {
  display: inline-flex; align-items: center; gap: var(--fp-s1);
  padding: 5px 10px; border-radius: var(--fp-r-pill);
  background: rgba(0,0,0,.3); border: 1px solid var(--fp-hairline);
  font-size: var(--fp-t-xs); font-weight: 700;
}
.fp-pill.gold { color: var(--fp-gold); }

.fp-iconbtn {
  width: 44px; height: 44px; flex: 0 0 auto;
  display: grid; place-items: center;
  border-radius: var(--fp-r-pill); border: 1px solid var(--fp-hairline);
  background: rgba(0,0,0,.25); color: var(--fp-ink);
  font-size: var(--fp-t-md); cursor: pointer;
  transition: background var(--fp-tap);
}
.fp-iconbtn:active { background: rgba(255,255,255,.10); }

/* ---------- free-online meter ---------- */
.fp-meter {
  flex: 0 0 auto; margin-bottom: var(--fp-s4);
  background: var(--fp-bg-elev); border: 1px solid var(--fp-hairline);
  border-radius: var(--fp-r-card); padding: var(--fp-s3);
  cursor: pointer; text-align: left; width: 100%;
  color: inherit; font: inherit;
}
.fp-meter-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--fp-s2); margin-bottom: var(--fp-s2);
}
.fp-meter-label { font-size: var(--fp-t-xs); color: var(--fp-ink-dim); }
.fp-meter-value { font-size: var(--fp-t-sm); font-weight: 800; }
.fp-meter-track {
  height: 6px; border-radius: var(--fp-r-pill);
  background: rgba(0,0,0,.4); overflow: hidden;
}
.fp-meter-fill {
  height: 100%; border-radius: var(--fp-r-pill);
  background: linear-gradient(90deg, var(--fp-good), var(--fp-accent));
  transition: width var(--fp-enter);
}
.fp-meter.empty .fp-meter-fill { background: var(--fp-bad); }
.fp-meter.premium .fp-meter-fill { background: linear-gradient(90deg, var(--fp-gold), var(--fp-good)); }

/* ---------- action cards ---------- */
.fp-stack { display: flex; flex-direction: column; gap: var(--fp-s3); flex: 1 1 auto; }

.fp-card {
  display: flex; align-items: center; gap: var(--fp-s3);
  min-height: 64px; padding: var(--fp-s4);
  border-radius: var(--fp-r-card);
  background: var(--fp-bg-elev); border: 1px solid var(--fp-hairline);
  color: var(--fp-ink); font: inherit; text-align: left;
  cursor: pointer; width: 100%;
  transition: transform var(--fp-tap), background var(--fp-tap);
}
.fp-card:active { transform: scale(.985); background: var(--fp-bg-elev-2); }
.fp-card[disabled] { opacity: .5; cursor: not-allowed; }
.fp-card[disabled]:active { transform: none; }

.fp-card.primary {
  background: linear-gradient(135deg, var(--fp-accent), #2b7fd4);
  border-color: rgba(255,255,255,.18);
  color: #fff; min-height: 76px;
  box-shadow: var(--fp-shadow-2);
}
.fp-card-glyph { font-size: var(--fp-t-lg); width: 32px; text-align: center; flex: 0 0 auto; }
.fp-card-body { flex: 1 1 auto; min-width: 0; }
.fp-card-title { font-size: var(--fp-t-md); font-weight: 800; }
.fp-card.primary .fp-card-title { font-size: var(--fp-t-lg); }
.fp-card-desc { font-size: var(--fp-t-xs); color: var(--fp-ink-dim); margin-top: 2px; }
.fp-card.primary .fp-card-desc { color: rgba(255,255,255,.8); }

.fp-lock {
  font-size: var(--fp-t-xs); font-weight: 800;
  padding: 4px 9px; border-radius: var(--fp-r-pill);
  background: rgba(255,215,106,.14); color: var(--fp-gold);
  border: 1px solid rgba(255,215,106,.3);
  flex: 0 0 auto;
}

/* Pending challenges/friend requests on the Friends rail item. */
.fp-rail-badge {
  font-size: var(--fp-t-xs); font-weight: 800; line-height: 1;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: var(--fp-r-pill);
  background: #ff4d4f; color: #fff;
  display: flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
}

/* Split card: one row, two independently tappable halves. */
.fp-split { display: flex; gap: var(--fp-s2); }
.fp-split .fp-card { flex: 1 1 0; min-width: 0; }
.fp-split .fp-card-title { font-size: var(--fp-t-sm); }

/* ---------- banner ad slot ---------- */
.fp-adslot {
  flex: 0 0 auto; margin-top: var(--fp-s3);
  min-height: 50px; display: grid; place-items: center;
  border-radius: var(--fp-r-input);
  background: rgba(0,0,0,.22); border: 1px dashed var(--fp-hairline);
  font-size: var(--fp-t-xs); color: var(--fp-ink-faint);
}
.fp-adslot.hidden { display: none; }

/* ---------- offline strip ---------- */
.fp-offline {
  flex: 0 0 auto; margin-bottom: var(--fp-s3);
  padding: var(--fp-s2) var(--fp-s3); border-radius: var(--fp-r-input);
  background: rgba(255,107,92,.12); border: 1px solid rgba(255,107,92,.3);
  color: var(--fp-bad); font-size: var(--fp-t-xs); font-weight: 700;
}
.fp-offline.hidden { display: none; }

/* ---------- form controls (create-room options) ---------- */
.fp-field { margin-bottom: var(--fp-s1); }
.fp-field-label {
  font-size: var(--fp-t-xs); color: var(--fp-ink-dim);
  margin-bottom: var(--fp-s2); font-weight: 700;
}
.fp-seg {
  display: flex; gap: 2px; padding: 3px;
  background: rgba(0,0,0,.3); border: 1px solid var(--fp-hairline);
  border-radius: var(--fp-r-pill);
}
.fp-seg-btn {
  flex: 1 1 0; min-height: 38px; border: 0; cursor: pointer;
  border-radius: var(--fp-r-pill); background: transparent;
  color: var(--fp-ink-dim); font: inherit; font-size: var(--fp-t-sm); font-weight: 700;
  transition: background var(--fp-tap), color var(--fp-tap);
}
.fp-seg-btn.on { background: var(--fp-accent); color: #fff; }

.fp-switch {
  width: 46px; height: 28px; flex: 0 0 auto;
  border-radius: var(--fp-r-pill); background: rgba(0,0,0,.45);
  border: 1px solid var(--fp-hairline);
  position: relative; transition: background var(--fp-tap);
}
.fp-switch.on { background: var(--fp-accent); }
.fp-switch-knob {
  position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px; border-radius: 50%;
  background: #fff; box-shadow: var(--fp-shadow-1);
  transition: transform var(--fp-tap);
}
.fp-switch.on .fp-switch-knob { transform: translateX(18px); }

.fp-card.busy { opacity: .7; pointer-events: none; }

.fp-formerr {
  font-size: var(--fp-t-sm); font-weight: 700; color: var(--fp-bad);
  min-height: 20px; text-align: center;
}

/* ---------- room code entry ---------- */
.fp-codebox { display: flex; justify-content: center; padding: var(--fp-s4) 0; }
.fp-code-input {
  width: 100%; max-width: 320px; text-align: center;
  font-family: ui-monospace, 'Cascadia Mono', Consolas, monospace;
  font-size: 34px; font-weight: 800; letter-spacing: .16em; text-indent: .16em;
  padding: var(--fp-s3); border-radius: var(--fp-r-input);
  background: rgba(0,0,0,.35); border: 2px solid var(--fp-hairline);
  color: var(--fp-ink); text-transform: uppercase;
  outline: none; transition: border-color var(--fp-tap);
}
.fp-code-input:focus { border-color: var(--fp-accent); }

/* ---------- share strip ---------- */
.fp-share {
  display: flex; flex-direction: column; align-items: center; gap: var(--fp-s3);
  padding: var(--fp-s4); border-radius: var(--fp-r-card);
  background: var(--fp-bg-elev); border: 1px solid var(--fp-hairline);
}
.fp-share-label {
  font-size: var(--fp-t-xs); color: var(--fp-ink-dim); font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em;
}
/* Large and letter-spaced so it stays readable across a table - the whole
 * point of a share code is being read off someone else's screen. */
.fp-code {
  font-family: ui-monospace, 'Cascadia Mono', Consolas, monospace;
  font-size: 38px; font-weight: 800; letter-spacing: .18em; text-indent: .18em;
  color: var(--fp-good);
}
.fp-qr {
  padding: var(--fp-s2); background: #fff; border-radius: var(--fp-r-input);
  line-height: 0; box-shadow: var(--fp-shadow-1);
}
.fp-share-btns { display: flex; gap: var(--fp-s2); flex-wrap: wrap; justify-content: center; }
.fp-minibtn {
  min-height: 40px; padding: 0 var(--fp-s4); cursor: pointer;
  border-radius: var(--fp-r-pill); border: 1px solid var(--fp-hairline);
  background: rgba(0,0,0,.28); color: var(--fp-ink);
  font: inherit; font-size: var(--fp-t-sm); font-weight: 700;
  transition: background var(--fp-tap);
}
.fp-minibtn:active { background: rgba(255,255,255,.10); }

/* ---------- lobby players ---------- */
.fp-players { display: flex; align-items: center; gap: var(--fp-s2); }
.fp-player {
  flex: 1 1 0; min-width: 0;
  display: flex; align-items: center; gap: var(--fp-s2);
  padding: var(--fp-s3); border-radius: var(--fp-r-card);
  background: var(--fp-bg-elev); border: 1.5px solid var(--fp-hairline);
  transition: border-color var(--fp-enter), box-shadow var(--fp-enter);
}
.fp-player.ready {
  border-color: rgba(140,255,170,.7);
  box-shadow: 0 0 14px rgba(140,255,170,.18);
}
.fp-tick { color: var(--fp-good); font-weight: 800; font-size: var(--fp-t-md); }
.fp-vs {
  flex: 0 0 auto; font-size: var(--fp-t-xs); font-weight: 800;
  color: var(--fp-ink-faint); letter-spacing: .1em;
}
.fp-rules {
  display: flex; gap: var(--fp-s2); flex-wrap: wrap; justify-content: center;
}

/* ---------- store / paywall ---------- */
.fp-reason {
  padding: var(--fp-s3); margin-bottom: var(--fp-s3);
  border-radius: var(--fp-r-input);
  background: rgba(74,168,255,.12); border: 1px solid rgba(74,168,255,.3);
  color: var(--fp-ink); font-size: var(--fp-t-sm); font-weight: 700;
}
.fp-buycard {
  padding: var(--fp-s4); border-radius: var(--fp-r-card);
  background: var(--fp-bg-elev); border: 1px solid var(--fp-hairline);
  display: flex; flex-direction: column; gap: var(--fp-s3);
}
.fp-buycard.premium {
  border-color: rgba(255,215,106,.35);
  background: linear-gradient(160deg, rgba(255,215,106,.07), var(--fp-bg-elev) 60%);
}
.fp-buycard-head { display: flex; align-items: flex-start; gap: var(--fp-s3); }
.fp-buycard-head > div:first-child { flex: 1 1 auto; min-width: 0; }
.fp-buycard-title { font-size: var(--fp-t-md); font-weight: 800; }
.fp-buycard-price { font-size: var(--fp-t-sm); color: var(--fp-gold); font-weight: 700; margin-top: 2px; }

.fp-benefits { list-style: none; display: flex; flex-direction: column; gap: var(--fp-s2); }
.fp-benefit { display: flex; gap: var(--fp-s2); font-size: var(--fp-t-sm); align-items: flex-start; }
.fp-benefit-tick { color: var(--fp-good); font-weight: 800; flex: 0 0 auto; }

/* Disclosure text must be legible, not hidden in 9px grey - reviewers check,
 * and a player who can't read the renewal terms is a chargeback waiting. */
.fp-fineprint {
  font-size: var(--fp-t-xs); line-height: 1.5; color: var(--fp-ink-dim);
}

.fp-storelinks {
  display: flex; flex-wrap: wrap; gap: var(--fp-s4);
  justify-content: center; padding: var(--fp-s4) 0;
}
.fp-linkbtn {
  background: none; border: 0; cursor: pointer; padding: var(--fp-s2);
  color: var(--fp-ink-dim); font: inherit; font-size: var(--fp-t-xs);
  text-decoration: underline; min-height: 44px;
}

/* ---------- spectator count ----------
 * Sits above the chat button, clear of the cue path and the power meter. */
#fp-spectators {
  position: fixed; z-index: 31;
  right: calc(var(--fp-safe-r) + 12px);
  bottom: calc(var(--fp-safe-b) + 140px);
  padding: 5px 11px; border-radius: var(--fp-r-pill);
  background: rgba(0,0,0,.55); border: 1px solid var(--fp-hairline);
  color: var(--fp-ink); font-family: var(--fp-font);
  font-size: var(--fp-t-xs); font-weight: 700;
  pointer-events: none;
  animation: fp-in var(--fp-enter);
  display: inline-flex; align-items: center; gap: 4px;
}
#fp-spectators.hidden { display: none; }

/* ---------- reconnect banner ----------
 * Above the table and the chat (z 35) but below the shell, because a menu
 * opening should still cover it. */
#fp-reconnect {
  position: fixed; left: 0; right: 0;
  top: calc(var(--fp-safe-t) + 56px);
  z-index: 35; display: flex; justify-content: center;
  pointer-events: none; padding: 0 var(--fp-s4);
}
#fp-reconnect.hidden { display: none; }
.fp-recon-card {
  pointer-events: auto; width: min(420px, 100%);
  padding: var(--fp-s3) var(--fp-s4);
  border-radius: var(--fp-r-card);
  background: rgba(10, 26, 18, .94);
  border: 1px solid rgba(255,200,97,.4);
  box-shadow: var(--fp-shadow-2);
  animation: fp-in var(--fp-enter);
}
.fp-recon-title { font-size: var(--fp-t-sm); font-weight: 800; color: var(--fp-warn); }
.fp-recon-sub { font-size: var(--fp-t-xs); color: var(--fp-ink-dim); margin-top: 2px; }
.fp-recon-track {
  height: 4px; border-radius: var(--fp-r-pill); overflow: hidden;
  background: rgba(0,0,0,.5); margin: var(--fp-s2) 0;
}
.fp-recon-fill {
  height: 100%; width: 100%; background: var(--fp-warn);
  transition: width .25s linear;
}
.fp-recon-actions { display: flex; gap: var(--fp-s2); justify-content: flex-end; }
.fp-minibtn.primary { background: var(--fp-accent); border-color: transparent; color: #fff; }

/* ---------- in-match chat ----------
 * Sits above the table (z 30) but below the shell (z 40), so opening a menu
 * always covers it rather than the two fighting. */
#fp-chat { position: fixed; inset: 0; z-index: var(--fp-z-chat); pointer-events: none; }
#fp-chat.hidden { display: none; }

.fp-chat-btn {
  position: absolute; right: calc(var(--fp-safe-r) + 12px);
  bottom: calc(var(--fp-safe-b) + 84px);
  width: 48px; height: 48px; border-radius: var(--fp-r-pill);
  border: 1px solid var(--fp-hairline); background: rgba(0,0,0,.55);
  color: var(--fp-ink); font-size: 22px; cursor: pointer;
  pointer-events: auto; box-shadow: var(--fp-shadow-1);
}
/* ---- leave-match confirmation ---- */
.fp-leave-backdrop {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  padding: var(--fp-s4); background: rgba(0,0,0,.66);
}
.fp-leave-dialog {
  width: min(420px, 100%); display: flex; flex-direction: column; gap: var(--fp-s2);
  padding: var(--fp-s4); border-radius: var(--fp-r-card);
  border: 1px solid var(--fp-hairline); background: var(--fp-surface, #12241a);
  box-shadow: var(--fp-shadow-2); color: var(--fp-ink);
  max-height: 100%; overflow-y: auto;
}
.fp-leave-title { font-size: 18px; font-weight: 700; }
.fp-leave-body { font-size: 13px; line-height: 1.45; color: var(--fp-ink-dim); }
/* Room between the buttons.
 *
 * The dialog's own gap is the text spacing, which put "keep my place" and
 * "forfeit the match" close enough to mis-tap - and one of those two is not
 * recoverable. A destructive choice needs a margin around it, plus a little
 * extra separation before it. */
.fp-leave-btn {
  margin-top: 6px;
  padding: 13px 16px; cursor: pointer; font: inherit; font-weight: 600;
  border-radius: var(--fp-r-pill); border: 1px solid var(--fp-hairline);
  background: rgba(255,255,255,.07); color: var(--fp-ink);
}
.fp-leave-btn.primary { background: var(--fp-accent, #2f81f7); border-color: transparent; color: #fff; }
.fp-leave-btn.danger { margin-top: 12px; background: transparent; color: var(--fp-bad, #e5534b); border-color: var(--fp-bad, #e5534b); }
.fp-leave-btn:active { transform: scale(.99); }

/* ---- chat panel: chat / game-log tabs ---- */
.fp-chat-tabs {
  display: flex; gap: 4px; padding: 0 var(--fp-s3) var(--fp-s2);
  flex: 0 0 auto;
}
.fp-chat-tab {
  flex: 1 1 0; padding: 6px 10px; cursor: pointer;
  border: 1px solid transparent; border-radius: var(--fp-r-pill);
  background: transparent; color: var(--fp-ink-dim);
  font: inherit; font-size: 13px; font-weight: 600;
}
.fp-chat-tab.active {
  background: rgba(255,255,255,.10); color: var(--fp-ink);
  border-color: var(--fp-hairline);
}
.fp-chat-events { display: block; }
.fp-chat-event {
  display: flex; gap: 8px; align-items: baseline;
  padding: 3px 0; font-size: 12px; line-height: 1.35;
}
.fp-chat-event-time {
  flex: 0 0 auto; color: var(--fp-ink-dim);
  font-variant-numeric: tabular-nums; font-size: 11px;
}
.fp-chat-event-text { color: var(--fp-ink); min-width: 0; word-break: break-word; }
.fp-chat-composer { display: flex; flex-direction: column; flex: 0 0 auto; }
.fp-chat-composer.hidden, .fp-chat-events.hidden, .fp-chat-log.hidden { display: none; }

/* The sheet has its own close button, so while it is open the floating button
 * is a second control for the same thing sitting on top of the panel it would
 * close. Faded out rather than removed, so it does not reflow anything. */
#fp-chat.open .fp-chat-btn {
  opacity: 0; pointer-events: none;
  transition: opacity var(--fp-tap);
}
.fp-chat-badge {
  position: absolute; top: -2px; right: -2px;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: var(--fp-r-pill); background: var(--fp-bad);
  color: #fff; font-size: 11px; font-weight: 800; line-height: 18px;
}
.fp-chat-badge.hidden { display: none; }

.fp-chat-sheet {
  position: absolute; right: 0; bottom: 0;
  width: min(360px, 100%); max-height: 70%;
  display: flex; flex-direction: column;
  background: var(--fp-bg-elev); border-left: 1px solid var(--fp-hairline);
  border-top: 1px solid var(--fp-hairline);
  border-radius: var(--fp-r-card) 0 0 0;
  padding: var(--fp-s3) var(--fp-s3) calc(var(--fp-safe-b) + var(--fp-s3));
  transform: translateY(105%); transition: transform var(--fp-enter);
  pointer-events: auto;
}
#fp-chat.open .fp-chat-sheet { transform: translateY(0); }

.fp-chat-head {
  display: flex; align-items: center; justify-content: space-between;
  font-size: var(--fp-t-sm); font-weight: 800; margin-bottom: var(--fp-s2);
}
.fp-chat-log {
  flex: 1 1 auto; overflow-y: auto; min-height: 120px;
  display: flex; flex-direction: column; gap: var(--fp-s2);
  padding-right: var(--fp-s1);
}
.fp-chat-msg {
  max-width: 85%; padding: var(--fp-s2) var(--fp-s3);
  border-radius: var(--fp-r-card); background: var(--fp-bg-elev-2);
  align-self: flex-start;
}
.fp-chat-msg.mine { align-self: flex-end; background: rgba(74,168,255,.22); }
.fp-chat-meta {
  display: flex; align-items: baseline; gap: var(--fp-s2);
  font-size: var(--fp-t-xs); color: var(--fp-ink-faint);
}
.fp-chat-who { font-weight: 700; }
.fp-chat-report {
  background: none; border: 0; padding: 2px 4px; cursor: pointer;
  color: var(--fp-ink-faint); font: inherit; font-size: 11px;
  text-decoration: underline;
}
.fp-chat-text { font-size: var(--fp-t-sm); margin-top: 2px; word-break: break-word; }
.fp-chat-note {
  font-size: var(--fp-t-xs); color: var(--fp-ink-faint);
  text-align: center; padding: var(--fp-s1) 0;
}

.fp-chat-quick {
  display: flex; gap: var(--fp-s2); overflow-x: auto; padding: var(--fp-s2) 0;
  scrollbar-width: none;
}
.fp-chat-quick::-webkit-scrollbar { display: none; }
.fp-chat-phrase {
  flex: 0 0 auto; min-height: 36px; padding: 0 var(--fp-s3); cursor: pointer;
  border-radius: var(--fp-r-pill); border: 1px solid var(--fp-hairline);
  background: rgba(0,0,0,.28); color: var(--fp-ink);
  font: inherit; font-size: var(--fp-t-xs); font-weight: 700; white-space: nowrap;
}
.fp-chat-inputrow { display: flex; gap: var(--fp-s2); }
.fp-chat-input {
  flex: 1 1 auto; min-width: 0; min-height: 40px; padding: 0 var(--fp-s3);
  border-radius: var(--fp-r-input); border: 1px solid var(--fp-hairline);
  background: rgba(0,0,0,.35); color: var(--fp-ink); font: inherit;
  font-size: var(--fp-t-sm); outline: none;
}
.fp-chat-input:disabled { opacity: .6; }

/* Spectators read the players' chat but cannot write into it. */
#fp-chat.readonly .fp-chat-quick,
#fp-chat.readonly .fp-chat-inputrow { display: none; }

.fp-chat-bubble {
  position: absolute; right: calc(var(--fp-safe-r) + 68px);
  bottom: calc(var(--fp-safe-b) + 92px);
  max-width: 60%; padding: var(--fp-s2) var(--fp-s3);
  border-radius: var(--fp-r-card); background: rgba(0,0,0,.72);
  border: 1px solid var(--fp-hairline);
  font-size: var(--fp-t-xs); pointer-events: none;
  animation: fp-in var(--fp-enter); transition: opacity var(--fp-enter);
}
.fp-chat-bubble.out { opacity: 0; }

/* ---------- tournament bracket ----------
 * Columns scroll horizontally inside their own container; the page body must
 * never scroll sideways. */
.fp-bracket {
  display: flex; gap: var(--fp-s4); overflow-x: auto;
  padding: var(--fp-s2) 0 var(--fp-s4); -webkit-overflow-scrolling: touch;
}
.fp-bracket-col {
  flex: 0 0 auto; display: flex; flex-direction: column;
  gap: var(--fp-s3); min-width: 150px;
  /* Later rounds have fewer matches; centring keeps the tree readable. */
  justify-content: space-around;
}
.fp-bracket-round {
  font-size: var(--fp-t-xs); color: var(--fp-ink-faint); font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em; text-align: center;
}
.fp-bracket-match {
  border-radius: var(--fp-r-input); overflow: hidden;
  border: 1px solid var(--fp-hairline); background: var(--fp-bg-elev);
}
.fp-bracket-match.mine { border-color: var(--fp-accent); }
.fp-bracket-slot {
  padding: var(--fp-s2) var(--fp-s3); font-size: var(--fp-t-xs);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  border-bottom: 1px solid var(--fp-hairline); color: var(--fp-ink-dim);
}
.fp-bracket-slot:last-child { border-bottom: 0; }
.fp-bracket-slot.won { color: var(--fp-good); font-weight: 800; }

.fp-champion {
  text-align: center; padding: var(--fp-s5) var(--fp-s4);
  border-radius: var(--fp-r-card);
  background: linear-gradient(160deg, rgba(255,215,106,.14), var(--fp-bg-elev) 70%);
  border: 1px solid rgba(255,215,106,.35);
}
.fp-champion-crown { color: var(--fp-gold); }
.fp-champion-name { font-size: var(--fp-t-lg); font-weight: 800; margin-top: var(--fp-s2); }

/* ---------- matchmaking ---------- */
.fp-search {
  flex: 0 0 auto; text-align: center;
  padding: var(--fp-s6) var(--fp-s4);
  display: flex; flex-direction: column; align-items: center; gap: var(--fp-s3);
}
.fp-search-orb {
  width: 96px; height: 96px; border-radius: 50%;
  display: grid; place-items: center; font-size: 40px;
  background: radial-gradient(circle at 35% 30%, var(--fp-bg-elev-2), var(--fp-bg-elev));
  border: 2px solid var(--fp-accent);
  animation: fp-pulse 1.6s ease-in-out infinite;
}
@keyframes fp-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(74,168,255,.45); }
  50%      { box-shadow: 0 0 0 16px rgba(74,168,255,0); }
}
@media (prefers-reduced-motion: reduce) {
  .fp-search-orb { animation: none; }
}
.fp-search-status { font-size: var(--fp-t-md); font-weight: 800; }
.fp-search-detail { font-size: var(--fp-t-sm); color: var(--fp-ink-dim); }

/* ---------- result ---------- */
.fp-result-head { text-align: center; padding: var(--fp-s5) 0 var(--fp-s4); }
.fp-result-title {
  font-size: var(--fp-t-xl); font-weight: 800; letter-spacing: .01em;
}
.fp-result-sub { font-size: var(--fp-t-sm); color: var(--fp-ink-dim); margin-top: var(--fp-s1); }

.fp-deltas {
  display: flex; gap: var(--fp-s3); margin: var(--fp-s4) 0;
}
.fp-delta {
  flex: 1 1 0; text-align: center; padding: var(--fp-s3);
  border-radius: var(--fp-r-card);
  background: var(--fp-bg-elev); border: 1px solid var(--fp-hairline);
}
.fp-delta-label {
  font-size: var(--fp-t-xs); color: var(--fp-ink-faint);
  text-transform: uppercase; letter-spacing: .08em; font-weight: 700;
}
.fp-delta-value { font-size: var(--fp-t-lg); font-weight: 800; margin-top: var(--fp-s1); }
.fp-delta-value.gold { color: var(--fp-gold); }
.fp-delta-diff { font-size: var(--fp-t-sm); font-weight: 800; }

/* ---------- Home: hero + rail (UI_SPEC §2) ----------
 * Landscape-only, so this is the single layout - no portrait variant to keep
 * in sync. Hero takes ~58% of the width because exactly one action should
 * dominate; the rail carries everything else at a glance. */
.fp-home { display: flex; flex-direction: column; height: 100%; }

.fp-idbar {
  flex: 0 0 auto; display: flex; align-items: center; gap: var(--fp-s3);
  margin-bottom: var(--fp-s3);
}
.fp-id {
  display: flex; align-items: center; gap: var(--fp-s2);
  background: none; border: 0; padding: 0; color: inherit; cursor: pointer;
  min-width: 0; font: inherit;
}
.fp-id-text { min-width: 0; text-align: left; }
.fp-id-name {
  font-size: var(--fp-t-sm); font-weight: 800;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fp-id-rating {
  display: flex; align-items: baseline; gap: 6px;
  font-size: var(--fp-t-xs); color: var(--fp-ink-dim); font-weight: 700;
}
.fp-id-delta { font-weight: 800; display: inline-flex; align-items: center; gap: 2px; }
.fp-id-delta.up { color: var(--fp-good); }
.fp-id-delta.down { color: var(--fp-bad); }
.fp-avatar-img { background-size: cover; background-position: center; }

.fp-premium-chip {
  padding: 4px 10px; border-radius: var(--fp-r-pill);
  background: rgba(255,215,106,.14); color: var(--fp-gold);
  border: 1px solid rgba(255,215,106,.35);
  font-size: var(--fp-t-xs); font-weight: 800; letter-spacing: .04em;
  display: inline-flex; align-items: center; gap: 4px;
}

.fp-offline-strip {
  flex: 0 0 auto; margin-bottom: var(--fp-s3);
  padding: var(--fp-s2) var(--fp-s3); border-radius: var(--fp-r-input);
  background: rgba(255,107,92,.12); border: 1px solid rgba(255,107,92,.3);
  color: var(--fp-bad); font-size: var(--fp-t-xs); font-weight: 700;
}
.fp-offline-strip.hidden { display: none; }

.fp-home-body {
  flex: 1 1 auto; display: flex; gap: var(--fp-s3); min-height: 0;
}

/* --- hero --- */
/* The hero sits on a real pool-hall photograph rather than flat colour - it
 * is the one place on Home with room for atmosphere, and a photo sells the
 * game better than a blue rectangle.
 *
 * Everything on top lives in a frosted panel: the image is busy and text over
 * an unmodified photo is unreadable at speed. `backdrop-filter` frosts the
 * image behind the panel where supported; the semi-opaque background is the
 * fallback and also guarantees contrast where it is not. */
/* When the hero is offering a rematch it gains a sibling, so the flex slot the
 * hero used to own moves out to the block and the hero fills it. Without this
 * the two buttons each claimed a full column and squeezed the rail. */
.fp-hero-block {
  flex: 1.4 1 0; min-width: 0;
  display: flex; flex-direction: column; gap: var(--fp-s2);
}
.fp-hero-block > .fp-hero { flex: 1 1 auto; }

.fp-hero-alt {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center; gap: var(--fp-s2);
  padding: var(--fp-s3); cursor: pointer;
  border-radius: var(--fp-r-card); border: 1px solid rgba(255,255,255,.16);
  background: rgba(255,255,255,.07); color: #fff;
  font: inherit; font-weight: 600;
  transition: transform var(--fp-tap), background var(--fp-tap);
}
.fp-hero-alt:active { transform: scale(.99); background: rgba(255,255,255,.12); }
.fp-hero-alt-glyph { font-size: 1.1em; }

.fp-hero {
  position: relative; overflow: hidden;
  flex: 1.4 1 0; min-width: 0;
  display: flex; flex-direction: column; justify-content: space-between;
  padding: var(--fp-s4); cursor: pointer; text-align: left;
  border-radius: var(--fp-r-card); border: 1px solid rgba(255,255,255,.16);
  /* menu-bg is composed for this job: dark, uncluttered centre for text with
   * the gold framing and balls at the edges. A photo of a full room (e.g.
   * pool-club.jpg) crops to an unreadable green blob at this aspect ratio. */
  background-image: url('../assets/bgs/menu-bg.jpg');
  background-size: cover; background-position: center center;
  color: #fff; font: inherit;
  box-shadow: var(--fp-shadow-2);
  transition: transform var(--fp-tap), filter var(--fp-tap);
}
/* Darkening wash: keeps the photo readable as a backdrop without washing the
 * frosted panels out. */
.fp-hero::before {
  content: ''; position: absolute; inset: 0;
  /* 15% less transparent than it was (.18/.48). A stronger wash both settles
     the photograph down behind the copy and gives the drifting balls something
     to read against, which they did not have over the brighter areas. */
  background: linear-gradient(160deg, rgba(6,20,13,.33), rgba(6,20,13,.63));
  pointer-events: none;
}
.fp-hero > * { position: relative; z-index: 1; }

.fp-hero-top, .fp-hero-mid, .fp-hero-meter {
  background: rgba(8, 22, 15, .67);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  backdrop-filter: blur(14px) saturate(1.1);
  border: 1px solid rgba(255,255,255,.14);
  border-radius: var(--fp-r-card);
  padding: var(--fp-s3);
}

/* The middle panel is the one the balls are behind, so it is the one that
   has to let them through. At .67 over a 14px blur they were erased
   completely: the balls were drawing the whole time and simply could not be
   seen. Thinner glass and a softer blur put them back without giving up the
   contrast the record line needs. The top bar and the meter keep the
   original treatment, because nothing is behind them. */
.fp-hero-mid {
  background: rgba(8, 22, 15, .30);
  -webkit-backdrop-filter: blur(5px) saturate(1.05);
  backdrop-filter: blur(5px) saturate(1.05);
  border: 1px solid rgba(255,255,255,.14);
  border-radius: var(--fp-r-card);
  padding: var(--fp-s3);
}
@supports not (backdrop-filter: blur(1px)) {
  .fp-hero-top, .fp-hero-meter { background: rgba(10, 26, 18, .78); }
  .fp-hero-mid { background: rgba(10, 26, 18, .42); }
}
.fp-hero:active { transform: scale(.99); }
.fp-hero.disabled, .fp-hero[disabled] {
  filter: grayscale(.6) brightness(.7); cursor: not-allowed;
}
.fp-hero.disabled:active { transform: none; }
.fp-hero-top { display: flex; align-items: center; gap: var(--fp-s3); }

/* The button affordance inside the hero. Solid accent fill against the frosted
 * panel, so it reads as the thing to press even though the whole card is the
 * target. 44px min height is the platform touch floor. */
.fp-hero-cta {
  flex: none; display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 0 18px;
  border-radius: 999px;
  background: var(--fp-accent);
  color: #04140c; font-size: var(--fp-t-sm); font-weight: 900;
  letter-spacing: .06em;
  box-shadow: 0 6px 18px rgba(0,0,0,.38), inset 0 1px 0 rgba(255,255,255,.35);
}
.fp-hero-cta-arrow { font-size: 1.1em; line-height: 1; }
.fp-hero:active .fp-hero-cta { filter: brightness(.92); }

/* A subtle pulse the first time it renders, drawing the eye to the primary
 * action once rather than animating forever. */
@keyframes fp-cta-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.045); }
}
.fp-hero-cta { animation: fp-cta-pulse 2.4s ease-in-out 3; }
@media (prefers-reduced-motion: reduce) {
  .fp-hero-cta { animation: none; }
}
.fp-hero-glyph { font-size: 34px; line-height: 1; }
.fp-hero-title { font-size: var(--fp-t-xl); font-weight: 800; line-height: 1.1; }
.fp-hero-sub { font-size: var(--fp-t-sm); color: rgba(255,255,255,.82); margin-top: 4px; }

.fp-hero-meter { margin-top: var(--fp-s2); }
.fp-hero-meter-row {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--fp-s2); margin-bottom: 6px;
}
.fp-hero-meter-label { font-size: var(--fp-t-xs); color: rgba(255,255,255,.78); }
.fp-hero-meter-value { font-size: var(--fp-t-sm); font-weight: 800; }
.fp-hero-meter-track {
  height: 6px; border-radius: var(--fp-r-pill);
  background: rgba(0,0,0,.32); overflow: hidden;
}
.fp-hero-meter-fill {
  height: 100%; border-radius: var(--fp-r-pill);
  background: #fff; transition: width var(--fp-enter);
}
.fp-hero-meter.empty .fp-hero-meter-fill { background: var(--fp-bad); }
.fp-hero-meter.premium .fp-hero-meter-fill {
  background: linear-gradient(90deg, var(--fp-gold), #fff);
}

/* --- rail --- */
.fp-rail {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; gap: var(--fp-s2);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
}
.fp-rail-item {
  flex: 1 1 0; min-height: 52px;
  display: flex; align-items: center; gap: var(--fp-s3);
  padding: var(--fp-s2) var(--fp-s3); cursor: pointer; text-align: left;
  border-radius: var(--fp-r-card); border: 1px solid var(--fp-hairline);
  background: var(--fp-bg-elev); color: var(--fp-ink); font: inherit;
  transition: background var(--fp-tap), transform var(--fp-tap);
}
.fp-rail-item:active { transform: scale(.985); background: var(--fp-bg-elev-2); }
.fp-rail-item.disabled, .fp-rail-item[disabled] { opacity: .45; cursor: not-allowed; }
.fp-rail-item.disabled:active { transform: none; }
.fp-rail-glyph { font-size: var(--fp-t-md); width: 26px; text-align: center; flex: 0 0 auto; }
.fp-rail-body { flex: 1 1 auto; min-width: 0; }
.fp-rail-title { font-size: var(--fp-t-sm); font-weight: 800; }
.fp-rail-desc {
  font-size: var(--fp-t-xs); color: var(--fp-ink-dim); margin-top: 1px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Very short viewports: drop the rail descriptions before anything scrolls. */
@media (max-height: 400px) {
  .fp-rail-desc { display: none; }
  .fp-hero-title { font-size: var(--fp-t-lg); }
  /* Keep the CTA - it is the affordance - but tighten it. Never below the
   * 44px touch floor. */
  .fp-hero-cta { min-height: 44px; padding: 0 14px; letter-spacing: .04em; }
}

/* ---------- portrait guard (UI_SPEC decision 4) ----------
 * The app is landscape-only. Where the orientation lock is not permitted
 * (most mobile browsers outside fullscreen) we ask rather than render a
 * layout that was never designed. */
#fp-rotate {
  position: fixed; inset: 0; z-index: 90; display: none;
  flex-direction: column; align-items: center; justify-content: center;
  gap: var(--fp-s3); padding: var(--fp-s5);
  background: var(--fp-bg); color: var(--fp-ink);
  font-family: var(--fp-font); text-align: center;
}
#fp-rotate .glyph { font-size: 56px; animation: fp-rotate-hint 2.4s ease-in-out infinite; }
#fp-rotate .title { font-size: var(--fp-t-lg); font-weight: 800; }
#fp-rotate .sub { font-size: var(--fp-t-sm); color: var(--fp-ink-dim); }
@keyframes fp-rotate-hint {
  0%, 40%, 100% { transform: rotate(0deg); }
  60%, 90% { transform: rotate(-90deg); }
}
@media (orientation: portrait) {
  #fp-rotate { display: flex; }
}
@media (prefers-reduced-motion: reduce) {
  #fp-rotate .glyph { animation: none; }
}

/* ---------- hero middle: player form ----------
 * Fills the space the hero reserves. Centred and vertically flexible so the
 * hero looks deliberate at any height rather than top-weighted with a void. */
.fp-hero-mid {
  flex: 1 1 auto; display: flex; flex-direction: column;
  align-items: flex-start; justify-content: center; gap: var(--fp-s2);
  margin: var(--fp-s2) 0;
}
.fp-hero-record {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 34px; font-weight: 800; line-height: 1;
}
.fp-hero-rec-w { color: #fff; }
.fp-hero-rec-sep { color: rgba(255,255,255,.45); font-size: var(--fp-t-lg); }
.fp-hero-rec-l { color: rgba(255,255,255,.62); }

.fp-hero-form { display: flex; gap: 5px; }
.fp-form-pip {
  width: 22px; height: 22px; border-radius: 6px;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 800;
}
.fp-form-pip.w { background: rgba(140,255,170,.9); color: #06301a; }
.fp-form-pip.l { background: rgba(0,0,0,.28); color: rgba(255,255,255,.75); }

.fp-hero-streak {
  font-size: var(--fp-t-xs); font-weight: 700; color: rgba(255,255,255,.85);
  display: flex; align-items: center; gap: 5px;
}
.fp-hero-streak .fp-icon { color: var(--fp-warn); }
.fp-hero-invite { font-size: var(--fp-t-lg); font-weight: 800; color: #fff; }
.fp-hero-invitesub {
  font-size: var(--fp-t-sm); color: rgba(255,255,255,.8); max-width: 34ch;
}

@media (max-height: 400px) {
  .fp-hero-record { font-size: var(--fp-t-lg); }
  .fp-hero-invitesub { display: none; }
}

/* ---------- short-viewport pass ----------
 * A 1600x720 phone at 300dpi gives a CSS viewport of roughly 853x384. The
 * Home layout was designed against a desktop-sized window and overflowed
 * badly there: the hero meter and the last rail item were cut off entirely.
 *
 * Rather than scatter magic numbers, type and spacing scale with viewport
 * height between the phone case and the desktop one. clamp() keeps both ends
 * sane - nothing shrinks below legibility, nothing balloons on a tablet. */
@media (max-height: 520px) {
  .fp-screen {
    padding: calc(var(--fp-safe-t) + 8px) calc(var(--fp-safe-r) + 12px)
             calc(var(--fp-safe-b) + 8px) calc(var(--fp-safe-l) + 12px);
  }

  .fp-idbar { margin-bottom: 8px; }
  .fp-avatar { width: 34px; height: 34px; font-size: var(--fp-t-sm); }

  .fp-home-body { gap: 8px; }

  .fp-hero { padding: 10px; }
  .fp-hero-top, .fp-hero-mid, .fp-hero-meter { padding: 8px 10px; }
  .fp-hero-glyph { font-size: clamp(20px, 5vh, 30px); }
  .fp-hero-title { font-size: clamp(17px, 5.2vh, 26px); }
  .fp-hero-sub { font-size: clamp(11px, 2.6vh, 13px); }
  .fp-hero-mid { margin: 6px 0; gap: 4px; }
  .fp-hero-record { font-size: clamp(18px, 5.6vh, 28px); }
  .fp-form-pip { width: 18px; height: 18px; font-size: 10px; border-radius: 5px; }
  .fp-hero-invite { font-size: clamp(15px, 4vh, 20px); }
  .fp-hero-meter { margin-top: 6px; }
  .fp-hero-meter-label, .fp-hero-meter-value { font-size: clamp(10px, 2.4vh, 13px); }

  .fp-rail { gap: 6px; }
  .fp-rail-item { min-height: 0; padding: 6px 10px; }
  .fp-rail-glyph { font-size: clamp(14px, 3.4vh, 18px); width: 22px; }
  .fp-rail-title { font-size: clamp(12px, 3vh, 15px); }
  .fp-rail-desc { font-size: clamp(10px, 2.4vh, 12px); }

  /* The ad slot keeps a reserved height, but a 50px band is a large share of
   * a 384px viewport - trim it to the AdMob small-banner height. */
  .fp-adslot { min-height: 32px; margin-top: 8px; }
}

/* Very short (split-screen, small phones in landscape): drop the secondary
 * copy before anything is allowed to clip. */
@media (max-height: 360px) {
  .fp-rail-desc, .fp-hero-invitesub { display: none; }
  .fp-hero-sub { display: none; }
}

/* The same short-viewport treatment for the shared components. Home was fixed
 * first, but Create Room, the lobby, the store and the profile all use
 * .fp-card / .fp-stack / .fp-topbar and overflowed identically on a 384px
 * viewport - the primary button sat below the fold on every one of them. */
@media (max-height: 520px) {
  .fp-topbar { margin-bottom: 10px; }
  .fp-stack { gap: 8px; }

  .fp-card { min-height: 0; padding: 10px 12px; }
  .fp-card.primary { min-height: 0; padding: 12px; }
  .fp-card-glyph { font-size: clamp(15px, 3.6vh, 20px); width: 26px; }
  .fp-card-title { font-size: clamp(12px, 3.1vh, 15px); }
  .fp-card.primary .fp-card-title { font-size: clamp(14px, 3.8vh, 19px); }
  .fp-card-desc { font-size: clamp(10px, 2.4vh, 12px); }

  .fp-seg-btn { min-height: 32px; font-size: clamp(11px, 2.8vh, 14px); }
  .fp-switch { width: 40px; height: 24px; }
  .fp-switch-knob { width: 17px; height: 17px; }
  .fp-switch.on .fp-switch-knob { transform: translateX(16px); }

  .fp-field-label { margin-bottom: 4px; }
  .fp-codebox { padding: 8px 0; }
  .fp-code-input { font-size: clamp(22px, 7vh, 34px); padding: 8px; }
  .fp-code { font-size: clamp(24px, 7.5vh, 38px); }
  .fp-qr { padding: 4px; }
  .fp-qr svg { width: clamp(90px, 26vh, 150px); height: clamp(90px, 26vh, 150px); }
  .fp-share { padding: 10px; gap: 8px; }

  .fp-player { padding: 8px 10px; }
  .fp-avatar { width: 32px; height: 32px; }
  .fp-deltas { margin: 10px 0; gap: 8px; }
  .fp-delta { padding: 8px; }
  .fp-delta-value { font-size: clamp(14px, 4vh, 20px); }

  .fp-buycard { padding: 10px 12px; gap: 8px; }
  .fp-buycard-title { font-size: clamp(13px, 3.2vh, 16px); }
  .fp-fineprint { font-size: clamp(9px, 2.2vh, 11px); }
  .fp-benefit { font-size: clamp(11px, 2.6vh, 13px); }

  .fp-result-head { padding: 10px 0 8px; }
  .fp-result-title { font-size: clamp(20px, 6vh, 30px); }
  .fp-search { padding: 16px; gap: 8px; }
  .fp-search-orb { width: clamp(52px, 16vh, 90px); height: clamp(52px, 16vh, 90px); font-size: clamp(22px, 7vh, 38px); }
}

/* ---------- onboarding, short landscape ----------
 * These live here rather than in index.html's inline <style> because that
 * block declares the base #onboarding-steps rule *after* the media query,
 * and equal-specificity later rules win. shell.css loads after the inline
 * styles, so this reliably takes effect. */
@media (max-height: 520px) {
  #onboarding-card { gap: 8px !important; padding: 12px 16px !important;
    width: min(620px, 94vw) !important; }
  #onboarding-card h2 { font-size: 18px !important; }
  #onboarding-card p { font-size: 12px !important; }
  #onboarding-steps { flex-direction: row !important; gap: 6px !important; }
  #onboarding-steps .ob-step { font-size: 11px !important; padding: 6px 8px !important;
    flex: 1 1 0; align-items: flex-start; }
  #onboarding-steps .ob-step-icon { font-size: 14px !important; }
}

/* ---------- locker swatches ----------
 * A wrapping grid rather than a carousel: every option visible at once beats
 * swiping through a reel to find the one you want. */
.fp-swatches {
  display: flex; flex-wrap: wrap; gap: var(--fp-s2);
}
.fp-swatch {
  display: flex; align-items: center; gap: var(--fp-s2);
  padding: 6px 10px 6px 6px; cursor: pointer;
  border-radius: var(--fp-r-pill);
  border: 1.5px solid var(--fp-hairline);
  background: var(--fp-bg-elev); color: var(--fp-ink);
  font: inherit; font-size: var(--fp-t-xs); font-weight: 700;
  transition: border-color var(--fp-tap), background var(--fp-tap);
}
.fp-swatch:active { background: var(--fp-bg-elev-2); }
.fp-swatch.on {
  border-color: var(--fp-good);
  box-shadow: 0 0 10px rgba(140,255,170,.22);
}
/* Locked items stay tappable - the tap opens the paywall, which is more
 * useful than a dead control. */
.fp-swatch.locked { opacity: .62; }
.fp-swatch-chip {
  width: 22px; height: 22px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,.25); flex: 0 0 auto;
  box-shadow: inset 0 -2px 4px rgba(0,0,0,.4);
}
/* Cue chips show real sprite-sheet artwork, so they are cue-shaped rather
   than round - a circular crop of a 30x600 cue is just a smear of colour. */
.fp-swatch-chip.cue {
  width: 12px; height: 30px; border-radius: 3px;
  box-shadow: none; border-color: rgba(255,255,255,.18);
}
.fp-swatch-name { white-space: nowrap; }
.fp-swatch-lock { color: var(--fp-gold); font-size: 11px; }

@media (max-height: 520px) {
  .fp-swatch { padding: 4px 8px 4px 4px; font-size: 11px; }
  .fp-swatch-chip { width: 18px; height: 18px; }
  .fp-swatch-chip.cue { width: 10px; height: 24px; }
  .fp-swatches { gap: 6px; }
}

/* ---------- layer scale ----------
 * One place that decides what covers what. These were ad-hoc numbers, which
 * is how the in-match chat ended up at z-index 30 - underneath every other
 * overlay including the spectator chip, so the panel a player opened could be
 * covered by chrome they did not open. */
:root {
  --fp-z-hud:       20;   /* in-match HUD, always beneath any panel */
  --fp-z-chip:      25;   /* spectator chip and similar passive chrome */
  --fp-z-chat:      36;   /* in-match chat: above all in-match chrome */
  --fp-z-shell:     40;   /* the DOM screen stack */
  --fp-z-modal:     70;   /* decisions that block play */
  --fp-z-onboard:   90;   /* first-run, above everything */
}

/* ---------- blocking modal ---------- */
.fp-modal-wrap {
  position: fixed; inset: 0; z-index: var(--fp-z-modal);
  /* flex-start + margin:auto on the child, NOT align-items:center.
     Centring a flex child that is taller than its container overflows in
     BOTH directions, and the overflowing top cannot be scrolled to - which
     is how the setup modal came to have its own title cut off on a landscape
     phone. margin:auto centres while it fits and falls back to the top when
     it does not, so nothing is ever unreachable. */
  display: flex; align-items: flex-start; justify-content: center;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: var(--fp-s4);
  background: rgba(4, 12, 8, .72);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  font-family: var(--fp-font);
}
.fp-modal {
  width: min(460px, 100%);
  margin: auto;
  background: var(--fp-surface); color: var(--fp-ink);
  border: 1px solid rgba(255,255,255,.14);
  border-radius: var(--fp-r-card);
  padding: var(--fp-s4);
  box-shadow: var(--fp-shadow-2);
}
.fp-modal-title { font-size: var(--fp-t-lg); font-weight: 800; }
.fp-modal-body { font-size: var(--fp-t-sm); color: var(--fp-ink-dim); margin-top: 6px; }
.fp-modal-count {
  font-size: var(--fp-t-xs); color: var(--fp-ink-dim);
  margin-top: var(--fp-s2); font-variant-numeric: tabular-nums;
}
.fp-modal-actions {
  display: flex; gap: var(--fp-s2); margin-top: var(--fp-s3);
  flex-wrap: wrap;
}
.fp-btn {
  flex: 1 1 auto; min-height: 44px; padding: 0 var(--fp-s3);
  border-radius: var(--fp-r-btn);
  border: 1px solid rgba(255,255,255,.18);
  background: rgba(255,255,255,.06); color: var(--fp-ink);
  font: inherit; font-weight: 700; cursor: pointer;
}
.fp-btn.primary {
  background: var(--fp-accent); color: #04140c;
  border-color: transparent;
}
.fp-btn:active { transform: scale(.99); }

/* Short landscape phones: the modal must never need scrolling. */
@media (max-height: 420px) {
  .fp-modal { padding: var(--fp-s3); }
  .fp-modal-body { font-size: var(--fp-t-xs); }
}

/* ---------- chat emote row (#6) ----------
 * Emotes moved out of a standalone in-match button and into the chat sheet.
 * Sized to the 44px touch floor, and scrollable rather than wrapping so the
 * sheet's height never changes as the set grows. */
.fp-chat-emotes {
  display: flex; gap: 6px; padding: var(--fp-s2) 0;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.fp-chat-emotes::-webkit-scrollbar { display: none; }
.fp-chat-emote {
  flex: 0 0 auto;
  min-width: 44px; min-height: 44px;
  border-radius: var(--fp-r-input);
  border: 1px solid var(--fp-hairline);
  background: rgba(255,255,255,.06);
  font-size: 20px; line-height: 1; cursor: pointer;
}
.fp-chat-emote:active { transform: scale(.92); }
/* Spectators read the chat but cannot write to it. */
#fp-chat.readonly .fp-chat-emotes { display: none; }

@media (max-height: 420px) {
  .fp-chat-emote { min-width: 40px; min-height: 40px; font-size: 18px; }
}

/* ---------- first-run setup ---------- */
.fp-setup { width: min(560px, 100%); }
.fp-setup-head { margin-bottom: var(--fp-s3); }
.fp-setup-name {
  width: 100%; min-height: 44px; padding: 0 var(--fp-s3);
  border-radius: var(--fp-r-input);
  border: 1px solid var(--fp-hairline);
  background: rgba(0,0,0,.3); color: var(--fp-ink);
  font: inherit; font-size: var(--fp-t-md);
}
.fp-setup-avatars {
  display: flex; gap: 8px; padding: 4px 0 2px;
  overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.fp-setup-avatars::-webkit-scrollbar { display: none; }
.fp-setup-avatar {
  flex: 0 0 auto; width: 52px; height: 52px; border-radius: 50%;
  border: 2px solid transparent; background-color: rgba(255,255,255,.06);
  cursor: pointer; position: relative; padding: 0;
  display: grid; place-items: center;
}
.fp-setup-avatar.on {
  border-color: var(--fp-accent);
  box-shadow: 0 0 0 3px rgba(74,168,255,.25);
}
.fp-setup-avatar.custom {
  border: 2px dashed var(--fp-hairline); color: var(--fp-ink-dim);
  background-size: cover; background-position: center;
}
.fp-setup-plus { font-size: 22px; line-height: 1; }
.fp-setup-lock {
  position: absolute; right: -2px; bottom: -2px;
  font-size: 11px; color: var(--fp-gold);
  background: var(--fp-surface); border-radius: 50%;
  width: 18px; height: 18px; display: grid; place-items: center;
  border: 1px solid rgba(255,215,106,.4);
}
.fp-setup-promo {
  display: flex; align-items: center; gap: var(--fp-s2);
  width: 100%; min-height: 44px; margin-top: var(--fp-s3);
  padding: 0 var(--fp-s3); border-radius: var(--fp-r-input);
  background: rgba(255,215,106,.10);
  border: 1px solid rgba(255,215,106,.28);
  color: var(--fp-gold); font: inherit; font-size: var(--fp-t-xs);
  font-weight: 700; cursor: pointer; text-align: left;
}
.fp-setup-promo-text { flex: 1 1 auto; min-width: 0; }
.fp-setup-promo-go { font-size: var(--fp-t-md); }
.fp-setup-start { width: 100%; margin-top: var(--fp-s3); }

/* Short landscape phones: the whole modal must fit without scrolling. */
@media (max-height: 420px) {
  .fp-setup-avatar { width: 44px; height: 44px; }
  .fp-setup-head .fp-modal-body { display: none; }
  .fp-setup-promo { min-height: 40px; }
}

/* ---------- spectator waiting cover (#9) ---------- */
.fp-specwait {
  position: fixed; inset: 0; z-index: var(--fp-z-modal);
  display: grid; place-items: center; padding: var(--fp-s4);
  background: rgba(4, 12, 8, .88);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  font-family: var(--fp-font); color: var(--fp-ink);
}
.fp-specwait-card {
  width: min(520px, 100%); text-align: center;
  background: var(--fp-surface); border: 1px solid var(--fp-hairline);
  border-radius: var(--fp-r-card); padding: var(--fp-s4);
  box-shadow: var(--fp-shadow-2);
}
.fp-specwait-eye { font-size: 28px; }
.fp-specwait-title { font-size: var(--fp-t-lg); font-weight: 800; margin-top: 4px; }
.fp-specwait-sub { font-size: var(--fp-t-sm); color: var(--fp-ink-dim); margin-top: 4px; }
.fp-specwait-players {
  display: flex; gap: var(--fp-s3); justify-content: center;
  margin: var(--fp-s3) 0;
}
.fp-specwait-player { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.fp-specwait-rules { font-size: var(--fp-t-xs); color: var(--fp-ink-faint); }
.fp-specwait-count {
  font-size: var(--fp-t-xs); color: var(--fp-ink-dim);
  margin: var(--fp-s2) 0 var(--fp-s3);
}

/* ---------- spectator list (#17) ---------- */
.fp-speclist {
  position: fixed; z-index: var(--fp-z-modal);
  top: calc(var(--fp-safe-t) + 52px); right: calc(var(--fp-safe-r) + 10px);
  width: min(260px, 70vw); max-height: 60vh; overflow-y: auto;
  background: var(--fp-surface); border: 1px solid var(--fp-hairline);
  border-radius: var(--fp-r-card); padding: var(--fp-s3);
  box-shadow: var(--fp-shadow-2); font-family: var(--fp-font); color: var(--fp-ink);
}
.fp-speclist-head {
  display: flex; align-items: center; justify-content: space-between;
  font-weight: 800; font-size: var(--fp-t-sm); margin-bottom: var(--fp-s2);
}
.fp-speclist-row {
  display: flex; align-items: center; gap: var(--fp-s2);
  padding: 5px 0;
}
.fp-speclist-row .fp-avatar { width: 28px; height: 28px; font-size: 12px; }

#fp-spectators { cursor: pointer; border: none; font: inherit; }

@media (max-height: 420px) {
  .fp-specwait-card { padding: var(--fp-s3); }
  .fp-specwait-sub { display: none; }
}

/* ---------- friends ---------- */
.fp-friendcode {
  background: rgba(255,255,255,.05);
  border: 1px solid var(--fp-hairline);
  border-radius: var(--fp-r-card);
  padding: var(--fp-s3);
}
.fp-friendcode-value {
  font-family: ui-monospace, 'Cascadia Mono', Consolas, monospace;
  font-size: var(--fp-t-xl); font-weight: 800; letter-spacing: .18em;
  color: var(--fp-gold);
}
@media (max-height: 420px) {
  .fp-friendcode { padding: var(--fp-s2); }
  .fp-friendcode-value { font-size: var(--fp-t-lg); }
}

/* ---------- in-match banner ----------
 * Docked to the bottom band, below the cushion - never over the playing
 * surface and never over a control. The close button is a 44px target and
 * opens the paywall rather than silently dismissing (see gameBanner.js). */
/* Full-width row, close button pinned to the right EDGE of the screen.
 *
 * Centring the row meant the close button sat wherever the banner happened to
 * end - which on a wide screen is directly over the table. The ad itself is
 * drawn natively at the bottom centre; only the button needs a home, and the
 * far right corner is both out of the cue's way and where a dismiss control
 * is expected. */
#fp-gamebanner {
  position: fixed; z-index: var(--fp-z-chip);
  left: 0; right: 0;
  bottom: calc(var(--fp-safe-b) + 4px);
  display: flex; align-items: center; justify-content: flex-end;
  gap: 6px;
  padding: 0 calc(var(--fp-safe-r) + 8px) 0 calc(var(--fp-safe-l) + 8px);
  pointer-events: none;      /* the row is a layout shell, not a target */
  font-family: var(--fp-font);
}
#fp-gamebanner > * { pointer-events: auto; }
#fp-gamebanner.hidden { display: none; }
.fp-gamebanner-slot {
  /* Only reserves space for the native banner, which is drawn outside the
     WebView. It must not take the width the close button needs. */
  /* Height follows the adaptive banner the native side now requests: about
     50dp on a phone, up to 90dp on a tablet. The reservation has to match, or
     the ad is clipped from below on a large screen - which is what made the
     in-game banner look squashed on the tablet. Width stays at zero because
     the ad is drawn outside the WebView; this element only holds space. */
  flex: 0 1 auto; min-width: 0; min-height: 50px; max-width: 0;
  padding: 0; border: none; background: transparent; overflow: hidden;
  display: grid; place-items: center;
  border-radius: var(--fp-r-input);
  background: rgba(0,0,0,.45); border: 1px solid var(--fp-hairline);
  font-size: var(--fp-t-xs); color: var(--fp-ink-faint);
}
.fp-gamebanner-close {
  flex: 0 0 auto; width: 44px; height: 44px;
  border-radius: 50%; cursor: pointer;
  background: rgba(0,0,0,.72); color: var(--fp-ink);
  border: 1px solid rgba(255,255,255,.28);
  font-size: 15px; line-height: 1;
  display: grid; place-items: center;
}
.fp-gamebanner-close:active { transform: scale(.92); }

/* Tablets get the taller adaptive banner, so the in-match slot reserves for
   it. Same breakpoint as the menu slots, for the same AdMob height table. */
@media (min-width: 728px) {
  .fp-gamebanner-slot { min-height: 90px; }
}

/* Short landscape phones: keep the 44px close target, shrink only the slot. */
@media (max-height: 420px) {
  #fp-gamebanner { bottom: calc(var(--fp-safe-b) + 2px); }
  .fp-gamebanner-slot { min-height: 44px; }
}

/* Shown only once a picture has been chosen by a non-subscriber - the offer
   is concrete at that point, because their photo is already on screen. */
.fp-setup-upsell {
  display: none; align-items: center; gap: var(--fp-s2);
  margin-top: var(--fp-s2); padding: var(--fp-s2) var(--fp-s3);
  border-radius: var(--fp-r-input);
  background: rgba(255,215,106,.10);
  border: 1px solid rgba(255,215,106,.28);
}
.fp-setup-upsell.show { display: flex; }
.fp-setup-upsell-text {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--fp-t-xs); font-weight: 700; color: var(--fp-gold);
}
.fp-setup-upsell-btn { flex: 0 0 auto; min-height: 40px; padding: 0 var(--fp-s3); }
.fp-setup-avatar.custom .fp-setup-plus { font-size: 20px; }

/* ---------- forfeit countdown ---------- */
.fp-forfeit { border-color: rgba(255,107,92,.45); }
.fp-forfeit-count {
  font-size: 40px; font-weight: 900; text-align: center;
  color: var(--fp-bad); font-variant-numeric: tabular-nums;
  margin: var(--fp-s2) 0;
}
@media (max-height: 420px) {
  .fp-forfeit-count { font-size: 30px; margin: 4px 0; }
}

/* ---------- report dialog ---------- */
.fp-report { width: min(480px, 100%); }
.fp-report-reasons {
  display: flex; flex-direction: column; gap: 6px;
  margin: var(--fp-s3) 0;
}
.fp-report-reason {
  display: flex; align-items: center; justify-content: space-between; gap: var(--fp-s2);
  width: 100%; min-height: 52px; padding: 8px var(--fp-s3);
  border-radius: var(--fp-r-input);
  border: 1px solid var(--fp-hairline);
  background: rgba(255,255,255,.04);
  color: var(--fp-ink); font: inherit; text-align: left; cursor: pointer;
}
.fp-report-reason.on {
  border-color: var(--fp-accent);
  background: rgba(74,168,255,.12);
}
.fp-report-reason-label { font-size: var(--fp-t-sm); font-weight: 700; }
.fp-report-reason-desc { font-size: var(--fp-t-xs); color: var(--fp-ink-dim); margin-top: 1px; }
.fp-report-tick { color: var(--fp-accent); opacity: 0; font-weight: 900; }
.fp-report-reason.on .fp-report-tick { opacity: 1; }
.fp-report-quote {
  font-size: var(--fp-t-xs); color: var(--fp-ink-dim);
  padding: var(--fp-s2); border-radius: var(--fp-r-input);
  background: rgba(0,0,0,.28); font-style: italic;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-height: 420px) {
  .fp-report-reason { min-height: 44px; }
  .fp-report-reason-desc { display: none; }
  .fp-report .fp-modal-body { display: none; }
}

/* Native banner present: the slot only reserves height. Without this the
   dashed placeholder draws an empty box above the real ad.

   Verified against the real device (chrome://inspect on the WebView): the
   native <BannerAd> is a flex sibling of the WebView in App.tsx (both
   flex:1/auto in the same column), not an overlay drawn on top of it. React
   Native already shrinks the WebView's own height to exactly exclude the
   banner's area before this element ever measures anything, so reserving
   height here a second time only ate into Home's own content box - the
   50-90px "adaptive banner" strip this used to carve out was never sitting
   under an ad, it was just dead space above one. */
.fp-adslot.native {
  background: transparent; border: none; color: transparent;
  min-height: 0;
}

/* ---------- home top-bar stat chips ---------- */
.fp-statchips { display: flex; gap: 6px; align-items: stretch; }
.fp-statchip {
  min-width: 58px; padding: 4px 10px;
  border-radius: var(--fp-r-input);
  background: rgba(255,255,255,.05);
  border: 1px solid var(--fp-hairline);
  text-align: center;
}
.fp-statchip-value {
  font-size: var(--fp-t-sm); font-weight: 800; line-height: 1.15;
  display: flex; align-items: center; justify-content: center; gap: 3px;
}
.fp-statchip-value .fp-icon { color: var(--fp-warn); }
.fp-statchip-label {
  font-size: 10px; color: var(--fp-ink-faint);
  text-transform: uppercase; letter-spacing: .06em;
}
/* Short phones: the rail and hero matter more than a full stat row. */
@media (max-height: 420px) {
  .fp-statchip { min-width: 46px; padding: 3px 7px; }
  .fp-statchip-label { display: none; }
}

/* ---------- sign-in dialog ---------- */
.fp-signin { width: min(460px, 100%); }
.fp-signin-google { width: 100%; margin-top: var(--fp-s3); }
.fp-signin-or {
  text-align: center; font-size: var(--fp-t-xs); color: var(--fp-ink-faint);
  margin: var(--fp-s3) 0 var(--fp-s2);
}
@media (max-height: 420px) {
  .fp-signin .fp-modal-body { display: none; }
  .fp-signin-or { margin: var(--fp-s2) 0 6px; }
}

/* ---------- phone: keep the in-match controls clear of the native ad ----------
 * The native side ad occupies the right edge during a match, and the chat
 * button and spectator chip were docked to that same edge - so the ad covered
 * them. They move LEFT of the spin widget (the cue-ball hit control), which is
 * the only other thing living in that corner, rather than being stacked above
 * it where the ad now sits.
 *
 * Phone only: on a tablet or desktop there is room for both, and moving them
 * there would only make them harder to reach. */
@media (max-height: 460px) {
  /* Immediately LEFT of the spin widget (the white cue-ball control), which
   * sits bottom-right at right:10px and is 60px wide - so its left edge is at
   * right:70px. These tuck in beside it, on the same baseline, rather than
   * stacking above it where the native ad now lives. */
  .fp-chat-btn {
    right: calc(var(--fp-safe-r) + 78px);
    bottom: calc(var(--fp-safe-b) + 12px);
    width: 44px; height: 44px; font-size: 19px;
  }
  #fp-spectators {
    right: calc(var(--fp-safe-r) + 130px);
    bottom: calc(var(--fp-safe-b) + 20px);
  }
  /* Left-handed players have the spin widget bottom-LEFT, so mirror. */
  body.lefty .fp-chat-btn {
    right: auto; left: calc(var(--fp-safe-l) + 78px);
  }
  body.lefty #fp-spectators {
    right: auto; left: calc(var(--fp-safe-l) + 130px);
  }
}


/* ---------- incoming challenge ---------- */
/* A card, not a modal: an invitation should be answerable at a glance and
   ignorable without standing in front of whatever the player was doing.
   Above the shell so it is visible from any menu screen. */
/* Centred on both axes: a challenge is a decision, and a decision belongs
   where the player is already looking rather than tucked along an edge. */
.fp-challenge {
  position: fixed; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 9600;
  display: flex; align-items: center; gap: 14px;
  max-width: min(520px, calc(100vw - 24px));
  padding: 12px 14px;
  border-radius: var(--fp-r-card);
  background: var(--fp-panel, rgba(12,32,22,.98));
  border: 1px solid var(--fp-hairline);
  box-shadow: 0 10px 30px rgba(0,0,0,.45);
  animation: fpChallengeIn .22s ease;
}
.fp-challenge-body { flex: 1; min-width: 0; }
.fp-challenge-title { font-weight: 700; font-size: 15px; }
.fp-challenge-sub { font-size: 12px; opacity: .75; margin-top: 2px; }
.fp-challenge-actions { display: flex; gap: 8px; flex-shrink: 0; }
.fp-challenge-actions .fp-btn { padding: 8px 14px; font-size: 13px; }
@keyframes fpChallengeIn {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 10px)) scale(.98); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@media (max-width: 480px) {
  .fp-challenge { flex-direction: column; align-items: stretch; gap: 10px; }
  .fp-challenge-actions .fp-btn { flex: 1; }
}


/* ---------- home hero: drifting balls ---------- */
/* Sits between the panel's photograph and its content, so the balls read as
   being on the table in the picture rather than on top of the card. The hero
   already establishes a stacking context; everything inside it is above this
   because this is the first child and unpositioned content follows. */
.fp-hero { position: relative; overflow: hidden; }
/* Specificity matters here: `.fp-hero > *` below sets position relative on
   every child, and it outranks a single class selector. Left as it was, the
   canvas stopped being absolutely positioned and took layout space at the top
   of the panel instead of sitting behind it. */
/* Above the frosted panels, and only across the empty middle band.
   Full-bleed would put balls over the heading and the meter; behind the
   panels the backdrop blur turns them to mush, and inside one the Android
   WebView does not paint them at all. This is the position that works
   everywhere and reads correctly. */
/* ---------- home hero: drifting balls ---------- */
/* Sits between the panel's photograph and its content, so the balls read as
   being on the table in the picture rather than on top of the card. The hero
   already establishes a stacking context; everything inside it is above this
   because this is the first child and unpositioned content follows. */
.fp-hero { position: relative; overflow: hidden; }
.fp-hero-balls {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  /* Nearly solid now. They sit behind a panel that has been thinned out to
     let them through, so fading them as well left nothing to see. */
  opacity: .92;
}
.fp-hero > *:not(.fp-hero-balls) { position: relative; }



/* ---------- policy documents ---------- */
/* Framed rather than opened in a new window: the app has no second window and
   asking for one crashes it. Fills the screen under the back bar. */
.fp-policy-frame {
  flex: 1 1 auto;
  width: 100%;
  min-height: 0;
  border: 0;
  border-radius: var(--fp-r-card);
  background: var(--fp-bg);
}


/* ---------- opponent card, and incoming friend requests ---------- */
/* Opened by tapping the opponent's plate in the HUD and from the result
   screen. Above the shell, because in a match the shell is not open at all
   and the card still has to be on top of the table. */
.fp-pc-overlay {
  position: fixed; inset: 0; z-index: 90;
  display: flex; align-items: center; justify-content: center;
  padding: var(--fp-s4);
  background: rgba(0,0,0,0.62);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
}
.fp-pc-card {
  width: 100%; max-width: 360px;
  padding: var(--fp-s4);
  border-radius: var(--fp-r-card);
  background: var(--fp-panel, #16181d);
  border: 1px solid rgba(255,255,255,0.10);
  box-shadow: 0 18px 50px rgba(0,0,0,0.55);
}
.fp-pc-head { display: flex; align-items: center; gap: var(--fp-s3); }
.fp-pc-av { width: 46px; height: 46px; font-size: 20px; }

/* Four numbers on one row on any phone: a fixed column count wraps badly at
   320px, so they share the width instead. */
.fp-pc-stats {
  display: flex; gap: var(--fp-s2);
  margin: var(--fp-s4) 0;
}
.fp-pc-stat {
  flex: 1 1 0; min-width: 0; text-align: center;
  padding: var(--fp-s2) 2px;
  border-radius: var(--fp-r-sm, 8px);
  background: rgba(255,255,255,0.05);
}
.fp-pc-stat-v { font-weight: 700; font-size: 1.05rem; }
.fp-pc-stat-k { font-size: .72rem; opacity: .65; margin-top: 2px; }

.fp-pc-actions { display: flex; gap: var(--fp-s2); }
.fp-pc-actions .fp-btn { flex: 1 1 0; }

.fp-req {
  display: flex; align-items: center; gap: var(--fp-s2);
  padding: var(--fp-s2) 0;
}
.fp-minibtn-quiet { opacity: .7; }

/* ---------- capture: the REC toggle (ui/recorder.js) ----------
 *
 * Used to float over the table canvas, docked to whichever corner the shot
 * controls hadn't already claimed. There wasn't one: every corner belonged to
 * the cue, the power meter, the rack or the ad. It now sits in #hud instead
 * (index.html, #fp-rec-slot), in the band already reserved above the felt and
 * immediately right of the player's own card - an ordinary inline chip rather
 * than a fixed overlay, so it cannot land on the table at any size.
 *
 * Not in the in-game menu, despite that being the obvious "off the table"
 * home: #btn-menu tears the match down (main.js backToMenu clears G.balls),
 * so a record toggle reachable only from there would have no table left to
 * record by the time the player saw it. */
#fp-rec-slot { display: contents; }
.fp-rec {
  display: none; align-items: center; gap: 7px;
  flex: 0 0 auto;
  /* #hud is pointer-events:none so the felt stays draggable through the gaps
     between its cards; anything actually tappable in there opts back in. */
  pointer-events: auto;
  /* Deliberately the smallest thing in the HUD row. It is a secondary
     control sharing a band with the two player cards and the turn status,
     all of which a player needs to read mid-shot; the record toggle is
     something they reach for once a match at most. */
  padding: 4px 9px 4px 7px;
  border-radius: var(--fp-r-pill);
  background: rgba(6, 20, 14, .5);
  border: 1px solid rgba(255,255,255,.14);
  color: var(--fp-ink);
  font-family: var(--fp-font); font-size: 10px; font-weight: 700;
  letter-spacing: .2px; line-height: 1;
  cursor: pointer; user-select: none; -webkit-user-select: none;
  min-height: 24px;
}
.fp-rec.visible { display: inline-flex; }
.fp-rec:active { background: rgba(6, 20, 14, .82); }

.fp-rec-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--fp-bad);
  box-shadow: 0 0 0 2px rgba(255,107,92,.22);
  flex: 0 0 auto;
}
.fp-rec-label { font-variant-numeric: tabular-nums; }

.fp-rec.live { border-color: rgba(255,107,92,.65); background: rgba(60, 12, 10, .72); }
.fp-rec.live .fp-rec-dot { animation: fp-rec-blink 1.1s steps(1, end) infinite; }
@keyframes fp-rec-blink { 50% { opacity: .15; } }
@media (prefers-reduced-motion: reduce) {
  .fp-rec.live .fp-rec-dot { animation: none; }
}

/* ---------- Clips screen (ui/clipsScreen.js) ---------- */

.fp-clip-hint {
  display: flex; align-items: center; gap: var(--fp-s2);
  margin: -4px 0 var(--fp-s2);
  font-size: var(--fp-t-xs); color: var(--fp-ink-faint);
}

/* Two per row on a phone, more as the screen allows. auto-fill rather than a
   fixed count so the tiles keep their aspect on a tablet instead of stretching
   into billboards. */
.fp-clip-grid {
  display: grid; gap: var(--fp-s3);
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  margin-bottom: var(--fp-s3);
}
.fp-clip {
  background: var(--fp-bg-elev); border: 1px solid var(--fp-hairline);
  border-radius: var(--fp-r-card); overflow: hidden;
  display: flex; flex-direction: column;
}
.fp-clip-open {
  position: relative; display: block; width: 100%;
  padding: 0; border: 0; background: none; cursor: pointer;
}
.fp-clip-thumb {
  /* Every clip is a different shape; one tile shape keeps the grid a grid and
     the thumbnail is cover-cropped into it. */
  aspect-ratio: 16 / 10; width: 100%;
  background-color: #071410;
  background-size: cover; background-position: center;
}
.fp-clip-thumb.empty {
  display: grid; place-items: center; color: var(--fp-ink-faint);
}
.fp-clip-ratio, .fp-clip-dur {
  position: absolute; bottom: 6px;
  padding: 2px 6px; border-radius: var(--fp-r-pill);
  background: rgba(0,0,0,.6); color: #fff;
  font-size: 10px; font-weight: 800; letter-spacing: .4px;
}
.fp-clip-ratio { left: 6px; color: var(--fp-gold); }
.fp-clip-dur { right: 6px; font-variant-numeric: tabular-nums; }

.fp-clip-meta {
  display: flex; justify-content: space-between; gap: var(--fp-s2);
  padding: var(--fp-s2) var(--fp-s2) 0;
  font-size: 11px; color: var(--fp-ink-faint);
}
.fp-clip-actions {
  display: flex; gap: var(--fp-s2);
  padding: var(--fp-s2);
}
.fp-clip-actions .fp-minibtn { flex: 1 1 0; }

.fp-clip-empty {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: var(--fp-s5) var(--fp-s4);
  border: 1px dashed var(--fp-hairline); border-radius: var(--fp-r-card);
  color: var(--fp-ink-faint); text-align: center;
}
.fp-clip-empty-title { font-size: var(--fp-t-sm); font-weight: 700; color: var(--fp-ink-dim); }
.fp-clip-empty-sub { font-size: var(--fp-t-xs); max-width: 34ch; }

/* The preview player is the clip's real shape, capped so a 9:16 clip cannot
   push the buttons under the fold on a short landscape phone. */
.fp-clip-modal { width: min(420px, 100%); }
.fp-clip-video {
  display: block; width: 100%; max-height: 46vh;
  margin-bottom: var(--fp-s3);
  border-radius: var(--fp-r-input); background: #000;
  object-fit: contain;
}

/* ---------- table size slider (ui/profileScreen.js) ----------
 * The value sits beside the track rather than under it: this is a preview
 * control, so the number and the board it is changing want to be readable in
 * the same glance. */
.fp-scale-row {
  display: flex; align-items: center; gap: var(--fp-s3);
  margin: var(--fp-s2) 0 4px;
}
.fp-scale-slider {
  flex: 1 1 auto; min-width: 0;
  accent-color: var(--fp-accent);
}
.fp-scale-value {
  flex: 0 0 auto; min-width: 46px; text-align: right;
  font-size: var(--fp-t-sm); font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.fp-scale-actions {
  display: flex; gap: var(--fp-s2); margin-top: var(--fp-s2);
}
.fp-scale-actions .fp-minibtn {
  flex: 1 1 0;
  display: inline-flex; align-items: center; justify-content: center; gap: 5px;
  padding: 0 var(--fp-s2);
}

/* ---------- paywall columns (ui/storeScreen.js) ----------
 * Three offers, three columns, so the free option and the cheaper one-off are
 * visible beside Premium instead of below it. A single stack put everything
 * except Premium under the fold on a landscape phone - hiding the free
 * rewarded-ad option from exactly the players least willing to pay.
 *
 * Premium gets the wider column because it carries the benefit list and the
 * renewal disclosure; the other two are a headline and a button. */
.fp-paywall-cols {
  display: grid;
  grid-template-columns: 1.35fr 1fr 1fr;
  gap: var(--fp-s3);
  align-items: start;
  margin-bottom: var(--fp-s3);
}
.fp-paywall-col {
  display: flex; flex-direction: column; gap: var(--fp-s3);
  min-width: 0;
}
.fp-paywall-col > .fp-buycard { height: 100%; }

/* Two columns, then one. Three abreast below ~820px leaves each card too
   narrow to state its own terms, and the renewal disclosure is the part that
   must never be squeezed. Premium spans the full width once it is alone on
   its row, so it still leads. */
@media (max-width: 820px) {
  .fp-paywall-cols { grid-template-columns: 1fr 1fr; }
  .fp-paywall-col.lead { grid-column: 1 / -1; }
}
@media (max-width: 520px) {
  .fp-paywall-cols { grid-template-columns: 1fr; }
  .fp-paywall-col.lead { grid-column: auto; }
}

/* The trial badge. Positioned rather than inline so it reads as a flag on the
   card instead of another line of copy competing with the price. */
.fp-buycard.has-trial { position: relative; border-color: rgba(140,255,170,.45); }
.fp-trial-flag {
  position: absolute; top: 0; right: var(--fp-s3);
  transform: translateY(-50%);
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 10px; border-radius: var(--fp-r-pill);
  background: linear-gradient(135deg, #8cffaa, #2fae5e);
  color: #04140c; font-size: 10px; font-weight: 900; letter-spacing: .06em;
  box-shadow: 0 2px 8px rgba(0,0,0,.35);
  white-space: nowrap;
}
.fp-trial-flag .fp-icon { color: #04140c; }

/* Server notices reuse the challenge card's shape (net/notices.js). The
   existing action sizing targets .fp-btn; these use .fp-minibtn, so they need
   the same trim or they tower over the card they sit in. */
.fp-notice .fp-challenge-actions .fp-minibtn {
  min-height: 34px; padding: 0 14px; font-size: 13px;
}

/* Redeem: a header control, not an offer. Sized to sit inline beside the
   title without competing with it. */
.fp-redeem-btn {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 36px; padding: 0 var(--fp-s3);
  border-radius: var(--fp-r-pill);
  border: 1px solid var(--fp-hairline);
  background: rgba(0,0,0,.28); color: var(--fp-ink-dim);
  font: inherit; font-size: var(--fp-t-xs); font-weight: 700;
  cursor: pointer; white-space: nowrap;
  transition: background var(--fp-tap), color var(--fp-tap);
}
.fp-redeem-btn:active { background: rgba(255,255,255,.10); color: var(--fp-ink); }
@media (max-width: 520px) {
  /* The label is the first thing to go when the header runs out of room -
     the ticket glyph still says what it is. */
  .fp-redeem-btn span { display: none; }
  .fp-redeem-btn { padding: 0 10px; }
}

/* ---------- home: rotating Premium promo chip (ui/homeScreen.js) ----------
 * Sits where the PREMIUM badge sits for a subscriber, so the top bar keeps
 * one shape whether or not the player has paid. Gold-tinted to read as an
 * offer rather than as status, and it must never grow enough to squeeze the
 * stat chips beside it. */
.fp-promo-chip {
  flex: 0 1 auto; min-width: 0;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 10px; border-radius: var(--fp-r-pill);
  background: rgba(255,215,106,.12);
  border: 1px solid rgba(255,215,106,.32);
  color: var(--fp-gold);
  font: inherit; font-size: var(--fp-t-xs); font-weight: 800;
  letter-spacing: .02em; cursor: pointer;
  transition: background var(--fp-tap);
}
.fp-promo-chip:active { background: rgba(255,215,106,.22); }
.fp-promo-text {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A phrase swap should read as the chip changing its mind, not as the layout
   jumping - so only the text fades, and the chip keeps its place. */
@keyframes fp-promo-in { from { opacity: 0; } to { opacity: 1; } }
.fp-promo-text { animation: fp-promo-in 320ms ease-out; }
@media (prefers-reduced-motion: reduce) {
  .fp-promo-text { animation: none; }
}
/* Short viewports are the NORMAL case here - a landscape phone is about
   326px of CSS height - so the text must survive them. Hiding it left a gold
   dot that said nothing, which is worse than no chip at all: the phrase IS
   the feature. It shrinks and ellipsises instead, and only gives up the label
   when the bar genuinely cannot hold it. */
@media (max-height: 400px) {
  .fp-promo-chip { padding: 4px 8px; font-size: 10px; }
  .fp-promo-text { max-width: 22ch; }
}
@media (max-width: 420px) {
  .fp-promo-text { display: none; }
  .fp-promo-chip { padding: 4px 8px; }
}

/* Friend-request card (ui/friendRequestDialog.js). Same shape as the
   challenge card - an offer that can be answered or ignored - so it uses the
   same box and only needs its buttons trimmed to match. */
.fp-friendreq .fp-challenge-actions .fp-minibtn {
  min-height: 34px; padding: 0 14px; font-size: 13px;
  display: inline-flex; align-items: center; gap: 5px;
}

/* First-run sign-in (ui/setupModal.js). Below "Start playing" and visually
   quieter than it: an account is worth offering at the one moment nothing has
   been earned yet, but it must never look like a requirement to play. */
.fp-setup-signin { margin-top: var(--fp-s3); text-align: center; }
.fp-setup-signin-or {
  font-size: var(--fp-t-xs); color: var(--fp-ink-faint);
  margin-bottom: 6px;
}
.fp-btn.fp-setup-google {
  width: 100%;
  background: rgba(255,255,255,.06);
  border: 1px solid var(--fp-hairline);
  color: var(--fp-ink);
}
.fp-btn.fp-setup-google:active { background: rgba(255,255,255,.12); }
.fp-setup-signin .fp-formerr { margin-top: 6px; }

/* The setup modal on a landscape phone (~384px tall).
 *
 * It is the tallest modal in the app - name, ten avatars, a Premium line, the
 * start button and now a sign-in block - and the viewport it most often opens
 * on is the shortest. Scrolling is available (see .fp-modal-wrap) but a first
 * screen that needs scrolling to reach its own primary button is a bad first
 * screen, so the parts that can shrink, do. */
@media (max-height: 500px) {
  .fp-setup-head { margin-bottom: 6px; }
  .fp-setup-head .fp-modal-body { display: none; }
  .fp-modal { padding: var(--fp-s3); }
  .fp-setup-avatar { width: 44px; height: 44px; }
  .fp-setup-avatars { gap: 6px; }
  .fp-setup-name { min-height: 38px; }
  .fp-setup-promo { padding: 6px 10px; }
  .fp-btn.fp-setup-start { padding: 10px; }
  .fp-setup-signin { margin-top: 8px; }
  .fp-setup-signin-or { margin-bottom: 4px; }
  .fp-btn.fp-setup-google { padding: 8px; }
}
