  /* ─── Leaderboard modal ──────────────────────────────────────────────────
     Full-screen overlay with a single rounded card: title + period tabs +
     stage filter pills + top-10 list. Opens from the landing RANKINGS pill
     or the game-over "View Rankings" link. Purely client-side; fetches
     /api/leaderboard in src/leaderboard.js. */
  #leaderboard-modal {
    position: fixed; inset: 0;
    display: none; align-items: center; justify-content: center;
    z-index: 1200;  /* above settings-modal (1100) — opened from inside settings */
    /* rc159 — opaque leaderboard backdrop. */
    background: var(--panel-bg-2, #0e1426);
  }
  #leaderboard-modal.visible { display: flex; }
  #leaderboard-modal .lb-backdrop { position: absolute; inset: 0; cursor: pointer; }
  #leaderboard-modal .lb-card {
    position: relative;
    width: min(760px, 92vw);
    height: 88vh;
    display: flex; flex-direction: column;
    /* rc159 — opaque leaderboard card. */
    background: var(--panel-bg, #131a2e);
    border: 1px solid #d1aeff;
    border-radius: 18px;
    box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
    overflow: hidden;
    padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  }
  .lb-head {
    display: flex; align-items: center;
    padding: calc(34px + env(safe-area-inset-top, 0px)) 22px 18px;
    border-bottom: 1px solid #2a3454;
  }
  .lb-head h2 {
    font-size: 22px; font-weight: 800; letter-spacing: 1.2px; color: var(--text);
    flex: 1;
  }
  .lb-close {
    /* rc159 — opaque close button. */
    background: #2a3454; color: var(--text-dim);
    border: 1px solid #4a5478;
    border-radius: 50%;
    width: 32px; height: 32px;
    font-size: 18px; font-family: inherit;
    cursor: pointer; line-height: 1;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
  }
  .lb-close:hover {
    color: var(--text); border-color: #6a7498;
    background: #3a4470;
  }
  .lb-period-tabs {
    display: flex; gap: 8px;
    padding: 14px 22px 6px;
  }
  .lb-period {
    flex: 1;
    padding: 9px 12px;
    /* rc159 — opaque period pill. */
    background: #1c2236;
    color: var(--text-dim);
    border: 1px solid #2e3854;
    border-radius: var(--radius-pill);
    font-size: 11px; font-weight: 700; letter-spacing: 0.9px;
    font-family: inherit; cursor: pointer;
    transition: color 0.15s, background 0.15s, border-color 0.15s, transform 0.15s;
  }
  .lb-period:hover { color: var(--text); background: #2a3050; }
  .lb-period.active {
    color: #1a0a2a;
    background: linear-gradient(135deg, #9b7bff 0%, #d1aeff 100%);
    border-color: transparent;
    box-shadow: 0 3px 10px rgba(155, 123, 255, 0.35);
  }
  .lb-list {
    list-style: none;
    margin: 10px 0 0;
    padding: 6px 14px 14px;
    overflow-y: auto;
    flex: 1;
  }
  .lb-row {
    display: grid;
    grid-template-columns: 34px 1fr auto;
    grid-template-areas: "rank name score" "rank meta meta";
    align-items: center; column-gap: 12px; row-gap: 2px;
    padding: 10px 14px;
    border-radius: 12px;
    border: 1px solid transparent;
    transition: background 0.15s;
  }
  .lb-row:hover { background: rgba(255, 255, 255, 0.03); }
  .lb-row + .lb-row { margin-top: 4px; }
  .lb-row:nth-child(1) .lb-rank { color: var(--gold); }
  .lb-row:nth-child(2) .lb-rank { color: #d1d5e2; }
  .lb-row:nth-child(3) .lb-rank { color: #cd8a5f; }
  .lb-rank {
    grid-area: rank;
    font-size: 18px; font-weight: 800;
    color: var(--text-dim);
    text-align: center;
  }
  .lb-name {
    grid-area: name;
    font-size: 14px; font-weight: 700;
    color: var(--text);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .lb-score {
    grid-area: score;
    font-size: 15px; font-weight: 800;
    color: var(--gold);
    letter-spacing: 0.3px;
  }
  .lb-meta {
    grid-area: meta;
    font-size: 11px; color: var(--text-dim);
    letter-spacing: 0.2px;
  }
  /* Stage where the run was scored — leads the meta line as a distinct
     badge now that the board mixes all stages (no per-stage filter chips). */
  .lb-stage-tag {
    display: inline-block;
    margin-right: 7px;
    padding: 1px 8px;
    border-radius: var(--radius-pill);
    background: rgba(126, 226, 255, 0.14);
    border: 1px solid rgba(126, 226, 255, 0.3);
    color: #cfeeff;
    font-size: 10px; font-weight: 700; letter-spacing: 0.3px;
    vertical-align: middle;
  }
  .lb-loading, .lb-empty, .lb-error {
    list-style: none;
    text-align: center; padding: 24px 12px;
    color: var(--text-dim);
    font-size: 13px;
  }
  .lb-error { color: #ff9a9a; }
  .lb-skeleton .lb-rank,
  .lb-skeleton .lb-name,
  .lb-skeleton .lb-score,
  .lb-skeleton .lb-meta {
    background: linear-gradient(90deg,
      rgba(255, 255, 255, 0.04) 0%,
      rgba(255, 255, 255, 0.09) 50%,
      rgba(255, 255, 255, 0.04) 100%);
    background-size: 200% 100%;
    border-radius: 6px;
    color: transparent;
    min-height: 1em;
    animation: lb-shimmer 1.2s ease-in-out infinite;
  }
  .lb-skeleton .lb-rank  { width: 22px; margin: 0 auto; height: 16px; }
  .lb-skeleton .lb-name  { width: 55%;  height: 14px; }
  .lb-skeleton .lb-score { width: 64px; height: 15px; justify-self: end; }
  .lb-skeleton .lb-meta  { width: 70%;  height: 11px; margin-top: 2px; }
  .lb-skeleton:hover { background: transparent; }
  @keyframes lb-shimmer {
    0%   { background-position:  100% 0; }
    100% { background-position: -100% 0; }
  }
  @media (prefers-reduced-motion: reduce) {
    .lb-skeleton .lb-rank,
    .lb-skeleton .lb-name,
    .lb-skeleton .lb-score,
    .lb-skeleton .lb-meta { animation: none; }
  }
  .lb-foot {
    display: flex; align-items: center; gap: 12px;
    padding: 12px 22px;
    border-top: 1px solid rgba(255, 255, 255, 0.06);
    font-size: 12px; color: var(--text-dim);
  }
  .lb-foot .lb-identity { flex: 1; }
  .lb-foot .lb-my-name { color: var(--text); font-weight: 700; }
  .lb-change-name {
    background: rgba(255, 255, 255, 0.04);
    color: var(--text-dim);
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: var(--radius-pill);
    padding: 6px 14px; font-size: 11px; font-weight: 600;
    letter-spacing: 0.5px; cursor: pointer;
    font-family: inherit;
    transition: color 0.15s, background 0.15s, border-color 0.15s;
  }
  .lb-change-name:hover {
    color: var(--text);
    background: rgba(255, 255, 255, 0.08);
    border-color: rgba(255, 255, 255, 0.24);
  }

  /* Inline name-entry dialog — presented once on the first submission, and
     again from the leaderboard footer's "Change name" button. Styled to
     match the leaderboard card's dark-violet palette. */
  .lb-name-modal {
    position: fixed; inset: 0;
    display: flex; align-items: center; justify-content: center;
    z-index: 1300;  /* above #leaderboard-modal (1200) */
  }
  .lb-name-backdrop {
    position: absolute; inset: 0;
    background: rgba(6, 8, 18, 0.82);
    cursor: pointer;
  }
  .lb-name-card {
    position: relative;
    width: min(420px, 92vw);
    background: linear-gradient(160deg, rgba(30, 36, 60, 0.98), rgba(22, 18, 40, 0.98));
    border: 1px solid rgba(209, 174, 255, 0.3);
    border-radius: 16px;
    box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
    padding: 22px 24px;
  }
  .lb-name-card h3 {
    font-size: 16px; font-weight: 800; letter-spacing: 1px;
    color: var(--text); margin-bottom: 6px;
  }
  .lb-name-help {
    font-size: 12px; color: var(--text-dim);
    margin-bottom: 14px; line-height: 1.5;
  }
  .lb-name-input {
    width: 100%;
    background: rgba(0, 0, 0, 0.3);
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 10px;
    color: var(--text);
    font-family: inherit;
    font-size: 16px; font-weight: 600;
    padding: 10px 12px;
    outline: none;
    transition: border-color 0.15s, background 0.15s;
  }
  .lb-name-input:focus {
    border-color: rgba(209, 174, 255, 0.55);
    background: rgba(0, 0, 0, 0.42);
  }
  .lb-name-actions {
    display: flex; justify-content: flex-end; gap: 10px;
    margin-top: 16px;
  }
  .lb-name-cancel, .lb-name-save {
    font-family: inherit; font-weight: 700; letter-spacing: 0.6px;
    font-size: 12px; padding: 9px 18px;
    border-radius: var(--radius-pill);
    cursor: pointer;
  }
  .lb-name-cancel {
    background: transparent; color: var(--text-dim);
    border: 1px solid rgba(255, 255, 255, 0.14);
    transition: color 0.15s, border-color 0.15s;
  }
  .lb-name-cancel:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.3); }
  .lb-name-save {
    background: linear-gradient(135deg, #9b7bff 0%, #d1aeff 100%);
    color: #1a0a2a;
    border: none;
    box-shadow: 0 4px 14px rgba(155, 123, 255, 0.38);
    transition: transform 0.15s, box-shadow 0.18s, filter 0.15s;
  }
  .lb-name-save:hover {
    transform: translateY(-1px);
    box-shadow: 0 8px 22px rgba(155, 123, 255, 0.55);
    filter: brightness(1.08);
  }

  /* Leaderboard — portrait */
  @media (max-width: 600px) {
    #leaderboard-modal .lb-card { width: 94vw; height: 92vh; border-radius: 14px; }
    .lb-head { padding: 14px 16px; }
    .lb-head h2 { font-size: 18px; letter-spacing: 0.9px; }
    .lb-period-tabs { padding: 10px 14px 4px; gap: 6px; }
    .lb-period { font-size: 10px; padding: 8px 6px; letter-spacing: 0.6px; }
    .lb-list { padding: 4px 10px 10px; }
    .lb-row { grid-template-columns: 28px 1fr auto; padding: 8px 10px; column-gap: 10px; }
    .lb-name { font-size: 13px; }
    .lb-score { font-size: 14px; }
    .lb-meta { font-size: 10px; }
    .lb-foot { padding: 10px 16px; gap: 8px; }
    .lb-foot .lb-identity { font-size: 11px; }
    .lb-change-name { padding: 5px 10px; font-size: 10.5px; }
  }

  /* ─── ACHIEVEMENTS ─────────────────────────────────────────────────────── */
  /* Toast — mirrors the #stack-toast pattern but lives on its own DOM node so
     timing doesn't fight an in-flight stack-toast animation. Tier color
     drives the border + glow palette. .summary variant is used once on
     landing for the "X unlocked from your prior runs" backfill notice. */
  #achievement-toast {
    position: fixed; top: 22%; left: 50%; transform: translate(-50%, -40%) scale(0.6);
    display: flex; align-items: center; gap: 12px;
    font-family: 'Space Grotesk', sans-serif;
    color: #fff;
    background: linear-gradient(135deg, rgba(20,22,40,0.94), rgba(40,30,80,0.94));
    border: 1px solid rgba(255,215,102,0.55);
    border-radius: 14px;
    padding: 10px 16px 10px 12px;
    box-shadow: 0 10px 30px rgba(0,0,0,0.45), 0 0 24px rgba(255,215,102,0.25);
    pointer-events: none;
    opacity: 0;
    /* Toast must render above every gameplay overlay (powerup-screen 100,
       wave-modifier 140, pause 150, modals 160, victory/gameover 200,
       relic-shop 250, sub-modals 260) so an unlock popping mid-victory or
       mid-shop is still visible. Stays under stage-transition (9999) so the
       scene-swap curtain still covers it during a stage change. */
    z-index: var(--z-loader);
    max-width: 360px;
    white-space: normal;
  }
  #achievement-toast .ach-toast-icon {
    width: 44px; height: 44px;
    flex-shrink: 0;
  }
  #achievement-toast .ach-toast-icon svg { width: 100%; height: 100%; display: block; }
  #achievement-toast .ach-toast-eyebrow {
    font-size: 10.5px; letter-spacing: 2px; font-weight: 700;
    color: rgba(255,215,102,0.85); text-transform: uppercase;
    margin-bottom: 2px;
  }
  #achievement-toast .ach-toast-name {
    font-size: 16px; font-weight: 800; letter-spacing: 0.6px;
    line-height: 1.15;
  }
  #achievement-toast .ach-toast-desc {
    font-size: 12px; opacity: 0.78; margin-top: 3px; line-height: 1.35;
  }
  #achievement-toast.tier-bronze {
    border-color: rgba(245,176,106,0.65);
    box-shadow: 0 10px 30px rgba(0,0,0,0.45), 0 0 24px rgba(245,176,106,0.30);
  }
  #achievement-toast.tier-bronze .ach-toast-eyebrow { color: #ffd9a8; }
  #achievement-toast.tier-silver {
    border-color: rgba(220,228,240,0.65);
    box-shadow: 0 10px 30px rgba(0,0,0,0.45), 0 0 24px rgba(220,228,240,0.28);
  }
  #achievement-toast.tier-silver .ach-toast-eyebrow { color: #e6ecf6; }
  #achievement-toast.tier-gold {
    border-color: rgba(255,226,122,0.75);
    box-shadow: 0 10px 30px rgba(0,0,0,0.45), 0 0 28px rgba(255,215,102,0.40);
  }
  #achievement-toast.tier-gold .ach-toast-eyebrow { color: #ffe27a; }
  #achievement-toast.summary { border-color: rgba(126,226,255,0.55); }
  #achievement-toast.summary .ach-toast-eyebrow { color: #7ee2ff; }
  #achievement-toast.visible {
    animation: ach-toast-pop 2.71s cubic-bezier(.2,.9,.25,1.15) forwards;
  }
  @keyframes ach-toast-pop {
    0%   { transform: translate(-50%, -70%) scale(0.55); opacity: 0; }
    14%  { transform: translate(-50%, -40%) scale(1.04); opacity: 1; }
    24%  { transform: translate(-50%, -40%) scale(1);    opacity: 1; }
    78%  { transform: translate(-50%, -40%) scale(1);    opacity: 1; }
    100% { transform: translate(-50%, -10%) scale(0.94); opacity: 0; }
  }
  @media (max-width: 720px) {
    #achievement-toast { font-size: 14px; padding: 8px 14px 8px 10px; max-width: 92vw; }
    #achievement-toast .ach-toast-icon { width: 36px; height: 36px; }
    #achievement-toast .ach-toast-name { font-size: 14px; }
    #achievement-toast .ach-toast-desc { font-size: 11px; }
  }

  /* Landing-page badge — small pill that mirrors the relic-bar pill style.
     Lives inside #landing-utility-row alongside Relics + Rankings; spacing
     comes from the row's flex `gap` so no margin needed here.
     Double-id (#overlay #achievements-badge) beats the blanket
     `#overlay button` warm-gradient rule below so the gold pill background
     stays gold. */
  #overlay #achievements-badge {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 10px 16px;
    border-radius: var(--radius-pill);
    background: rgba(255, 215, 102, 0.10);
    border: 1px solid rgba(255, 215, 102, 0.35);
    color: #ffe9aa; font-weight: 700; letter-spacing: 0.5px;
    font-family: inherit; font-size: 13px;
    cursor: pointer; pointer-events: auto;
    margin: 0;
    box-shadow: none;
    transition: background 0.15s, border-color 0.15s, transform 0.15s;
  }
  #overlay #achievements-badge:hover,
  #overlay #achievements-badge:focus-visible {
    background: rgba(255, 215, 102, 0.18);
    border-color: rgba(255, 215, 102, 0.55);
    transform: translateY(-1px);
    box-shadow: none;
    outline: none;
  }
  #achievements-badge .ach-badge-icon {
    width: 18px; height: 18px; display: inline-flex;
  }
  #achievements-badge .ach-badge-icon svg { width: 100%; height: 100%; display: block; }

  /* Collection modal — mirrors #relic-shop layout. */
  #achievements-modal {
    position: fixed; inset: 0;
    display: none; align-items: center; justify-content: center;
    z-index: 1200;  /* above settings-modal (1100) — opened from inside settings */
    /* rc159 — opaque achievements backdrop. */
    background: var(--panel-bg-2, #0e1426);
  }
  #achievements-modal.visible { display: flex; }
  #achievements-panel {
    width: min(960px, 92vw);
    max-height: 88vh;
    display: flex; flex-direction: column;
    /* rc159 — opaque achievements panel. */
    background: var(--panel-bg, #131a2e);
    border: 1px solid #ffd766;
    border-radius: 18px;
    box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
    overflow: hidden;
    /* +16dp bottom inset on Android with status bar hidden so list
       content doesn't hug the very bottom edge of the panel. */
    padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  }
  #achievements-header {
    display: flex; align-items: center; gap: 18px;
    /* Status-bar inset — see the matching rule on .wiki-header. +16dp
       on top so the modal doesn't hug the very edge on Android with
       the status bar hidden. */
    padding: calc(34px + env(safe-area-inset-top, 0px)) 22px 18px;
    border-bottom: 1px solid #2a3454;
  }
  #achievements-title {
    font-size: 22px; font-weight: 800; letter-spacing: 1.2px; color: var(--text);
  }
  #achievements-subtitle {
    font-size: 12px; color: var(--text-dim); margin-top: 2px;
  }
  #achievements-progress {
    margin-left: auto;
    display: flex; align-items: center; gap: 8px;
    padding: 6px 14px;
    border-radius: var(--radius-pill);
    /* rc159 — opaque progress chip. */
    background: #3d2c0e;
    border: 1px solid #ffd766;
    font-weight: 700; font-size: 14px;
    color: #ffe9aa;
  }
  #achievements-close {
    background: none; border: none; color: var(--text-dim);
    font-size: 28px; line-height: 1; cursor: pointer;
    padding: 0 4px; font-family: inherit;
    transition: color 0.15s, transform 0.15s;
  }
  #achievements-close:hover { color: var(--text); transform: scale(1.1); }
  #achievements-body {
    flex: 1;
    overflow-y: auto;
    padding: 14px 22px 22px;
  }
  #achievements-grid {
    display: grid; gap: 12px;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  }
  .achievement-card {
    /* rc159 — opaque achievement card. */
    background: var(--card-bg, #1a2138);
    border: 1px solid var(--card-border, #2a3454);
    border-radius: var(--radius-md);
    padding: 14px 14px 12px;
    display: flex; flex-direction: column; gap: 6px;
    transition: border-color 0.18s, transform 0.18s, box-shadow 0.18s;
  }
  .achievement-card .ach-card-icon {
    width: 56px; height: 56px;
    margin: 0 auto 4px;
  }
  .achievement-card .ach-card-icon svg { width: 100%; height: 100%; display: block; }
  .achievement-card .ach-card-name {
    text-align: center;
    font-size: 14px; font-weight: 800; letter-spacing: 0.4px;
    color: var(--text);
  }
  .achievement-card .ach-card-desc {
    text-align: center;
    font-size: 12px; color: var(--text-dim); line-height: 1.4;
    min-height: 2.8em;
  }
  .achievement-card .ach-card-foot {
    margin-top: auto;
    display: flex; align-items: center; justify-content: center; gap: 6px;
    font-size: 10.5px; letter-spacing: 0.4px;
  }
  .achievement-card .ach-card-locked   { color: var(--text-dim); opacity: 0.7; }
  .achievement-card .ach-card-unlocked { color: #cfd6e8; opacity: 0.9; }
  .achievement-card .ach-card-retro {
    color: #1a2240; background: rgba(126,226,255,0.85);
    padding: 1px 6px; border-radius: var(--radius-pill); font-weight: 800;
  }
  .achievement-card.locked {
    opacity: 0.62;
    filter: grayscale(0.7);
  }
  .achievement-card.unlocked.tier-bronze { border-color: rgba(245,176,106,0.45); }
  .achievement-card.unlocked.tier-silver { border-color: rgba(220,228,240,0.45); }
  .achievement-card.unlocked.tier-gold {
    border-color: rgba(255,215,102,0.55);
    box-shadow: 0 0 18px rgba(255,180,80,0.16), 0 0 4px rgba(255,215,102,0.30) inset;
  }
  @media (max-width: 720px) {
    #achievements-panel { max-height: 92vh; border-radius: 0; }
    /* Force exactly 2 columns on mobile so cards stay legible at narrow
       widths instead of squeezing to 1 column or shrinking past readability. */
    #achievements-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
    #achievements-body { padding: 12px 14px 18px; }
    /* Header gets cramped under 720px — let the title/subtitle cluster shrink
       and the progress pill float to the side. Title size drops; subtitle gets
       a touch tighter so the row doesn't overflow on small phones. */
    #achievements-header { gap: 10px; padding: 14px 16px; flex-wrap: wrap; }
    #achievements-title { font-size: 18px; }
    #achievements-subtitle { font-size: 11px; }
    #achievements-progress { padding: 5px 10px; font-size: 12px; }
    #achievements-close { font-size: 26px; }
    .achievement-card { padding: 12px 10px 10px; }
    .achievement-card .ach-card-icon { width: 52px; height: 52px; }
    .achievement-card .ach-card-name { font-size: 13px; letter-spacing: 0.3px; }
    .achievement-card .ach-card-desc { font-size: 11.5px; min-height: 3.2em; }
    .achievement-card .ach-card-foot { font-size: 10px; gap: 4px; }
    .achievement-card .ach-card-retro { padding: 1px 5px; }
  }
  /* Very narrow phones (≤380px) — tighten further but stay 2 columns. */
  @media (max-width: 380px) {
    #achievements-grid { gap: 8px; }
    #achievements-body { padding: 10px 10px 14px; }
    .achievement-card { padding: 10px 8px 8px; }
    .achievement-card .ach-card-icon { width: 44px; height: 44px; }
    .achievement-card .ach-card-name { font-size: 12px; }
    .achievement-card .ach-card-desc { font-size: 11px; min-height: 3.4em; }
  }

  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      transition-duration: 0.01ms !important;
      animation-duration: 0.01ms !important;
    }
  }

  /* ─── i18n: webfont application ───────────────────────────────────────── */
  /* Active CJK / Arabic pack sets --i18n-font from src/i18n-fonts.js
     (e.g. Japanese → 'Noto Sans JP'; Arabic → 'Noto Naskh Arabic, Noto
     Sans Arabic'). Latin packs (en / es / pt-BR / fr / it / tr / vi)
     UNSET the variable rather than setting it empty — an empty value
     would invalidate the whole declaration via var() and the browser
     would cascade to serif/Times.

     Font ORDER: Outfit FIRST, then the per-language webfont. Outfit
     covers Latin + Cyrillic + Greek + Vietnamese diacritics. When the
     active language is Japanese, the page contains BOTH Japanese
     characters (need Noto Sans JP) AND Latin characters (still want
     Outfit so the Latin text doesn't visibly flicker between fonts on
     language change). Putting Outfit first lets the browser resolve
     Latin codepoints from Outfit and CJK / Arabic codepoints from the
     fallback. The `var(--i18n-font, 'Outfit')` default makes the unset
     case duplicate Outfit (harmless). */
  html, body {
    font-family: 'Outfit', var(--i18n-font, 'Outfit'), 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  }

  /* ─── i18n: RTL adjustments ──────────────────────────────────────────── */
  /* When the active language is Arabic the runtime sets <html dir="rtl">.
     The 3D game canvas and HUD numerics are direction-agnostic and stay
     unchanged. Text-bearing modals (settings, wiki, leaderboard, sync)
     use flex layouts that auto-mirror under dir=rtl. We also flip a
     handful of elements that hardcode physical sides:
       - close buttons that are absolute-right become absolute-left
       - text-align: left fragments of pause-card flip to right
     Spot-fix list rather than a global flip — keeps the LTR layout
     identical when the active language is anything other than Arabic. */
  html[dir="rtl"] body { text-align: right; }
  /* Settings modal close button is in a flex row, so flex direction
     handles it naturally. The "Open progress sync" CTA's flex-direction
     is row by default — under dir=rtl that already places the SVG icon
     on the leading (right) edge and the label on the trailing (left)
     edge, which matches the LTR convention of icon-leading. No override
     needed. */
  /* Pause modal: the touch / desktop control hints rely on inline-flow
     direction, which auto-reverses under dir=rtl. The kbd-style key labels
     are language-agnostic so they don't need flipping. */
  html[dir="rtl"] .pc-desktop, html[dir="rtl"] .pc-touch { text-align: right; }
  html[dir="rtl"] #pause-controls .pc-title { text-align: right; }
  /* Wiki — sidebar + entry list float on the leading edge in LTR; flex
     auto-handles this. Just nudge the chevron on category buttons so it
     points the leading direction (toward the entry list). */
  html[dir="rtl"] .wiki-cat-chevron { transform: scaleX(-1); }

  /* ─── Settings cog pill (landing top-right) ──────────────────────────── */
  /* Settings pill — sized to match the achievements badge + rankings pill
     so all three top-right utility chips read as a uniform set. */
  #settings-btn.settings-pill {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 10px 16px;
    background: rgba(20, 26, 44, 0.45);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: var(--radius-pill);
    color: var(--text, #e6ebf6);
    font: inherit;
    font-size: 13px; letter-spacing: 0.5px;
    font-weight: 700;
    cursor: pointer;
    transition: background 0.16s, border-color 0.16s, transform 0.12s;
  }
  #settings-btn.settings-pill:hover {
    background: rgba(34, 44, 72, 0.85);
    border-color: rgba(255, 255, 255, 0.18);
    transform: translateY(-1px);
  }
  #settings-btn.settings-pill .settings-pill-icon {
    font-size: 18px; line-height: 1; opacity: 0.9;
  }
  /* Mobile (touch OR narrow window): settings collapses to icon-only
     so the cog reads as a glyph. Desktop keeps the full label. */
  @media (pointer: coarse), (max-width: 540px) {
    #settings-btn.settings-pill .settings-label { display: none; }
    #settings-btn.settings-pill { padding: 10px 12px; }
  }


  /* ─── Settings modal ─────────────────────────────────────────────────── */
  #settings-modal {
    position: fixed; inset: 0;
    display: none;
    z-index: 1100;
  }
  #settings-modal.visible { display: block; }
  #settings-modal .settings-backdrop {
    position: absolute; inset: 0;
    /* rc159 — opaque settings backdrop. */
    background: var(--panel-bg-2, #0e1426);
  }
  #settings-modal .settings-card {
    position: relative;
    margin: 7vh auto 0;
    max-width: 560px; width: calc(100% - 32px);
    max-height: calc(100vh - 14vh);
    display: flex; flex-direction: column;
    /* rc159 — opaque settings card. */
    background: var(--panel-bg, #131a2e);
    border: 1px solid var(--card-border, #2a3454);
    border-radius: 16px;
    color: var(--text, #e6ebf6);
    box-shadow: 0 30px 80px rgba(0,0,0,0.55);
    overflow: hidden;
    padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  }
  #settings-modal .settings-head {
    display: flex; align-items: center; justify-content: space-between;
    /* Top padding includes the system status-bar inset so the SETTINGS
       title and × close button don't butt against the clock/battery on
       Android 15 edge-to-edge (targetSdk 35) — without this, the
       full-screen mobile sheet (margin-top: 0 below 540px) puts the
       head directly under the OS status bar. Desktop reports the env()
       as 0 so this is a no-op there. +16dp on top (and matching +16dp
       on the card's padding-bottom further down) so the modal isn't
       hugging the very edge on Android with the status bar hidden. */
    padding: calc(34px + env(safe-area-inset-top, 0px)) 22px 18px;
    border-bottom: 1px solid rgba(255,255,255,0.06);
  }
  #settings-modal .settings-head h2 {
    margin: 0;
    font-size: 18px; letter-spacing: 1.4px; font-weight: 800;
  }
  #settings-modal .settings-close {
    background: none; border: 0; color: inherit;
    font-size: 26px; line-height: 1; cursor: pointer; padding: 4px 8px;
    opacity: 0.8;
  }
  #settings-modal .settings-close:hover { opacity: 1; }
  #settings-modal .settings-body {
    padding: 18px 22px 22px;
    display: flex; flex-direction: column; gap: 22px;
    /* Scroll the body when content exceeds the card height. min-height:0
       is needed so flex doesn't force the body to its content height and
       defeat the parent's max-height cap. */
    overflow-y: auto;
    min-height: 0;
  }
  #settings-modal .settings-section {
    display: flex; flex-direction: column; gap: 10px;
  }
  #settings-modal .settings-section h3 {
    margin: 0;
    font-size: 11.5px; letter-spacing: 1.6px; font-weight: 700;
    color: rgba(255,255,255,0.55);
    text-transform: uppercase;
  }
  #settings-modal .settings-row { display: flex; align-items: center; gap: 12px; }
  #settings-modal .settings-row > label {
    min-width: 92px;
    font-size: 13px; font-weight: 600;
    color: rgba(255,255,255,0.78);
  }
  /* Language picker — single dropdown rather than a 14-tile grid.
     Native <select> on Capacitor opens the OS picker which renders
     CJK/Arabic endonyms via the system font without needing our
     in-app webfont stack. ~250px shorter than the previous grid. */
  #settings-modal .settings-lang-select {
    flex: 1; min-width: 0;
    display: block; width: 100%;
    padding: 10px 38px 10px 14px;
    background: rgba(255,255,255,0.06);
    border: 1px solid rgba(255,255,255,0.14);
    border-radius: 10px;
    color: inherit;
    font: inherit; font-size: 14px; font-weight: 600;
    line-height: 1.2;
    cursor: pointer;
    /* Strip the OS default arrow and render our own caret so the picker
       reads as part of our settings card aesthetic, not as a stock UI
       chrome control. */
    appearance: none; -webkit-appearance: none; -moz-appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'><path d='M1 1.5L6 6.5L11 1.5' stroke='%23c6cee0' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-repeat: no-repeat;
    background-position: right 14px center;
    transition: background 0.14s, border-color 0.14s;
  }
  #settings-modal .settings-lang-select:hover,
  #settings-modal .settings-lang-select:focus {
    background-color: rgba(126,226,255,0.10);
    border-color: rgba(126,226,255,0.45);
    outline: none;
  }
  /* The <option> children render in the OS native picker — our CSS
     barely applies, but set a sensible color/background for the few
     browsers (Firefox desktop) that respect it. */
  #settings-modal .settings-lang-select option {
    background: #1a1f30;
    color: #f4f6fb;
  }
  #settings-modal .settings-cta {
    align-self: flex-start;
    display: inline-flex; align-items: center; gap: 8px;
    padding: 9px 14px;
    background: rgba(126,226,255,0.14);
    border: 1px solid rgba(126,226,255,0.4);
    border-radius: 10px;
    color: #d8f1ff;
    font: inherit; font-size: 13.5px; font-weight: 600;
    cursor: pointer;
    transition: background 0.14s, border-color 0.14s, transform 0.12s;
  }
  #settings-modal .settings-cta:hover {
    background: rgba(126,226,255,0.22);
    border-color: rgba(126,226,255,0.65);
    transform: translateY(-1px);
  }
  @media (max-width: 540px) {
    #settings-modal .settings-card { margin-top: 0; border-radius: 0; min-height: 100vh; }
    #settings-modal .settings-row { flex-wrap: wrap; }
    #settings-modal .settings-row > label { min-width: 0; flex: 1 0 100%; }
    #settings-modal .settings-cta { align-self: stretch; justify-content: center; }
  }

