/* ==========================================================================
   PUB New Water — parent shell + leaderboard overlay styles
   ========================================================================== */

@font-face {
  font-family: "Nunito";
  src: url("assets/Nunito font/static/Nunito-ExtraBold.ttf") format("truetype");
  font-weight: 800;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("assets/Nunito font/static/Nunito-SemiBold.ttf") format("truetype");
  font-weight: 600;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("assets/Nunito font/static/Nunito-Bold.ttf") format("truetype");
  font-weight: 700;
  font-display: swap;
}
@font-face {
  font-family: "Nunito";
  src: url("assets/Nunito font/static/Nunito-Black.ttf") format("truetype");
  font-weight: 900;
  font-display: swap;
}

* { box-sizing: border-box; }

/* ==========================================================================
   Score screen + Input screen shared / specific styles
   ========================================================================== */

/* --- Score screen panel (score-name-input-background-container.svg used as
       panel bg; next-btn.png is the CTA button image) ---------------------- */

/* The panel SVG is 1214/937 — same container as the leaderboard, so sc-stage
   reuses the same centering logic as lb-stage. */
.sc-stage {
  position: relative;
  width: min(
    80vw,
    860px,
    calc(var(--lb-frame-w) * 0.60),
    calc(var(--lb-frame-h) * 0.86 * 1.159),
    calc((var(--lb-vh) - clamp(46px, 8vmin, 130px)) * 1.159)
  );
  margin-right: calc(
    (var(--lb-inset-right) - var(--lb-inset-left)) / 2
  );
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Score panel — uses score-name-input-background-container.svg as background */
.sc-panel {
  position: relative;
  width: 100%;
  aspect-ratio: 1085 / 936;
  container-type: inline-size;
  container-name: sc-panel;
}
.sc-panel-bg {
  position: absolute;
  inset: 0;
  background: url("assets/score-name-input-background-container.svg") center / 100% 100% no-repeat;
}
.sc-panel-inner {
  position: absolute;
  inset: 35% 14% 14% 12.5%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3cqw;
  overflow: visible;
}

/* "Your final score" SVG asset replaces the text heading */
.sc-title-img {
  width: clamp(120px, 69cqw, 620px);
  display: block;
  margin: 0 auto;
  /* Ensure smooth sub-pixel rendering when SVG is scaled */
  image-rendering: -webkit-optimize-contrast; /* Safari legacy */
  image-rendering: smooth;                     /* Modern browsers */
}

/* Score number truly centered — symmetric 1fr|auto|1fr grid.
   Score sits in col 2 (center), Points in col 3 (right 1fr cell). */
.sc-score-wrap {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: end;
  width: 100%;
  overflow: visible;
}

/* Stack container — grid so all 3 spans overlap perfectly */
.sc-score-stack {
  display: grid;
  overflow: visible;
  grid-column: 2;
  justify-self: center;
}
.sc-score-stack > * {
  grid-area: 1 / 1;
  text-align: center;
  align-self: center;
  justify-self: center;
}

/* Shared font for all 3 layers */
.sc-score-shadow,
.sc-score-border,
.sc-score-num {
  font-family: "Nunito", system-ui, sans-serif;
  font-style: normal;
  font-weight: 800;
  line-height: 1;
  white-space: nowrap;
  user-select: none;
  font-size: clamp(40px, 22cqw, 200px);
}

/* Layer 3 (bottom) — shadow: same color as shadow, shifted down */
.sc-score-shadow {
  color: #003062;
  transform: translateY(15px);
  z-index: 1;
  font-weight: 900;
}

/* Layer 2 (middle) — border: #00AEF6, heavier font-weight so strokes are
   thicker and peek out around the white layer, creating a clean border */
.sc-score-border {
  font-weight: 900;
  color: #00AEF6;
  z-index: 2;
  -webkit-text-stroke: 13px #00AEF6;
  -webkit-border-radius: 15px;
  inner-border-radius: 10px;
}

/* Layer 1 (top) — white face, base font size sits on top of border */
.sc-score-num {
  
  color: #ffffff;
  z-index: 3;
}

/* "Points" label — 2-layer stacked gradient + outside stroke */
.sc-point-label {
  display: grid;
  grid-column: 3;          /* right column of sc-score-wrap grid */
  align-self: end;         /* sit at the bottom, aligned with base of score */
  justify-self: start;     /* left-align within col 3 */
  margin-bottom: 0.4em;    /* fine-tune vertical alignment with score baseline */
  line-height: 1;
}
.sc-point-label > span {
  grid-area: 1 / 1;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 800;
  font-size: clamp(12px, 4.5cqw, 56px);
  line-height: 1;
  white-space: nowrap;
  user-select: none;
  margin-left: 10px;
}
/* Layer 1 – solid fill + dark stroke so only outer ring is visible */
.sc-point-stroke {
  color: #81D3EF;
  -webkit-text-stroke: 6px #003062;
  paint-order: stroke fill;
}
/* Layer 2 – gradient fill covers the body, leaving only the outer stroke ring */
.sc-point-top {
  background: linear-gradient(180deg, #CCF2FF 0%, #40CFFF 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}

/* Next button image */
.sc-next-btn {
  position: absolute;
  bottom: -4%;
  left: 50%;
  transform: translateX(-50%);
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  width: clamp(90px, 20cqw, 260px);
  transition: transform 0.1s ease;
  z-index: 5;
}
.sc-next-btn:active { transform: translateX(-50%) scale(0.93); }
.sc-next-btn img { width: 100%; display: block; }

/* --- Confirm screen panel (Top 10: new vs existing) ----------------------- */
.cf-panel {
  position: relative;
  width: 100%;
  aspect-ratio: 1118 / 658;
  container-type: inline-size;
  container-name: cf-panel;
}
.cf-panel-bg {
  position: absolute;
  inset: 0;
  background: url("assets/confirm-container.png") center / 100% 100% no-repeat;
}
.cf-panel-inner {
  position: absolute;
  /* Push the heading lower so it clears the mascot overhang and sits inside
     the lower half of the container. */
  inset: 52% 10% 10% 10%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: 5cqw;
}
.cf-heading {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(11px, 4cqw, 48px);
  color: #13204F;
  text-align: center;
  line-height: 1.25;
}
/* Choice buttons sit below and outside the container, overhanging the bottom
   edge (like the Submit button on the name screen). */
.cf-btn-row {
  position: absolute;
  bottom: -14%;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5cqw;
  width: 90%;
  z-index: 5;
}
.cf-choice-btn {
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  width: clamp(110px, 34cqw, 420px);
  transition: transform 0.1s ease;
}
.cf-choice-btn:active { transform: scale(0.93); }
.cf-choice-btn img { width: 100%; display: block; }

/* --- Input screen panel --------------------------------------------------- */
.inp-panel {
  position: relative;
  width: 100%;
  aspect-ratio: 1085 / 936;
  container-type: inline-size;
  container-name: inp-panel;
}
.inp-panel-bg {
  position: absolute;
  inset: 0;
  background: url("assets/score-name-input-background-container.svg") center / 100% 100% no-repeat;
}
.inp-panel-inner {
  position: absolute;
  /* Inner light-blue box measured from SVG:
     top y=302.9/936=32.4%, bottom y=874.8/936 → gap=6.5%, left/right x≈8.4%/7.9% */
  inset: 23% 8% 7% 8%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 5cqw 4cqw 1cqw;
  gap: 4cqw;
}

/* "Congratulations!" title image above the heading copy */
.inp-congrats-img {
  display: block;
  width: clamp(120px, 42cqw, 520px);
  height: auto;
  margin: 0 auto;
}

.inp-heading {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(10px, 3.5cqw, 44px);
  color: #13204F;
  text-align: center;
  line-height: 1.2;
}

/* Wrapper provides the outer white pill (fill #FFF 85%, stroke #003062 4px) */
.inp-field-wrap {
  position: relative;
  width: 100%;
  background: rgba(255, 255, 255, 0.85);
  border: 3px solid #003062;
  border-radius: 999px;
  padding: 4px; /* gap between outer stroke and inner field */
  box-sizing: border-box;
}

.inp-field {
  width: 100%;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(12px, 3cqw, 38px);
  color: #13204F;
  /* Inner field: fill #CBF0FA 70%, stroke #AEDCE7 6px center */
  background: rgba(203, 240, 250, 0.70);
  border: 6px solid #AEDCE7;
  border-radius: 999px;
  padding: 0.45em 1.1em;
  outline: none;
  box-sizing: border-box;
  display: block;
  transition: border-color 0.15s;
}
.inp-field::placeholder { color: rgba(19, 32, 79, 0.4); }
.inp-field:focus,
.inp-field:focus-visible { outline: none; box-shadow: none; }
.inp-field-wrap:focus-within { outline: none; }

/* --- Keyboard-docked name input (mobile) ---------------------------------
   A lightweight bar pinned to the top of the on-screen keyboard. The game panel
   stays untouched; the user types here and the value is copied back + submitted.
   Its `bottom` offset is set from JS (keyboard-fit.js) to sit on the keyboard. */
.kbd-dock {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0; /* overridden by JS to the keyboard height */
  z-index: 2147483647; /* above the overlay and the game iframe */
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  background: #ffffff;
  border-top: 1px solid rgba(0, 48, 98, 0.18);
  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.12);
  box-sizing: border-box;
}
.kbd-dock[hidden] { display: none; }
/* Now a contenteditable <div> (not an <input>) to avoid iOS's AutoFill Contact
   bar. Styled to look and behave like a single-line text field. */
.kbd-dock-input {
  flex: 1 1 auto;
  min-width: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: 17px; /* >=16px avoids iOS auto-zoom on focus */
  color: #13204F;
  background: #f3fbfe;
  border: 2px solid #AEDCE7;
  border-radius: 10px;
  padding: 10px 12px;
  outline: none;
  box-sizing: border-box;
  /* Single-line behaviour for the editable div. */
  white-space: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  line-height: 1.3;
}
/* Placeholder: shown via data-placeholder while the div is empty. */
.kbd-dock-input:empty::before {
  content: attr(data-placeholder);
  color: rgba(19, 32, 79, 0.4);
  pointer-events: none;
}
.kbd-dock-done {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  line-height: 1;
  color: #ffffff;
  background: #0a84ff;
  border: none;
  border-radius: 10px;
  cursor: pointer;
}
.kbd-dock-done:active { transform: scale(0.94); }

/* Error state — shown when Submit is pressed with an empty name. The outer pill
   turns red and the inner field fills light pink to signal the problem. */
.inp-field-wrap.is-error {
  background: rgba(255, 234, 234, 0.95);
  border-color: #E53935;
}
.inp-field-wrap.is-error .inp-field {
  background: rgba(255, 224, 224, 0.9);
  border-color: #f3c5c5;
  color: #E53935;
}
.inp-field-wrap.is-error .inp-field::placeholder { color: rgba(229, 57, 53, 0.5); }
/* Warning badge on the right edge of the field when in error. */
.inp-field-wrap.is-error::after {
  content: "!";
  position: absolute;
  top: 50%;
  right: 3%;
  transform: translateY(-50%);
  width: clamp(16px, 3.4cqw, 42px);
  height: clamp(16px, 3.4cqw, 42px);
  display: flex;
  align-items: center;
  justify-content: center;
  background: #E53935;
  color: #fff;
  border-radius: 50%;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 800;
  font-size: clamp(11px, 2.4cqw, 30px);
  line-height: 1;
  pointer-events: none;
}

/* Back button (top-left of the input panel, used in confirm sub-flows) */
.inp-back-btn {
  position: absolute;
  top: 38%;
  left: 13%;
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  width: clamp(36px, 8cqw, 90px);
  z-index: 6;
  transition: transform 0.1s ease;
}
.inp-back-btn:active { transform: scale(0.9); }
.inp-back-btn[hidden] { display: none; }
.inp-back-btn img { width: 100%; display: block; }

/* Suggestions dropdown shown while typing an existing name */
.inp-suggestions {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: #FFFFFF;
  border: 3px solid #003062;
  border-radius: 18px;
  max-height: 40cqw;
  overflow-y: auto;
  z-index: 20;
  box-shadow: 0 6px 16px rgba(0, 48, 98, 0.25);
}
.inp-suggestions[hidden] { display: none; }
.inp-suggestions li {
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(11px, 2.6cqw, 34px);
  color: #13204F;
  padding: 0.4em 0.9em;
  border-radius: 12px;
  cursor: pointer;
}
.inp-suggestions li:hover,
.inp-suggestions li.is-active {
  background: rgba(203, 240, 250, 0.9);
}

/* ---- Score / Input screen — phone landscape (very short viewport) --------- */
/* Mirror the leaderboard landscape rule: tighten insets, padding and gaps so
   the content fits inside the ~170 px of available inner height. */
@media (orientation: landscape) and (max-height: 500px) {
  /* Make the stage a bit wider relative to the frame so text doesn't shrink
     further than needed — the height constraint is the real limit here. */
  .sc-stage {
    /* Panel is 1.159:1 (w/h). On very short landscape (e.g. iPhone 16, 393px
       tall) the panel height was reaching ~96% of the inner frame, leaving no
       room, so the Next button — which hangs BELOW the panel — overflowed the
       bottom border. Cap the panel height to ~0.66 of the frame height so the
       button's overhang stays inside the frame. */
    width: min(
      90vw,
      calc(var(--lb-frame-w) * 0.72),
      calc(var(--lb-frame-h) * 0.78 * 1.159)
    );
  }

  /* Score screen: tighter vertical gap between title image and score number */
  .sc-panel-inner {
    gap: 1.8cqw;
  }

  /* Keep the Next button's overhang small and its footprint compact so it never
     crosses the bottom frame line on short landscape screens. */
  .sc-next-btn {
    bottom: -6%;
    width: clamp(80px, 16cqw, 180px);
  }

  /* The input overlay reuses .sc-stage but its Submit button hugs the panel
     bottom (no tall overhang like the score's Next button), so it can use more
     of the frame height. Give it a bigger cap so the panel fills the space
     instead of floating small with wide empty margins. */
  .input-overlay .sc-stage {
    width: min(
      88vw,
      calc(var(--lb-frame-w) * 0.72),
      calc(var(--lb-frame-h) * 0.86 * 1.159)
    );
  }
  /* Smaller Submit button so it occupies less of the panel bottom and leaves
     room for the error message above it. */
  .inp-submit-btn {
    bottom: 1%;
    width: clamp(72px, 14cqw, 160px);
  }

  /* Input screen: landscape — compact. The bottom inset is raised to ~16% so the
     inner content box ENDS above the Submit button, and the error message (the
     last item in the content) no longer overlaps the button. */
  .inp-panel-inner {
    inset: 30% 7% 16% 7%;
    padding: 2cqw 4cqw 0;
    gap: 1.4cqw;
    justify-content: center;
  }

  /* Heading: allow a tighter minimum so it doesn't force layout overflow */
  .inp-heading {
    font-size: clamp(9px, 2.6cqw, 44px);
    line-height: 1.1;
    margin: 0;
  }

  /* Field: slimmer pill + smaller text so the whole stack fits. */
  .inp-field {
    padding: 0.15em 0.9em;
    font-size: clamp(9px, 2.3cqw, 38px);
  }
  .inp-field-wrap { padding: 2px; }

  /* Error message: shrink and keep it compact so it stays inside the panel. */
  .inp-error {
    font-size: clamp(8px, 1.9cqw, 26px);
    line-height: 1.05;
  }
}

/* ---- Score / Input screen — tablet portrait (≤ 820 px wide) --------------- */
/* No layout changes needed — cqw-based sizing already scales the content.
   Just ensure the submit / next buttons never clip the viewport bottom. */
@media (max-width: 820px) and (orientation: portrait) {
  .sc-next-btn,
  .inp-submit-btn {
    /* Pull the button up a hair so it clears the safe-area / browser chrome */
    bottom: -1%;
  }
}

.inp-error {
  margin: 0;
  color: #E53935;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(10px, 2.4cqw, 30px);

}
.inp-error[hidden] { display: none; }

/* Submit button */
.inp-submit-btn {
  position: absolute;
  bottom: -2%;
  left: 50%;
  transform: translateX(-50%);
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  width: clamp(100px, 22cqw, 280px);
  transition: transform 0.1s ease;
  z-index: 5;
}
.inp-submit-btn:active { transform: translateX(-50%) scale(0.93); }
.inp-submit-btn img { width: 100%; display: block; }

/* Visually-disabled state while the name field is empty. The button is dimmed
   but stays clickable (no `disabled` attribute / pointer-events) so tapping it
   surfaces the "please enter your name" error. */
.inp-submit-btn.is-disabled img { opacity: 0.5; }

/* Loading spinner shown inside the submit button while a score submission is
   in flight (Firestore round-trip). Hidden by default; the `.is-submitting`
   state on the button swaps the Submit image out for a spinning ring so the
   player gets clear "loading…" feedback and knows not to tap again. The button
   is also `disabled` from JS to block repeat submissions. */
.inp-submit-spinner {
  display: none;
  position: absolute;
  top: 50%;
  left: 50%;
  width: 28%;
  aspect-ratio: 1 / 1;
  margin: 0;
  transform: translate(-50%, -50%);
  border: 0.35em solid rgba(0, 0, 0, 0.2);
  border-top-color: #000;
  border-radius: 50%;
  box-sizing: border-box;
  animation: inp-submit-spin 0.7s linear infinite;
}
@keyframes inp-submit-spin {
  to { transform: translate(-50%, -50%) rotate(360deg); }
}

/* While submitting: hide the Submit image, reveal the spinner, and block taps.
   The button is also set to `disabled` from JS (belt-and-braces). */
.inp-submit-btn.is-submitting {
  cursor: progress;
  pointer-events: none;
}
.inp-submit-btn.is-submitting .inp-submit-img { visibility: hidden; }
.inp-submit-btn.is-submitting .inp-submit-spinner { display: block; }
/* Suppress the active-press scale while loading so the button looks stable. */
.inp-submit-btn.is-submitting:active { transform: translateX(-50%); }
.inp-submit-btn:disabled { cursor: progress; }

/* Build version badge — pinned to the bottom-right corner of the viewport. */
.app-version {
  position: fixed;
  right: 8px;
  bottom: 6px;
  z-index: 9999;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: 11px;
  line-height: 1;
  color: #000;
  pointer-events: none;
  user-select: none;
}

/* ==========================================================================
   Connection strength indicator — small signal bars pinned to the top-right.
   Bars fill according to the measured connection quality (0–4). Turns red
   when offline. Updated by js/app.js.
   ========================================================================== */
.conn-indicator {
  position: fixed;
  top: 8px;
  right: 8px;
  z-index: 9999;
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 16px;
  padding: 3px;
  border-radius: 4px;
  background: rgba(0, 20, 45, 0.6);
  pointer-events: none;
  user-select: none;
}
.conn-indicator[hidden] { display: none; }

.conn-bar {
  width: 3px;
  border-radius: 1px;
  background: rgba(255, 255, 255, 0.3); /* inactive bar */
  transition: background-color 0.2s ease;
}
.conn-bar-1 { height: 25%; }
.conn-bar-2 { height: 50%; }
.conn-bar-3 { height: 75%; }
.conn-bar-4 { height: 100%; }

/* Active bars for a given strength level (1–4). */
.conn-indicator[data-level="1"] .conn-bar-1,
.conn-indicator[data-level="2"] .conn-bar-1,
.conn-indicator[data-level="2"] .conn-bar-2,
.conn-indicator[data-level="3"] .conn-bar-1,
.conn-indicator[data-level="3"] .conn-bar-2,
.conn-indicator[data-level="3"] .conn-bar-3,
.conn-indicator[data-level="4"] .conn-bar-1,
.conn-indicator[data-level="4"] .conn-bar-2,
.conn-indicator[data-level="4"] .conn-bar-3,
.conn-indicator[data-level="4"] .conn-bar-4 {
  background: #00e676; /* good — bright green */
  box-shadow: 0 0 4px rgba(0, 230, 118, 0.9);
}

/* Weak connection (level 1) tints the single active bar amber. */
.conn-indicator[data-level="1"] .conn-bar-1 { background: #ffb300; }

/* Offline: all bars red. */
.conn-indicator[data-level="0"] .conn-bar {
  background: #e53935;
}

/* ==========================================================================
   Portrait-lock overlay
   Shown whenever window.matchMedia("(orientation: portrait)") is true on a
   touch device. Hides the game iframe and all overlays behind it.
   ========================================================================== */
.rotate-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: #e3faff;
  display: flex;
  align-items: center;
  justify-content: center;
}
.rotate-overlay[hidden] { display: none; }

.rotate-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  background: rgba(255,255,255,0.85);
  border: 4px solid #003062;
  border-radius: 28px;
  padding: 40px 48px;
  max-width: min(88vw, 380px);
  box-shadow: 0 8px 32px rgba(0,48,98,0.18);
}

.rotate-icon {
  width: clamp(120px, 50vw, 200px);
  animation: rotate-hint 1.8s ease-in-out infinite;
}

@keyframes rotate-hint {
  0%   { transform: scale(1);    opacity: 1; }
  50%  { transform: scale(1.06); opacity: 0.85; }
  100% { transform: scale(1);    opacity: 1; }
}

.rotate-title {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 900;
  font-size: clamp(18px, 5vw, 28px);
  color: #003062;
  text-align: center;
}

.rotate-sub {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(13px, 3.5vw, 18px);
  color: #13204F;
  text-align: center;
  opacity: 0.75;
}

/* ==========================================================================
   Add-to-Home-Screen popup
   Shown ONCE before the game starts on iOS devices (iPhone / iPad) that are not
   already installed as a PWA and not launched in kiosk mode. Uses the same
   colour scheme as the portrait-lock overlay for visual consistency.
   ========================================================================== */
.a2hs-overlay {
  position: fixed;
  inset: 0;
  z-index: 9997; /* below rotate/offline overlays, above the game iframe */
  /* Translucent scrim so the game keeps showing through in the background. */
  background: rgba(0, 48, 98, 0.45);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  box-sizing: border-box;
}
.a2hs-overlay[hidden] { display: none; }

.a2hs-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  /* Blue card per mockup: #0057AA at 80% opacity with a 4px centered #0057AA stroke. */
  background: rgba(0, 87, 170, 0.8);
  border: 4px solid #0057AA;
  border-radius: 28px;
  padding: 32px 40px;
  max-width: min(92vw, 520px);
  max-height: 92vh;
  overflow-y: auto;
  box-shadow: 0 8px 32px rgba(0, 48, 98, 0.25);
  text-align: center;
}