/* Spec 2a — relic shop async-purchase feedback */
.relic-card-buy.purchasing {
  opacity: 0.6;
  pointer-events: none;
  position: relative;
}
.relic-card-buy.purchasing::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 16px; height: 16px;
  margin: -8px 0 0 -8px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin-360 0.6s linear infinite;
}
.relic-shop-toast {
  position: absolute;
  bottom: 90px; left: 50%;
  transform: translateX(-50%) translateY(8px);
  padding: 10px 18px;
  background: rgba(75, 50, 37, 0.92);
  color: #f0e0c8;
  font-size: 13px;
  border-radius: 8px;
  border: 1px solid rgba(180, 140, 100, 0.3);
  pointer-events: none;
  opacity: 0;
  transition: opacity 240ms ease, transform 240ms ease;
  z-index: 50;
}
/* "Verifying purchase…" overlay. Shown for the duration of the
   /api/iap/google-verify roundtrip (typically 1-3s, longer on poor
   networks). Built lazily by src/iap.js _showVerifying() and reused.
   Non-dismissable — only the verify finally hook closes it. z-index
   sits below the celebration modal (10000) but above every other
   modal so it can show on top of the IAP modal even before that
   modal is closed. */
.iap-verify-modal {
  position: fixed; inset: 0;
  display: none;
  align-items: center; justify-content: center;
  z-index: 9990;
  padding: 24px;
  background: rgba(8, 6, 24, 0.78);
}
.iap-verify-modal.visible { display: flex; }
.iap-verify-card {
  display: flex; flex-direction: column;
  align-items: center; gap: 14px;
  padding: 26px 36px 22px;
  background: linear-gradient(180deg, rgba(20, 24, 40, 0.96), rgba(14, 18, 32, 0.96));
  border: 1px solid rgba(126, 226, 255, 0.32);
  border-radius: 18px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55),
              inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.iap-verify-spinner {
  width: 38px; height: 38px;
  border: 3px solid rgba(126, 226, 255, 0.18);
  border-top-color: rgba(126, 226, 255, 0.85);
  border-radius: 50%;
  animation: spin-360 0.95s linear infinite;
  filter: drop-shadow(0 0 6px rgba(126, 226, 255, 0.42));
}
.iap-verify-text {
  font-size: 14px; font-weight: 600;
  color: #d8f4ff;
  letter-spacing: 0.4px;
  text-shadow: 0 0 6px rgba(126, 226, 255, 0.32);
}

/* Celebratory post-purchase modal. Lazily built and mounted on body by
   src/iap.js _showPurchaseSuccess(). Sits above every other modal
   (z-index 10000) since it's the terminal confirmation of a successful
   purchase. The card uses the BEST-VALUE gold-rim + lilac wash
   treatment that the IAP T5 tile uses, so the celebration reads as
   "you got the prize." */
.iap-success-modal {
  position: fixed; inset: 0;
  display: none;
  align-items: center; justify-content: center;
  z-index: 10000;
  padding: 24px;
}
.iap-success-modal.visible { display: flex; }
.iap-success-backdrop {
  position: absolute; inset: 0;
  /* Premium clean stage. Base layer is 100% opaque so the compositor
     can short-circuit ALL painting of the layers below — anything
     behind the popup (relic shop, canvas, landing photons) doesn't
     need to be sampled, blurred, or repainted regardless of what
     it's doing. A small radial halo on top adds the gold/purple
     glow at the artwork anchor point. */
  background:
    radial-gradient(ellipse 60% 50% at 50% 38%,
      rgba(120, 80, 200, 0.32) 0%,
      rgba(60, 30, 110, 0.18) 30%,
      transparent 65%),
    #08061a;
  /* Composite to its own GPU layer so the static background doesn't
     re-rasterise when the rays/bob animations repaint above it. */
  will-change: opacity;
  transform: translateZ(0);
}
.iap-success-card {
  position: relative;
  max-width: 360px; width: 100%;
  padding: 28px 24px 22px;
  text-align: center;
  background: linear-gradient(135deg, rgba(255, 142, 211, 0.12), rgba(196, 124, 255, 0.20));
  border: 1px solid rgba(255, 215, 102, 0.62);
  border-radius: 22px;
  box-shadow:
    0 0 36px rgba(196, 124, 255, 0.32),
    0 24px 72px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.12);
  color: var(--text);
  animation: iap-success-pop 480ms cubic-bezier(0.2, 0.9, 0.2, 1.05) both;
  overflow: hidden;
}
/* On touch devices the popup goes full-screen — vault artwork is the
   moment, so it gets to fill the device. Drop the modal's 24px outer
   padding, make the card full-bleed (no border, no rounded corners,
   no max-width), center content vertically, scale the SKU image +40%
   (132→185, wrap 168→235, flare/rays 280→392). Confetti layer keeps
   pointer-events:none and stays as-is — it spawns relative to the
   modal, not the card. Card background dropped to transparent —
   premium clean: the backdrop's deep-black + halo is the whole stage,
   no competing lavender wash. */