.a2hs-title {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 900;
  font-size: clamp(20px, 5vw, 28px);
  color: #ffffff;
}

.a2hs-sub {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(13px, 3.5vw, 17px);
  color: #ffffff;
  line-height: 1.4;
}

.a2hs-steps {
  list-style: none;
  margin: 8px 0 4px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 100%;
  text-align: left;
}

.a2hs-step {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.a2hs-step-num {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: #0057AA;
  border: 2px solid #ffffff;
  box-sizing: border-box;
  color: #ffffff;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 900;
  font-size: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.a2hs-step-text {
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(13px, 3.4vw, 16px);
  color: #ffffff;
  line-height: 1.45;
}

.a2hs-step-text strong { color: #ffffff; font-weight: 900; }

.a2hs-inline-icon {
  width: 1.15em;
  height: 1.15em;
  vertical-align: -0.25em;
  margin: 0 2px;
}

.a2hs-continue-btn {
  margin-top: 8px;
  border: none;
  background: transparent;
  padding: 0;
  cursor: pointer;
  line-height: 0;
  transition: transform 0.12s ease;
}
.a2hs-continue-btn img {
  display: block;
  width: clamp(180px, 45vw, 240px);
  height: auto;
}
.a2hs-continue-btn:active {
  transform: scale(0.96);
}

/* ==========================================================================
   Sound-prompt popup
   Shown briefly on the landing screen to remind the player to raise their
   device volume. Auto-hidden after 2s by js/app.js. Uses the same blue card
   styling as the add-to-home-screen popup for visual consistency.
   ========================================================================== */
.sound-overlay {
  position: fixed;
  inset: 0;
  z-index: 9996; /* just below the a2hs popup */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  box-sizing: border-box;
  pointer-events: none; /* purely informational — never blocks the game */
}
.sound-overlay[hidden] { display: none; }

.sound-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  background: rgba(0, 87, 170, 0.8);
  border: 4px solid #0057AA;
  border-radius: 28px;
  padding: 24px 40px;
  max-width: min(90vw, 440px);
  box-shadow: 0 8px 32px rgba(0, 48, 98, 0.25);
  text-align: center;
}

.sound-icon {
  width: clamp(30px, 7vw, 40px);
  height: clamp(30px, 7vw, 40px);
}

.sound-text {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 900;
  font-size: clamp(16px, 4vw, 24px);
  color: #ffffff;
  line-height: 1.3;
}

/* iPhone-only "turn off the silent switch" reminder inside the sound card. */
.sound-subnote {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(13px, 3.2vw, 18px);
  color: #ffffff;
  opacity: 0.9;
  line-height: 1.3;
}
.sound-subnote[hidden] { display: none; }
.sound-subnote strong { font-weight: 900; }

/* ==========================================================================
   Responsive refinements for the popups
   The game runs in landscape, so on phones the viewport is very SHORT (often
   320–420px tall). Tighten padding, shrink the icon, and reduce step gaps so
   the add-to-home-screen card never overflows / needs scrolling, and the
   sound card stays compact. Also handle narrow widths.
   ========================================================================== */

/* Short viewports (landscape phones & small tablets) */
@media (max-height: 480px) {
  .a2hs-card {
    gap: 10px;
    padding: 18px 28px;
    border-radius: 22px;
    max-height: 94vh;
  }
  .a2hs-title { font-size: clamp(17px, 4.5vw, 22px); }
  .a2hs-sub   { font-size: clamp(12px, 3vw, 15px); }
  .a2hs-steps { gap: 8px; margin: 4px 0 2px; }
  .a2hs-step-num { width: 24px; height: 24px; font-size: 12px; }
  .a2hs-step-text { font-size: clamp(12px, 3vw, 15px); line-height: 1.35; }
  .a2hs-continue-btn { margin-top: 4px; }
  .a2hs-continue-btn img { width: clamp(150px, 34vw, 200px); }

  .sound-card {
    gap: 6px;
    padding: 16px 28px;
    border-radius: 22px;
  }
  .sound-icon { width: clamp(26px, 6vw, 34px); height: clamp(26px, 6vw, 34px); }
  .sound-text { font-size: clamp(14px, 3.4vw, 20px); }
  .sound-subnote { font-size: clamp(12px, 3vw, 16px); }
}

/* Narrow viewports (small phones) — let text wrap naturally instead of the
   forced <br>, and give the cards a little more breathing room on the sides. */
@media (max-width: 420px) {
  .a2hs-card,
  .sound-card {
    max-width: 94vw;
    padding-left: 24px;
    padding-right: 24px;
  }
  .sound-text br { display: none; }
}

/* ==========================================================================
   No-connection overlay
   Shown whenever the browser fires the "offline" event DURING active gameplay.
   Sits above everything (z-index 10000) — even above the rotate-overlay — so
   the player cannot interact with a frozen game while disconnected.
   ========================================================================== */
.offline-overlay {
  position: fixed;
  inset: 0;
  z-index: 10000;
  background: #e3faff;
  display: flex;
  align-items: center;
  justify-content: center;
}
.offline-overlay[hidden] { display: none; }

.offline-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  background: rgba(255, 255, 255, 0.88);
  border: 4px solid #003062;
  border-radius: 28px;
  padding: 40px 48px;
  max-width: min(88vw, 400px);
  box-shadow: 0 8px 32px rgba(0, 48, 98, 0.18);
}

.offline-icon {
  width: clamp(80px, 28vw, 140px);
  animation: offline-pulse 2s ease-in-out infinite;
}

@keyframes offline-pulse {
  0%   { opacity: 1;    transform: scale(1); }
  50%  { opacity: 0.6;  transform: scale(0.94); }
  100% { opacity: 1;    transform: scale(1); }
}

.offline-title {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 900;
  font-size: clamp(18px, 5vw, 28px);
  color: #003062;
  text-align: center;
}

.offline-sub {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(13px, 3.5vw, 18px);
  color: #13204F;
  text-align: center;
  opacity: 0.75;
  line-height: 1.5;
}


:root {
  /* Uniform breathing room above and below the border frame. Applied to BOTH
     the top and the bottom, so the frame is vertically centred inside the
     viewport. Shrinks on small screens so it doesn't eat the limited landscape
     height. */
  --lb-frame-gap: clamp(10px, 2.5vmin, 30px);

  /* Horizontal breathing room at the LEFT/RIGHT viewport edges. Zero by default
     so the border frame bleeds edge-to-edge and the centre tiles across the
     full width; bump this if you want the scene inset from the sides. */
  --lb-side-gap: 0px;

  /* The border frame is now built from three separate pieces so it can stretch
     responsively: fixed-width left/right caps and a horizontally TILED centre
     that fills whatever width is left. Aspect ratios (width / height) are baked
     off the artwork, all authored at 1200px tall. */
  --lb-border-left-ar: 0.13583;  /* border-left.png    163/1200 */
  --lb-border-right-ar: 0.2075;  /* border-right.png   249/1200 */
  /* water-tank-right.svg is 54x1141, authored to sit INSIDE the 1200px-tall cap,
     so its rendered height is frame-h * 1141/1200 (not the full frame height) and
     its width is derived from that. */
  --lb-tank-hf: 0.95083;         /* 1141/1200 — tank height as fraction of frame */
  --lb-tank-ar: 0.045;           /* rendered width / frame-h = 54/1200 */

  /* Default (top-10) panel. Overridden on .lb-overlay--long — see below — where
     the taller "out of top 10" artwork is used instead. */
  --lb-panel-ar: 1.2956;  /* leaderboard-container-full-accessories.svg 1214/937 */

  /* Viewport height that accounts for mobile browser UI chrome. `dvh` is
     overridden below where supported; `vh` is the fallback. */
  --lb-vh: 100vh;

  /* Rendered height of the border frame. The frame now spans the full viewport
     WIDTH (the centre piece tiles), so height is constrained by the vertical
     axis alone — the leftover space top/bottom shows the background image. */
  --lb-frame-h: calc(var(--lb-vh) - var(--lb-frame-gap) * 2);

  /* Widths of the fixed scene pieces, derived from the frame height. */
  --lb-border-left-w: calc(var(--lb-frame-h) * var(--lb-border-left-ar));
  --lb-border-right-w: calc(var(--lb-frame-h) * var(--lb-border-right-ar));
  --lb-tank-w: calc(var(--lb-frame-h) * var(--lb-tank-ar));
  --lb-tank-h: calc(var(--lb-frame-h) * var(--lb-tank-hf));

  /* Position of each cap's actual FRAME LINE, measured from the artwork as a
     fraction of that cap's width. The usable interior starts at these lines, NOT
     at the full cap width — the right cap in particular has a wide empty band
     (the gauge) beyond its line, so treating the whole cap as non-usable pushed
     the panel too far left and let it overflow the frame. */
  --lb-border-left-line: 0.184;   /* left cap:  line at x~30 / 163  */
  --lb-border-right-line: 0.526;  /* right cap: line at x~131 / 249 */

  /* Width each cap consumes of the interior = from viewport edge to its line. */
  --lb-inset-left: calc(var(--lb-border-left-w) * var(--lb-border-left-line));
  --lb-inset-right: calc(var(--lb-border-right-w) * var(--lb-border-right-line));

  /* The scene spans the whole viewport width (minus optional side gaps). */
  --lb-scene-x: var(--lb-side-gap);
  --lb-scene-w: calc(100vw - var(--lb-side-gap) * 2);

  /* Usable content width = the inner area BETWEEN the two frame LINES. Downstream
     panel/stage rules size themselves off this token. */
  --lb-frame-w: calc(
    var(--lb-scene-w) - var(--lb-inset-left) - var(--lb-inset-right)
  );

  /* Close button width, and the vertical room it needs. close-btn.png is
     223x115, and the button straddles the panel edge so only half of it hangs
     below — but reserve its full height so it never clips the viewport. */
  --lb-close-w: clamp(96px, 22vmin, 240px);
  --lb-close-h: calc(var(--lb-close-w) * 115 / 223);

  /* Scrollbar width — scales with the viewport so the 20px desktop bar doesn't
     eat a disproportionate slice of the narrow phone-landscape panel. */
  --lb-sb-w: clamp(8px, 2vmin, 20px);
  /* Thumb inset (transparent border) — must stay under half the bar width. */
  --lb-sb-inset: clamp(2px, 0.4vmin, 3px);
}

@supports (height: 100dvh) {
  :root { --lb-vh: 100dvh; }
}

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #e3faff;
  font-family: "Nunito", system-ui, sans-serif;
}