body.touch:not(.tablet) .iap-success-modal { padding: 0; }
body.touch:not(.tablet) .iap-success-card {
  /* dvh adapts to actual visible area — earlier this used 100lvh
     for stability but on iOS Safari that extended behind the URL
     bar and hid the bottom CTA. */
  width: 100vw; height: 100vh; height: 100dvh;
  max-width: none;
  margin: 0;
  padding: calc(env(safe-area-inset-top, 0px) + 28px)
           calc(env(safe-area-inset-right, 0px) + 24px)
           calc(env(safe-area-inset-bottom, 0px) + 22px)
           calc(env(safe-area-inset-left, 0px) + 24px);
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
}
/* Full-screen real estate — let the loot dominate. Vault doubles to
   370px (was 185), wrap to 470px to give the rays room. The flare +
   rays scale up too but cap at min(640px, 92vw) so they don't overflow
   on narrow phones; vw guard keeps the rays inside the screen. */
body.touch:not(.tablet) .iap-success-art-wrap {
  width: 470px; height: 470px;
  max-width: 92vw; max-height: 92vw;
  margin: 8px auto 22px;
}
body.touch:not(.tablet) .iap-success-art {
  width: 370px; height: 370px;
  max-width: 78vw; max-height: 78vw;
}
body.touch:not(.tablet) .iap-success-flare,
body.touch:not(.tablet) .iap-success-rays {
  width: 640px; height: 640px;
  max-width: 110vw; max-height: 110vw;
}
/* Artwork wrapper — the SKU image is the new hero element. Holds the
   rotating rays + gold flare behind it (pseudo-elements live inside
   .iap-success-art-wrap so they anchor on the artwork, not on the
   card). pointer-events:none on rays + flare so taps still hit the
   button. */
.iap-success-art-wrap {
  position: relative;
  width: 168px; height: 168px;
  margin: 6px auto 14px;
  display: flex; align-items: center; justify-content: center;
}
.iap-success-art {
  position: relative;
  width: 132px; height: 132px;
  object-fit: contain;
  filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.65))
          drop-shadow(0 0 22px rgba(184, 155, 255, 0.40));
  /* Two-stage animation: pop in (720ms with overshoot), then continuous
     gentle float (4.6s sine ease) — slower + smaller throw than the
     prior bob, so the loot reads as "premium suspended" rather than
     bobbing. The pop has fill-mode: both to hold the final transform
     until the float takes over via animation chaining. */
  animation:
    iap-success-art-pop 720ms cubic-bezier(0.2, 1.4, 0.4, 1) 120ms both,
    iap-success-art-bob 4.6s ease-in-out 920ms infinite;
  z-index: 2;
  pointer-events: none;
  user-select: none;
  /* Promote to its own GPU layer so the bob keyframe is pure compositor
     work — without this the drop-shadow re-rasterises the artwork
     each frame on Android Chrome, which is the source of the jank. */
  will-change: transform;
}
/* Gold radial flare anchored on the artwork — fades in then sustains. */
.iap-success-flare {
  position: absolute;
  top: 50%; left: 50%;
  width: 280px; height: 280px;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(255, 215, 102, 0.42) 0%, rgba(255, 142, 211, 0.22) 32%, transparent 70%);
  pointer-events: none;
  z-index: 0;
  animation: iap-success-flare 1.2s ease-out both;
  will-change: opacity, transform;
}
/* Eyebrow — premium "presented to you" treatment. On desktop it stays
   compact; on touch it triples to become the de-facto screen title.
   Hairline divider flanks (CSS pseudos with gold gradient fade) frame
   the words like a proper title cartouche. */