/* Full-screen iframe, no spacing whatsoever.
   height uses the dynamic viewport height (dvh) so that when the page is NOT in
   fullscreen the iframe matches the browser's *visible* viewport — i.e. the
   window height minus any browser chrome (address/tab bar). `100vh` is kept as a
   fallback for browsers that don't support dvh. */
#game {
  position: fixed;
  inset: 0;
  width: 100vw;
  width: 100dvw;
  height: 100vh; /* fallback */
  height: 100dvh;
  border: 0;
  margin: 0;
  padding: 0;
  display: block;
}

/* --- offline banner ------------------------------------------------------ */
.offline-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
  background: #ff9500;
  color: #fff;
  font-weight: 800;
  text-align: center;
  padding: 10px 16px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
}

/* Retry button inside the offline banner — re-checks the connection and, if
   back online, re-submits the pending score and shows the leaderboard. */
.offline-retry-btn {
  flex: 0 0 auto;
  border: 2px solid #fff;
  background: #fff;
  color: #ff9500;
  font-weight: 800;
  font-size: 0.9em;
  padding: 4px 16px;
  border-radius: 999px;
  cursor: pointer;
  transition: opacity 0.15s ease, transform 0.1s ease;
}
.offline-retry-btn:hover { opacity: 0.88; }
.offline-retry-btn:active { transform: scale(0.95); }
.offline-retry-btn[disabled] { opacity: 0.6; cursor: default; }