.iap-success-eyebrow {
  position: relative;
  display: inline-flex; align-items: center;
  gap: 14px;
  font-size: 14px; font-weight: 800;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: #ffe9aa;
  text-shadow: 0 0 16px rgba(255, 215, 102, 0.55);
  margin-bottom: 8px;
}
.iap-success-eyebrow::before,
.iap-success-eyebrow::after {
  content: '';
  display: inline-block;
  width: 32px; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 215, 102, 0.65), transparent);
}
/* Touch: title becomes the moment. Tripled font, longer flanks, more
   shadow lift. Wraps gracefully on narrow phones via flex-wrap so the
   side hairlines collapse rather than overflow. */
body.touch:not(.tablet) .iap-success-eyebrow {
  font-size: 38px; letter-spacing: 4.5px;
  gap: 18px;
  margin-bottom: 14px;
  flex-wrap: wrap; justify-content: center;
  text-shadow:
    0 0 28px rgba(255, 215, 102, 0.75),
    0 4px 0 rgba(0, 0, 0, 0.35);
}
body.touch:not(.tablet) .iap-success-eyebrow::before,
body.touch:not(.tablet) .iap-success-eyebrow::after {
  width: 64px; height: 2px;
}
.iap-success-amount {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  gap: 6px;
  font-size: 36px; font-weight: 900;
  letter-spacing: 0.5px;
  line-height: 1;
  color: #ffffff;
  text-shadow: 0 0 24px rgba(255, 142, 211, 0.55), 0 4px 0 rgba(0, 0, 0, 0.30);
  margin-bottom: 6px;
  z-index: 2;
}
.iap-success-plus {
  color: #ffe9aa;
  /* Subtle pop-in for the + sign */
  animation: iap-success-plus-pop 460ms cubic-bezier(0.2, 0.9, 0.2, 1.05) 80ms both;
}
.iap-success-count {
  /* Tabular nums so the digits don't shift width during the roll-up
     animation — keeps the layout stable while the counter ticks. */
  font-variant-numeric: tabular-nums;
}
.iap-success-gem {
  display: inline-flex; align-items: center;
  width: 1em; height: 1em;
  filter: drop-shadow(0 0 10px rgba(126, 226, 255, 0.65));
  /* Hero entrance — over-scale + slight rotation, settles back. Fires
     each time .visible is set on the modal. */
  animation: iap-success-gem-pop 700ms cubic-bezier(0.2, 1.4, 0.4, 1) 140ms both;
  transform-origin: center center;
}
.iap-success-gem svg {
  width: 100%; height: 100%; display: block;
}
.iap-success-title {
  position: relative;
  font-size: 20px; font-weight: 800;
  letter-spacing: 0.5px;
  color: #f6f8fe;
  margin: 0 0 6px;
}
.iap-success-blurb {
  position: relative;
  font-size: 13.5px; line-height: 1.45;
  color: var(--text-dim);
  margin: 0 0 22px;
}
/* Signature polygon-shooter gradient: gold → rose → lilac. Same palette
   the gameover SHARE button + landing PLAY pill both use, so the
   success CTA reads as part of the celebratory family. Static — no
   pulse, no glow expansion; the gradient itself carries the visual
   weight without animation noise. */
.iap-success-dismiss {
  position: relative;
  width: 100%;
  padding: 16px 24px;
  background: linear-gradient(135deg,
    #ffd36e 0%,
    #ff9ec4 45%,
    #b89bff 100%);
  border: 1px solid rgba(255, 255, 255, 0.36);
  border-radius: var(--radius-pill);
  color: #1a1530;
  font: inherit;
  font-size: 15px; font-weight: 800;
  letter-spacing: 1.4px;
  cursor: pointer;
  box-shadow:
    0 10px 28px rgba(255, 158, 196, 0.45),
    inset 0 1px 0 rgba(255, 255, 255, 0.55);
  transition: transform 0.12s ease, filter 0.12s ease, box-shadow 0.25s ease;
}
.iap-success-dismiss:hover {
  transform: translateY(-2px);
  filter: brightness(1.08) saturate(1.1);
}
.iap-success-dismiss:active {
  transform: translateY(0);
  filter: brightness(0.94);
}
/* Touch: chunkier button on big-screen layout — proportional to the
   bigger eyebrow + larger artwork. */
body.touch:not(.tablet) .iap-success-dismiss {
  padding: 18px 28px;
  font-size: 16px; letter-spacing: 1.6px;
  max-width: 380px;
  margin: 0 auto;
}
@keyframes iap-success-pop {
  0%   { transform: scale(0.7); opacity: 0; }
  60%  { transform: scale(1.04); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes iap-success-flare {
  0%   { opacity: 0; transform: translateX(-50%) scale(0.4); }
  40%  { opacity: 1; transform: translateX(-50%) scale(1.1); }
  100% { opacity: 0.85; transform: translateX(-50%) scale(1); }
}

/* Rotating light rays behind the SKU artwork — anchors the celebration on
   the just-purchased item. Conic gradient with alternating soft gold /
   transparent stripes, blurred + masked to a circle so it reads as a
   radiant aura. Positioned absolutely INSIDE .iap-success-art-wrap so
   the rays travel with the artwork, not the card. */
.iap-success-rays {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 280px; height: 280px;
  pointer-events: none;
  z-index: 0;
  background: conic-gradient(
    from 0deg,
    transparent 0deg, rgba(255, 215, 102, 0.22) 8deg, transparent 16deg,
    transparent 36deg, rgba(255, 215, 102, 0.18) 44deg, transparent 52deg,
    transparent 72deg, rgba(255, 215, 102, 0.24) 80deg, transparent 88deg,
    transparent 108deg, rgba(255, 215, 102, 0.16) 116deg, transparent 124deg,
    transparent 144deg, rgba(255, 215, 102, 0.20) 152deg, transparent 160deg,
    transparent 180deg, rgba(255, 215, 102, 0.18) 188deg, transparent 196deg,
    transparent 216deg, rgba(255, 215, 102, 0.22) 224deg, transparent 232deg,
    transparent 252deg, rgba(255, 215, 102, 0.16) 260deg, transparent 268deg,
    transparent 288deg, rgba(255, 215, 102, 0.20) 296deg, transparent 304deg,
    transparent 324deg, rgba(255, 215, 102, 0.18) 332deg, transparent 340deg,
    transparent 360deg
  );
  /* No filter:blur — combined with mask + conic-gradient + transform
     animation it forces a per-frame re-rasterise on Android Chrome
     even with will-change:transform. The conic stripes alone read as
     soft rays once the mask fades them at the inner ring; explicit
     blur was redundant. */
  mask: radial-gradient(circle at center, transparent 18%, #000 32%, #000 50%, transparent 80%);
  -webkit-mask: radial-gradient(circle at center, transparent 18%, #000 32%, #000 50%, transparent 80%);
  opacity: 0;
  animation:
    iap-success-rays-in 1200ms ease-out 240ms both,
    iap-success-rays 14s linear 240ms infinite;
  /* Critical for smoothness on Android Chrome — without this hint the
     conic-gradient + mask layer repaints on the main thread each
     rotation frame, which is the dominant source of jank. */
  will-change: transform;
}
@keyframes iap-success-rays {
  to { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes iap-success-rays-in {
  0%   { opacity: 0; transform: translate(-50%, -50%) rotate(0deg); }
  100% { opacity: 1; transform: translate(-50%, -50%) rotate(0deg); }
}

/* SKU artwork hero entrance — scales in from 0 with overshoot + slight
   rotation, settles to rest. Triggers each time .visible is set. */
@keyframes iap-success-art-pop {
  0%   { transform: scale(0) rotate(-12deg); opacity: 0; }
  55%  { transform: scale(1.18) rotate(6deg); opacity: 1; }
  80%  { transform: scale(0.96) rotate(-2deg); }
  100% { transform: scale(1) rotate(0); }
}
/* Continuous gentle float — keeps the loot "alive" but not cartoonish.
   Smaller throw (4px vs the prior 6px) + slower cycle (4.6s vs 3.4s)
   reads as "premium suspended in air" rather than bobbing. Sway
   reduced to 0.5° so it doesn't feel mechanical. */
@keyframes iap-success-art-bob {
  0%, 100% { transform: translateY(0)    rotate(0deg);   }
  50%      { transform: translateY(-4px) rotate(0.5deg); }
}
/* Counter "land" pulse — fires once when _animateCount hits target via
   adding the .land class. Brief scale punch + golden text flash so the
   number lands with a payoff instead of just stopping. */
.iap-success-amount.land {
  animation: iap-success-amount-land 540ms cubic-bezier(0.2, 1.4, 0.4, 1) both;
}
@keyframes iap-success-amount-land {
  0%   { transform: scale(1);    text-shadow: 0 0 24px rgba(255, 142, 211, 0.55), 0 4px 0 rgba(0, 0, 0, 0.30); }
  35%  { transform: scale(1.12); text-shadow: 0 0 36px rgba(255, 215, 102, 0.95), 0 0 18px rgba(255, 142, 211, 0.85), 0 4px 0 rgba(0, 0, 0, 0.30); }
  70%  { transform: scale(0.98); }
  100% { transform: scale(1);    text-shadow: 0 0 24px rgba(255, 142, 211, 0.55), 0 4px 0 rgba(0, 0, 0, 0.30); }
}

/* Cyan-gem entrance for the inline ◆ next to the count — same shape as
   the artwork pop but tighter, fires slightly later so the eye lands on
   the artwork first then notices the count beneath. */
@keyframes iap-success-gem-pop {
  0%   { transform: scale(0) rotate(-30deg); opacity: 0; }
  55%  { transform: scale(1.45) rotate(15deg); opacity: 1; }
  80%  { transform: scale(0.92) rotate(-4deg); }
  100% { transform: scale(1) rotate(0); }
}
@keyframes iap-success-plus-pop {
  0%   { transform: scale(0); opacity: 0; }
  60%  { transform: scale(1.3); opacity: 1; }
  100% { transform: scale(1); }
}

/* Twinkle stars — small white dots scattered around the card edges that
   flicker on a staggered cycle. Each <span> gets a position via
   nth-child + the same animation with different delays to feel random. */
.iap-success-stars {
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 1;
}
.iap-success-stars span {
  position: absolute;
  width: 8px; height: 8px;
  background: #ffffff;
  border-radius: 50%;
  box-shadow: 0 0 8px 2px rgba(255, 255, 255, 0.85);
  opacity: 0;
  animation: iap-success-twinkle 1.8s ease-in-out infinite both;
}
.iap-success-stars span:nth-child(1) { top:  8%; left:  12%; animation-delay: 0.1s; transform: scale(0.7); }
.iap-success-stars span:nth-child(2) { top: 14%; right: 10%; animation-delay: 0.4s; transform: scale(0.5); }
.iap-success-stars span:nth-child(3) { top: 38%; left:   6%; animation-delay: 0.7s; transform: scale(0.8); }
.iap-success-stars span:nth-child(4) { top: 42%; right:  8%; animation-delay: 1.0s; transform: scale(0.6); }
.iap-success-stars span:nth-child(5) { top: 62%; left:  14%; animation-delay: 1.3s; transform: scale(0.5); }
.iap-success-stars span:nth-child(6) { top: 66%; right: 16%; animation-delay: 0.2s; transform: scale(0.7); }
.iap-success-stars span:nth-child(7) { bottom: 20%; left:  10%; animation-delay: 0.55s; transform: scale(0.5); }
.iap-success-stars span:nth-child(8) { bottom: 24%; right: 12%; animation-delay: 0.9s; transform: scale(0.6); }
@keyframes iap-success-twinkle {
  0%, 100% { opacity: 0; transform: scale(0.4); }
  50%      { opacity: 1; transform: scale(1.2); }
}


/* Tier-tinted aura on the artwork wrap — JS sets data-tier on the modal
   so the bigger packs read as bigger moments. T1-T2 cyan, T3-T4 lilac,
   T5-T6 gold/iridescent. The flare's radial gradient and the rays'
   conic gradient both pull from --tier-aura-* so the whole hero
   palette retunes per SKU. */
.iap-success-modal[data-tier="t1"] { --tier-aura-1: rgba(126, 226, 255, 0.50); --tier-aura-2: rgba(159, 223, 255, 0.30); }
.iap-success-modal[data-tier="t2"] { --tier-aura-1: rgba(159, 223, 255, 0.55); --tier-aura-2: rgba(184, 155, 255, 0.32); }
.iap-success-modal[data-tier="t3"] { --tier-aura-1: rgba(184, 155, 255, 0.55); --tier-aura-2: rgba(255, 142, 211, 0.32); }
.iap-success-modal[data-tier="t4"] { --tier-aura-1: rgba(255, 142, 211, 0.55); --tier-aura-2: rgba(255, 215, 102, 0.35); }
.iap-success-modal[data-tier="t5"] { --tier-aura-1: rgba(255, 215, 102, 0.62); --tier-aura-2: rgba(255, 142, 211, 0.40); }
.iap-success-modal[data-tier="t6"] { --tier-aura-1: rgba(255, 215, 102, 0.78); --tier-aura-2: rgba(255, 180, 240, 0.50); }
.iap-success-modal[data-tier] .iap-success-flare {
  background: radial-gradient(circle,
    var(--tier-aura-1, rgba(255, 215, 102, 0.42)) 0%,
    var(--tier-aura-2, rgba(255, 142, 211, 0.22)) 32%,
    transparent 70%);
}

@media (prefers-reduced-motion: reduce) {
  .iap-success-card,
  .iap-success-flare,
  .iap-success-rays,
  .iap-success-art,
  .iap-success-gem,
  .iap-success-plus,
  .iap-success-amount.land,
  .iap-success-stars span { animation: none; }
}