/* --- leaderboard overlay ------------------------------------------------- */
.lb-overlay {
  position: fixed;
  inset: 0;
  z-index: 900;
  /* Blue sky base underneath everything */
  background: #e3faff;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.25s ease;
}
.lb-overlay.visible { opacity: 1; }

/* Safari composites a WebGL/canvas iframe on its own GPU layer that can paint
   over sibling DOM regardless of z-index. Promoting the overlay to its own
   layer (and hiding the iframe from JS while it's shown) keeps it on top. */
.lb-overlay {
  transform: translateZ(0);
  -webkit-transform: translateZ(0);
}



/* --- leaderboard entrance animations ------------------------------------- */
@keyframes lb-panel-in {
  0%   { opacity: 0; transform: scale(0.82) translateY(24px); }
  60%  { opacity: 1; transform: scale(1.03) translateY(0); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes lb-row-in {
  0%   { opacity: 0; transform: translateX(-36px) scale(0.96); }
  100% { opacity: 1; transform: translateX(0) scale(1); }
}
@keyframes lb-msg-in {
  0%   { opacity: 0; transform: translateY(14px); }
  100% { opacity: 1; transform: translateY(0); }
}
/* The tag gently bobs toward the row and back, drawing the eye to it while
   staying anchored at its resting position (106% past the plate edge). */
@keyframes lb-tag-loop {
  0%   { transform: translate(106%, var(--tag-offset-y, -50%)); }
  50%  { transform: translate(94%, var(--tag-offset-y, -50%)); }
  100% { transform: translate(106%, var(--tag-offset-y, -50%)); }
}

/* When the overlay is visible, play the panel entrance. Respect reduced motion. */
@media (prefers-reduced-motion: no-preference) {
  .lb-overlay.visible .lb-panel {
    animation: lb-panel-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }
  .lb-overlay.visible .lb-row {
    animation: lb-row-in 0.4s ease both;
    animation-delay: var(--row-delay, 0s);
  }
  .lb-overlay.visible .lb-message {
    animation: lb-msg-in 0.45s ease 0.35s both;
  }
}

/* --- Score screen entrance animations ------------------------------------ */
/*
 * sc-panel-in  : the whole panel pops in with a spring-like bounce.
 * sc-title-in  : "Your Final Score" image slides down from above + fades in.
 * sc-score-pop : the score number bounces from invisible-tiny to full size,
 *                overshooting slightly to feel punchy and celebratory.
 * sc-points-in : the "Points" label fades in and rises from below.
 * sc-next-in   : the Next button fades in and drifts up from below.
 */
@keyframes sc-panel-in {
  0%   { opacity: 0; transform: scale(0.78) translateY(30px); }
  65%  { opacity: 1; transform: scale(1.04) translateY(-4px); }
  85%  { transform: scale(0.98) translateY(0); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes sc-title-in {
  0%   { opacity: 0; transform: translateY(-28px) scale(0.92); }
  70%  { opacity: 1; transform: translateY(4px) scale(1.02); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes sc-score-pop {
  0%   { opacity: 0; transform: scale(0.35); }
  55%  { opacity: 1; transform: scale(1.18); }
  75%  { transform: scale(0.93); }
  90%  { transform: scale(1.05); }
  100% { opacity: 1; transform: scale(1); }
}
@keyframes sc-points-in {
  0%   { opacity: 0; transform: translateY(18px); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes sc-next-in {
  0%   { opacity: 0; transform: translateX(-50%) translateY(16px); }
  100% { opacity: 1; transform: translateX(-50%) translateY(0); }
}

@media (prefers-reduced-motion: no-preference) {
  /* Panel pops in first */
  .score-overlay.visible .sc-panel {
    animation: sc-panel-in 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }
  /* Title slides down slightly after the panel lands */
  .score-overlay.visible .sc-title-img {
    animation: sc-title-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s both;
  }
  /* Score number bounces in with a punchy delay so it feels like a reveal */
  .score-overlay.visible .sc-score-stack {
    animation: sc-score-pop 0.65s cubic-bezier(0.34, 1.56, 0.64, 1) 0.35s both;
  }
  /* "Points" label fades in after the number settles */
  .score-overlay.visible .sc-point-label {
    animation: sc-points-in 0.4s ease 0.72s both;
  }
  /* Next button rises in last */
  .score-overlay.visible .sc-next-btn {
    animation: sc-next-in 0.4s ease 0.55s both;
  }
}

/* Ensure the [hidden] attribute wins over display:flex above */
.lb-overlay[hidden] { display: none; }
.offline-banner[hidden] { display: none; }

/* The scene is now assembled from FIVE layers so it stretches responsively.
   Painting order in a multi-value `background` is FIRST value on TOP, LAST on
   the bottom, so the stack (top → bottom) is:
     1. water-tank-right.svg  — pinned to the right edge, ON TOP of border-right
     2. border-right.png      — fixed-width right cap
     3. border-left.png       — fixed-width left cap
     4. border-center.png     — TILED (repeat-x) to fill the gap between caps;
                                the caps painted above hide any tile overflow
     5. background-img.png     — cover-sized fill behind the whole frame
   Every border piece is scaled to --lb-frame-h and vertically centred so their
   ground lines align; the background image covers the full viewport. */
.lb-overlay::before {
  content: "";
  position: fixed;
  /* Constrain the scene box to the FRAME HEIGHT: full width, but inset from the
     top/bottom by the frame-gap so it's vertically centred. The leftover gap
     above/below sits OUTSIDE this box and shows the body's #e3faff, while the
     background image (cover, below) fills this inner box edge-to-edge — so no
     cyan shows on the sides inside the frame. */
  left: 0;
  right: 0;
  top: var(--lb-frame-gap);
  bottom: var(--lb-frame-gap);
  z-index: -1;
  /* Solid fill behind every layer as a safety backing. */
  background-color: #E3FAFF;
  background-image:
    url("assets/water-tank-right.svg"),
    url("assets/border-right.png"),
    url("assets/border-left.png"),
    url("assets/border-center.png"),
    url("assets/background-img.jpg");
  background-repeat:
    no-repeat,
    no-repeat,
    no-repeat,
    repeat-x,
    no-repeat;
  /* Water tank sits in the empty light band of the right cap, to the RIGHT of the
     cap's frame line — NOT pushed into the frame interior. In the 249px-wide cap
     the frame line sits at x~131, leaving a band from ~x133 to the cap's right
     edge; the tube is centred in that band. Its right edge therefore sits ~32px
     (32/249 ~= 0.1285 of the cap width) in from the cap's right edge. Caps pin to
     their viewport edges (plus the optional side gap); the tiled centre also
     starts at the left cap so its seams line up; the background image is centred
     and covers everything. */
  background-position:
    right calc(var(--lb-side-gap) + var(--lb-border-right-w) * 0.1285) center,
    right var(--lb-side-gap) center,
    left var(--lb-side-gap) center,
    left calc(var(--lb-side-gap) + var(--lb-border-left-w)) center,
    center center;
  background-size:
    auto var(--lb-tank-h),
    auto var(--lb-frame-h),
    auto var(--lb-frame-h),
    auto var(--lb-frame-h),
    cover;
  pointer-events: none;
}

/* Stage holds the panel. The panel is 1.2956:1, so sizing it from width alone
   overflowed the viewport height in phone landscape (iPhone 14 landscape was
   +209px, SE +99px). Cap the width by the height budget too, so the panel
   always fits entirely on screen.
   The panel must also stay inside the scene frame, hence the --lb-frame-w cap.
   Height budget leaves room for the Close button, which straddles the panel's
   bottom edge by half its own height. */
.lb-stage {
  position: relative;
  width: min(
    92vw,
    1100px,
    calc(var(--lb-frame-w) * 0.74),
    calc(var(--lb-frame-h) * 0.86 * var(--lb-panel-ar)),
    calc((var(--lb-vh) - var(--lb-close-h)) * var(--lb-panel-ar))
  );
  /* The flex parent centres on the VIEWPORT, but the usable inner area sits
     BETWEEN the two border caps, and the right cap is wider than the left. Its
     centre is therefore (rightCap − leftCap)/2 to the left of the viewport
     centre — shift the panel by that amount so it's centred in the frame. */
  margin-right: calc(
    (var(--lb-inset-right) - var(--lb-inset-left)) / 2
  );
  /* Nudge the whole panel up a little so it sits slightly above the vertical
     centre of the frame. The Close button straddles the panel's bottom edge and
     hangs below it, so a small upward shift keeps the composition balanced. */
  margin-bottom: var(--lb-close-h);
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Home button — pinned just inside the rounded top-left corner of the frame,
   near the LEFT edge. The left cap (border-left.png, 163px wide) has its frame
   line at x~30, so the button's left is offset by 0.185 of the cap width (just
   past the line) plus a small ~2% frame-height nudge. Deriving from the cap
   width keeps it locked to the corner at any aspect ratio. */
.lb-home {
  position: fixed;
  top: calc(50% - var(--lb-frame-h) / 2 + var(--lb-frame-h) * 0.05);
  left: calc(var(--lb-scene-x) + var(--lb-inset-left) + var(--lb-frame-h) * 0.02);
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  /* vmin so it tracks the short edge in landscape rather than the width. */
  width: clamp(40px, 7vmin, 90px);
  z-index: 5;
  transition: transform 0.1s ease;
}
.lb-home:active { transform: scale(0.92); }
.lb-home img { width: 100%; display: block; }

/* Panel — uses the full SVG with the "Leaderboard" title + sparkles + crown
   baked in, so no separate title image is needed. */
.lb-panel {
  position: relative;
  width: 100%;
  aspect-ratio: 1214 / 937;
  /* Establish a container so inner text can size off the PANEL width (cqw)
     rather than the viewport. The panel is now often height-constrained, so
     viewport-based font sizes drifted out of proportion with it — e.g. on a
     1366x1024 iPad the panel is capped well below 92vw, yet `2.4vw` text kept
     growing with the screen. */
  container-type: inline-size;
  container-name: lb-panel;
}
.lb-panel-bg {
  position: absolute;
  inset: 0;
  background: url("assets/leaderboard-container-full-accessories.svg") center / 100% 100% no-repeat;
}

/* ---------------------------------------------------------------------------
   "Out of top 10" state: the board has to show the tail of the top list PLUS
   the player's own row plus a two-line message, so it uses the taller artwork
   (leaderboard-container-full-accessories-long.svg, 1224x1052 => AR 1.1635).
   .lb-overlay--long is toggled by renderLeaderboard(). Everything that depends
   on the panel shape reads --lb-panel-ar, so overriding that one token here
   re-flows the stage width budget automatically. */
.lb-overlay--long {
  --lb-panel-ar: 1.1635; /* 1224/1052 */
}
.lb-overlay--long .lb-panel { aspect-ratio: 1224 / 1052; }
.lb-overlay--long .lb-panel-bg {
  background-image: url("assets/leaderboard-container-full-accessories-long.svg");
}
/* Inner light (E2FAFF) area of the long SVG: x 115.4->1095.9, y 132.9->996.4
   of a 1224x1052 canvas => left 9.4%, right 10.5%, top 12.6%, bottom 5.3%.
   Same padding deltas as the short panel (clear the baked-in title on top and
   the close button at the bottom). */
.lb-overlay--long .lb-panel-inner { inset: 21% 14% 11.5% 13%; }
/* Inner light (E2FAFF) area measured from the SVG:
   x 106->1087, y 170->896 of a 1214x937 canvas
   => left 8.8%, right 10.5%, top 18.1%, bottom 4.3%.
   Add a touch of padding so rows/text don't touch the rounded border. */
.lb-panel-inner {
  position: absolute;
  /* Extra top room so the first row clears the baked-in title, plus a bit more
     bottom room so the close button can straddle the outline. */
  inset: 26% 14% 13% 12.5%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
}

/* Rows list */
.lb-list {
  width: 100%;
  /* `min-height: 0` is required or the flex item would refuse to shrink below
     its content height and the list would overflow instead of scrolling.
     Fixed viewport height rather than "fill the leftover space": stretching the
     list pushed the pinned row and the message down to the panel's bottom edge.
     A row is 0.186 x list width (822/153 plate) and the list is ~73% of the
     panel width, so one row ~= 13.6cqw. ~41cqw fits three FULL plates
     (including rank 3's crown and bottom outline) with only a thin sliver of
     the fourth showing, which hints that the list scrolls. */
  /* `flex-shrink: 0` matters: as a column flex item the list was being squeezed
     back to the leftover space, which silently cancelled the height below. It
     is held to `max-height: 100%` so it still can't overflow the panel. */
  flex: 0 0 auto;
  height: calc(41cqw - 18px);
  max-height: 100%;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  /* Rows sit flush against each other — the SVG plates already carry their own
     breathing room, so any extra gap just wasted vertical space. */
  gap: 0;
  /* Right padding must clear the scrollbar; both scale together off --lb-sb-w
     so the bar never swallows a small panel in phone landscape. */
  padding: 0 calc(var(--lb-sb-w) + 2px) 0 4px;
  /* `auto` keeps the wider native scrollbar in Firefox; `thin` would shrink it. */
  scrollbar-width: auto;
  scrollbar-color: #00b6e8 #2e427e;
}
.lb-list::-webkit-scrollbar {
  width: var(--lb-sb-w);
}
.lb-list::-webkit-scrollbar-track {
  background: #2e427e;
  /* Fully rounded pill ends. */
  border-radius: 999px;
  margin: 4px 0;
}
.lb-list::-webkit-scrollbar-thumb {
  background: #00b6e8;
  border-radius: 999px;
  /* Transparent border + padding-box clip insets the thumb inside the track so
     both pills remain visible at this larger width. */
  border: var(--lb-sb-inset) solid transparent;
  background-clip: padding-box;
}
.lb-list::-webkit-scrollbar-thumb:hover {
  background: #00a0cc;
  background-clip: padding-box;
}

/* A single row: SVG background contains rank badge + nameplate.
   The base rule targets the accessorised ranks 1-3 canvas (822x153); rows
   without accessories override the geometry below so every rendered plate ends
   up the same size. */
.lb-row {
  position: relative;
  width: 100%;
  aspect-ratio: 822 / 153;
  background: var(--row-bg) center / 100% 100% no-repeat;
  display: flex;
  align-items: center;
  flex: 0 0 auto;
}

/* Ranks 4-10 (`4..10-container.svg`) are a 810x139 canvas whose rounded plate
   fills it edge-to-edge (plate = 806x135 at x2/y2). Ranks 1-3 are a 822x153
   canvas carrying crown accessories, so their identical 806x135 plate sits
   inset (x13.2 / y15.9).
   Stretching the plain canvas into the 822/153 box therefore scaled its plate
   UP — it read wider and taller than the top three. Here we give the plain rows
   their own native aspect and trim the width by the same 98.5% so the rendered
   plate lands at exactly the same size/left edge as ranks 1-3:
     plate w: 806/810 * 98.5%  == 806/822 * 100%
     plate h: 135/139 * (98.5% * 139/810) == 135/822 * 100% */
.lb-row--plain {
  aspect-ratio: 810 / 139;
  width: 98.5%;
  margin-left: 1.4%;
  /* Ranks 1-3 reserve 15.9 canvas units above the plate for the crown (plate
     spans y 15.9..150.9 of 153), which also acted as row spacing. Plain rows
     only have 2 units of padding (plate y 2..137 of 139), so that spacing has
     to be added back as a margin or they ride up too high.
     Resolving both against the list content width W:
       ranked->ranked plate gap = 2.1/822·W + gap + 15.9/822·W = 0.0219W + gap
       plain  plate gap         = 2/139·(139/810·0.985)W·2 + gap + m
                                = 0.0049W + gap + m
     => m = 0.0219W - 0.0049W = 1.7% gives every plate an even rhythm.
     (Percentage margins resolve against the container's width, same as the
     row widths above, so this stays proportional at any viewport size.) */
  margin-top: 1.7%;
}
/* The margin only exists to replace the missing crown padding *between* rows.
   If a plain row leads the list (e.g. the out-of-top-10 view showing ranks
   9 & 10), drop it so the list doesn't start with a dead band. */
.lb-row--plain:first-child { margin-top: 0; }

/* Vertical band occupied by the rounded plate inside each row's canvas, as a
   share of the row height. Text is centred against THIS band, not the whole
   canvas — otherwise the crown padding at the top of the 1-3 artwork drags the
   label above the plate's true centre.
   Measured from the plate paths in each asset:
     ranks 1-2   153 tall, plate y  15.92..150.92 -> 10.40% / 1.36%
     rank  3     154 tall, plate y  16.34..151.34 -> 10.61% / 1.73%
     ranks 4-10  139 tall, plate y   2.00..137.00 ->  1.44% / 1.44%
     out-of-10   141 tall, plate y   3.00..138.00 ->  2.13% / 2.13% */
.lb-row {
  --plate-top: 10.4%;
  --plate-bottom: 1.36%;
}
.lb-row--plain {
  --plate-top: 1.44%;
  --plate-bottom: 1.44%;
}
.lb-row--out {
  --plate-top: 2.13%;
  --plate-bottom: 2.13%;
}

.lb-name {
  position: absolute;
  top: var(--plate-top);
  bottom: var(--plate-bottom);
  left: 21%;
  right: 28%;
  display: flex;
  align-items: center;
  font-weight: 800;
  color: #003062;
  /* 3.5cqw ~= the old 34px at the 970px max panel width, but now tied to the
     panel so names stay proportional to their plate on every device. */
  font-size: clamp(11px, 3.5cqw, 34px);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Only the out-of-top-10 ("Your Rank") row has no rank badge, so its name can
   start near the left edge. Ranked rows must clear the baked-in badge. */
/* The out-of-top-10 plate has no rank badge, so the name starts near the left
   edge — nudged in so it doesn't hug the rounded border. */
.lb-row--out .lb-name { left: 9%; }
/* Plain rows are measured against the narrower 810-wide canvas, so re-express
   the same absolute offsets as a share of that width. */
.lb-row--plain .lb-name { left: 19.6%; right: 28.5%; }

.lb-score {
  position: absolute;
  /* Same plate-relative centring as .lb-name so the star + number line up with
     the name on every row type. */
  top: var(--plate-top);
  bottom: var(--plate-bottom);
  right: 8%;
  display: flex;
  align-items: center;
  gap: 4px;
}
.lb-star {
  /* Panel-relative too, so the star keeps its ratio to the score text. */
  width: clamp(16px, 5.2cqw, 56px);
  height: auto;
  display: block;
}
.lb-score-val {
  font-weight: 900;
  /* White glyph with a 4px outer brown outline. `-webkit-text-stroke` centres
     the stroke on the glyph edge, so we set 8px and use `paint-order: stroke`
     to draw the stroke *behind* the fill — the inner half is covered by the
     white fill, leaving exactly 4px visible outside. */
  color: #ffffff;
  /* Scales with the type: a flat 8px is ~4px visible at the 34px desktop size,
     but would swamp the glyphs at the 14px phone-landscape size. 0.235em keeps
     the visible outline proportional (8/34em). */
  -webkit-text-stroke: 0.235em #b86d03;
  paint-order: stroke fill;
  font-size: clamp(11px, 3.5cqw, 34px);
  min-width: 2ch;
  text-align: left;
}

/* "Keep going" message */
.lb-message {
  margin-top: 2%;
  text-align: center;
  /* Cream glyph with a dark blue outline, matching the row labels' treatment:
     the stroke is centred on the glyph edge, so `paint-order: stroke` draws it
     behind the fill and only the outer half stays visible. 0.16em keeps the
     outline proportional to the type instead of a fixed pixel width. */
  color: #fdfaf3;
  /* 4px visible outline. The stroke is centred on the glyph edge and half of it
     is hidden behind the fill, so 8px of stroke reads as 4px outside. Expressed
     as 0.25em (8/32) against the 32px design size so it stays proportional when
     the clamp() scales the type down. */
  -webkit-text-stroke: 0.25em #003062;
  paint-order: stroke fill;
  flex: 0 0 auto;
  font-weight: 900;
  line-height: 1.15;
  font-size: clamp(10px, 3.2cqw, 32px);
}
.lb-message b { color: #fdfaf3; }

/* Close button — straddles the bottom outline of the leaderboard panel */
.lb-close {
  position: absolute;
  /* The panel is shifted right within the frame (see .lb-stage margin-right) to
     sit centred between the asymmetric border caps. left:50% would centre the
     button on the PANEL; subtract the same inset delta so the Close button lines
     up with the true frame centre instead. */
  left: calc(50% - (var(--lb-inset-right) - var(--lb-inset-left)) / 2);
  bottom: 0;
  /* half above / half below the panel's bottom edge so it overlaps the outline */
  transform: translate(-50%, 50%);
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  /* Shared with the stage's height budget via the token, and vmin-based so it
     scales off the short edge in landscape instead of growing with width. */
  width: var(--lb-close-w);
  z-index: 7;
  transition: transform 0.1s ease;
}
.lb-close:active { transform: translate(-50%, 50%) scale(0.94); }
.lb-close img { width: 100%; display: block; }

/* Fallback for browsers without container queries (cqw). Without this the
   clamp() lower bound would apply and all text would render at its minimum. */
@supports not (container-type: inline-size) {
  .lb-name,
  .lb-score-val { font-size: clamp(11px, 2.4vw, 34px); }
  .lb-message { font-size: clamp(10px, 2.2vw, 32px); }
  .lb-star { width: clamp(16px, 3.4vw, 56px); }
  .lb-yourrank-tag { width: clamp(70px, 13vw, 250px); }
}

/* Short landscape (phones held sideways): the panel is height-driven there, so
   reclaim vertical space by tightening the inner padding and the message. */
@media (orientation: landscape) and (max-height: 500px) {
  .lb-panel-inner { inset: 25% 13% 12% 11.5%; }
  /* Needs its own rule: `.lb-overlay--long .lb-panel-inner` is more specific
     than the bare `.lb-panel-inner` above, so it would otherwise win here. */
  .lb-overlay--long .lb-panel-inner { inset: 20% 13% 13% 12%; }
  .lb-message { margin-top: 1%; }
}

/* Pinned "your rank" row: sits between the scrolling list and the message, so
   the player's own position is always visible while the full top 10 can still
   be scrolled through above it. Padded to match .lb-list's content box (same
   scrollbar allowance) so the row aligns with the rows above it. */
.lb-you {
  position: relative;
  flex: 0 0 auto;
  width: 100%;
  padding: 0 calc(var(--lb-sb-w) + 2px) 0 4px;
  margin-top: 0;
}
.lb-you[hidden] { display: none; }

/* "Your Rank" tag: anchored to the panel's inner box (NOT to the scrolling
   list) so it can overflow outside the list while still tracking the player's
   row — JS sets `--tag-y` (pixels from the top of .lb-panel-inner) on every
   render and on every list scroll. Pushed fully past the row's right edge so it
   never covers the star/score. */
.lb-yourrank-tag {
  position: absolute;
  /* Anchor to the ROW's right edge, not the inner box's: the list reserves
     `--lb-sb-w + 2px` on the right for the scrollbar, so the plates end that
     far short of the inner box. Without this the tag drifted right by the
     scrollbar's width and lost its alignment with the plate. */
  right: calc(var(--lb-sb-w) + 2px);
  top: 0;
  translate: 0 var(--tag-y, 0px);
  /* Most of the tag hangs past the plate's right edge so its label sits clear
     of the star + score; only the rounded left cap overlaps the plate corner.
     The Y offset (--tag-offset-y) centres the tag on the row's centre line so
     it points squarely at the player's plate. The pinned out-of-top-10 row
     (.lb-you) overrides it to sit lower. */
  transform: translate(106%, var(--tag-offset-y, -50%));
  width: clamp(70px, 19cqw, 250px);
  z-index: 6;
  pointer-events: none;
  /* Hidden by default; JS adds `.is-shown` once it has measured the row. Inside
     the top 10 the class is removed again after ~2s (or on the first scroll
     gesture), so the tag fades out instead of popping. */
  opacity: 0;
  transition: translate 0.08s linear, opacity 0.35s ease;
}
.lb-yourrank-tag.is-shown { opacity: 1; }
.lb-yourrank-tag[hidden] { display: none; }
/* The pinned out-of-top-10 row now uses the same layout-based --tag-y
   measurement as the list rows, so it centres at the default -50% too. Kept as
   a seam in case the pinned plate ever needs a per-state nudge. */
.lb-yourrank-tag.lb-yourrank-tag--pinned { --tag-offset-y: -50%; }

/* Looping attention animation: the tag slides in from the right to its resting
   position and gently bounces back out, drawing the eye to the player's row.
   JS keeps `translate` (the vertical --tag-y tracking) separate from `transform`
   (the horizontal loop), so the two never fight. */
@media (prefers-reduced-motion: no-preference) {
  .lb-yourrank-tag.is-shown {
    animation: lb-tag-loop 1.8s ease-in-out infinite;
  }
}

/* ==========================================================================
   Loading screen
   Shown while the game iframe is booting (first visit + after the leaderboard
   is closed and the game reloads). Hidden when the game posts FINISH_LOADING.
   Sits above everything so nothing behind it is visible while loading.
   ========================================================================== */
.loading-overlay {
  position: fixed;
  inset: 0;
  /* Below the rotate-overlay (9999) so the "please rotate" notice always wins,
     but above the leaderboard/offline layers. */
  z-index: 9998;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #003062;
  background-image: url("assets/loading-bg.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.loading-overlay[hidden] { display: none; }

.loading-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
}

.loading-spinner {
  width: clamp(40px, 8vw, 64px);
  height: clamp(40px, 8vw, 64px);
  border: 5px solid rgba(255, 255, 255, 0.3);
  border-top-color: #ffffff;
  border-radius: 50%;
  animation: loading-spin 0.9s linear infinite;
}

@keyframes loading-spin {
  to { transform: rotate(360deg); }
}

.loading-text {
  margin: 0;
  font-family: "Nunito", system-ui, sans-serif;
  font-weight: 600;
  font-size: clamp(16px, 4vw, 24px);
  color: #ffffff;
  text-align: center;
}

