/* Pantheon TCG — faithful recreation of the Claude Design prototype.
   Design medium was HTML/CSS/JS; this reproduces the visual output exactly.
   Per-card gradients, glows and holo backgrounds are set inline from JS
   because they are data-driven; structural/reused styling lives here. */

:root {
  /* Default theme = aegean (the prototype's default), applied to #app via JS.
     Fallbacks below mirror the ivory values used in the original inline styles. */
  --bg: #f2efe9;
  --panel: #fbfaf7;
  --ink: #1c1a16;
  --mut: #857f73;
  --line: rgba(28, 26, 22, .14);
  --acc: #9c7c2e;
  --chip: rgba(28, 26, 22, .07);
  /* Per-screen glow backdrop (extends Oracle's Trial's .oracle-stage-*
     effect game-wide) — admin-configurable via the "theme" GAME_SETTINGS
     group, applied over these fallbacks at boot by applyGlowTheme() in
     app.js. Pre-converted to rgba at the same fixed alpha JS uses, so a
     pre-JS/JS-disabled paint still looks intentional, matching how --bg
     etc. above already have literal fallback values. */
  --glow-packs: rgba(201, 161, 60, .30);
  --glow-summon: rgba(212, 166, 67, .30);
  --glow-binder: rgba(59, 110, 165, .30);
  --glow-market: rgba(47, 143, 126, .30);
  --glow-deck: rgba(90, 125, 140, .30);
  --glow-duel: rgba(181, 66, 58, .30);
  --glow-arcade: rgba(138, 90, 51, .30);
  --glow-leaderboard: rgba(185, 138, 58, .30);
  --glow-profile: rgba(63, 125, 84, .30);
}

* { box-sizing: border-box; }

body {
  margin: 0;
}

#app {
  min-height: 100vh;
  background-color: var(--bg);
  background-image:
    radial-gradient(ellipse at 50% 0%, var(--glow-packs), transparent 62%),
    url('uploads/page-bg.png');
  background-repeat: no-repeat;
  background-position: left top;
  background-size: contain;
  background-attachment: fixed;
  /* One blend-mode value per background-image layer, positionally matched:
     `normal` for the glow gradient (1st layer) so its hue is never washed
     out, `luminosity` for the photo (2nd layer) — the original single-layer
     treatment, unchanged for the photo itself. */
  background-blend-mode: normal, luminosity;
  color: var(--ink);
  font-family: Helvetica, Arial, sans-serif;
  transition: background-color .3s, color .3s;
}
/* Per-screen glow class, toggled on #app by render() (see GLOW_VIEW_CLASS
   in app.js) — each swaps just the gradient's color variable, keeping the
   same two-layer shape and photo choice as the base rule above except
   where a screen also swaps the photo itself (Arcade/Market, below). */
#app.view-summon      { background-image: radial-gradient(ellipse at 50% 0%, var(--glow-summon), transparent 62%),      url('uploads/page-bg.png'); }
#app.view-binder      { background-image: radial-gradient(ellipse at 50% 0%, var(--glow-binder), transparent 62%),      url('uploads/page-bg.png'); }
#app.view-deck        { background-image: radial-gradient(ellipse at 50% 0%, var(--glow-deck), transparent 62%),        url('uploads/page-bg.png'); }
#app.view-duel        { background-image: radial-gradient(ellipse at 50% 0%, var(--glow-duel), transparent 62%),        url('uploads/page-bg.png'); }
#app.view-leaderboard { background-image: radial-gradient(ellipse at 50% 0%, var(--glow-leaderboard), transparent 62%), url('uploads/page-bg.png'); }
#app.view-profile     { background-image: radial-gradient(ellipse at 50% 0%, var(--glow-profile), transparent 62%),     url('uploads/page-bg.png'); }
/* Arcade and Market share the page-bg-arcade.png photo (previously via a
   single shared .view-arcade class) — now split into two classes so each
   still gets its own distinct glow color layered over that same photo. */
#app.view-arcade { background-image: radial-gradient(ellipse at 50% 0%, var(--glow-arcade), transparent 62%), url('uploads/page-bg-arcade.png'); }
#app.view-market { background-image: radial-gradient(ellipse at 50% 0%, var(--glow-market), transparent 62%), url('uploads/page-bg-arcade.png'); }
#app.view-achievements { background-image: radial-gradient(ellipse at 50% 0%, var(--glow-achievements), transparent 62%), url('uploads/page-bg.png'); }

/* Mobile portrait: 'contain' shrinks the (landscape-shaped) background art
   down to a sliver at narrow/tall aspect ratios. Zooming in reads better
   than the whole image at a fraction of the width — applies to every page
   (default and .view-arcade both use #app's background-size). Oracle's
   Trial's five stage backdrops get the same treatment — see the dedicated
   media query right after their `background` shorthand declarations
   (styles.css ~3369-3393), NOT here: a shorthand declared later in the
   file always wins over longhand overrides declared earlier, regardless
   of which one is inside a media query, so the mobile override for those
   five selectors has to physically sit after their shorthand rule. */
@media (orientation: portrait) and (max-width: 680px) {
  #app { background-size: 200%; }
  /* Frees up navbar width on narrow portrait phones — the collected-count
     chip is redundant with the same number shown on the Binder screen. */
  .user-box .count { display: none; }
}

@keyframes ptcg-sweep { 0% { background-position: 220% 0; } 100% { background-position: -120% 0; } }
@keyframes ptcg-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-9px); } }
@keyframes ptcg-in { from { opacity: 0; } to { opacity: 1; } }

/* ---- Header ---- */
.header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px clamp(16px, 4vw, 40px);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 50;
  background: transparent;
  box-shadow: none;
  transition: background .25s ease, box-shadow .25s ease, backdrop-filter .25s ease;
}
/* Applied via JS once the header has actually scrolled under the top edge —
   only then does it need to separate itself from the content behind it.
   Fixed dark neutral (near #app's own --bg dark navy) rather than
   color-mix(var(--panel), ...) — with the per-screen radial glows now
   sitting behind it on every page (see #app.view-* further down), the
   theme-tinted panel color blended with the blurred glow underneath read
   as a muddy, inconsistent tint per screen. A near-black neutral at LOW
   opacity keeps it a true glass effect (still shows a soft hint of the
   page through the blur, matching the rest of this game's dark, moody
   design) instead of the much-too-opaque, much-too-light plate an earlier
   attempt at this used, which looked out of place against everything
   else. */
.header.stuck {
  background: rgba(13, 22, 38, .55);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 4px 18px rgba(0, 0, 0, .28);
}
.brand {
  font-family: Cinzel, serif;
  font-weight: 700;
  font-size: 18px;
  letter-spacing: .24em;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
.alpha-badge {
  font-family: system-ui, sans-serif;
  font-weight: 700;
  font-size: 7px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--acc);
  border: 1px solid var(--acc);
  border-radius: 999px;
  padding: 1px 5px;
  line-height: 1.4;
}
.nav { display: flex; gap: 6px; margin-left: auto; }
.tab {
  padding: 8px 16px;
  border-radius: 999px;
  border: none;
  color: inherit;
  font-size: 13px;
  letter-spacing: .05em;
  cursor: pointer;
  background: transparent;
}
.tab:hover { opacity: .75; }
.tab.active { background: var(--chip); }
/* "Play" nav dropdown (Decks/Arcade/Duel) — see shell(). Same collapsible
   behavior on both desktop and the mobile burger menu (the mobile
   @media block below repositions the floating panel to sit in-flow inside
   the already-vertical burger list instead of floating over it). */
.tab-dropdown { position: relative; }
.tab-dropdown-trigger { display: inline-flex; align-items: center; gap: 4px; }
.tab-dropdown-caret { display: inline-flex; flex: none; }
/* CHEVRON_SVG points right at rest — rotated +90deg here to point down
   (closed) and -90deg (i.e. +180deg from the down state) to point up
   (open), rather than reusing the match-pile-caret's right->down rotation
   verbatim. Slightly slower + eased than the .18s micro-icon default since
   this one accompanies the panel's own fade/slide, not an isolated flip. */
.tab-dropdown-caret .pile-caret-ico { width: 12px; height: 12px; transition: transform .22s cubic-bezier(.4, 0, .2, 1); transform: rotate(90deg); }
.tab-dropdown-caret.open .pile-caret-ico { transform: rotate(-90deg); }
/* Panel: dark inset relative to the header (not the same tone as the
   trigger/hover chip) so the grouped items read as a distinct sunken tray,
   not just more of the same bar. Its own children get a smaller radius
   than the tray's — a nested "pill in a rounded box" otherwise clips
   oddly at the tray's corners. Fades + slides open/closed instead of a
   hard display:none/flex snap: visibility+opacity+transform all
   transition, with the visibility flip delayed on the way out so it stays
   interactive/paintable until the fade finishes. */
.tab-dropdown-panel {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  background: color-mix(in srgb, var(--bg) 70%, black);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: 0 14px 34px rgba(0, 0, 0, .4);
  z-index: 40;
  min-width: 140px;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  transition: opacity .16s ease, transform .16s ease, visibility 0s linear .16s;
}
.tab-dropdown.open .tab-dropdown-panel {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity .16s ease, transform .16s ease;
}
.tab-dropdown-panel .tab { width: 100%; text-align: left; border-radius: 6px; }
.tab-dropdown-panel .tab.active { background: color-mix(in srgb, var(--chip) 65%, white 8%); }
/* Small yellow dot on the Summon tab when today's free summon is unclaimed. */
.free-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-left: 5px;
  border-radius: 50%;
  background: #ffcf3f;
  box-shadow: 0 0 6px rgba(255, 207, 63, .85);
  vertical-align: middle;
}
/* "Recently active" indicator — Leaderboard rows + Profile header. A
   last-seen approximation (no cron/WebSockets on this hosting, see
   GAME_SETTINGS' online_threshold_minutes), not real-time — not refreshed
   while a viewer stays on the page, only reflects the moment this page's
   data was fetched. Same sizing/placement as .free-dot above, but green
   (not yellow) so the two are never visually confused, and gently
   pulsing so it reads as "recently," not a flat static fact. */
.online-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  margin-left: 6px;
  border-radius: 50%;
  background: #4fd67a;
  vertical-align: middle;
  animation: online-dot-pulse 2s ease-in-out infinite;
}
@keyframes online-dot-pulse {
  0%, 100% { box-shadow: 0 0 4px 0 rgba(79, 214, 122, .55); }
  50%      { box-shadow: 0 0 9px 3px rgba(79, 214, 122, .9); }
}
@media (prefers-reduced-motion: reduce) {
  .online-dot { animation: none; box-shadow: 0 0 6px rgba(79, 214, 122, .85); }
}
.count { font-size: 12px; color: var(--mut); letter-spacing: .08em; }
.count b { color: var(--acc); font-weight: 700; }

/* user box (right of the header) */
.user-box { display: flex; align-items: center; gap: 12px; margin-left: auto; }
.settings-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 26px;
  height: 26px;
  padding: 0;
  border: none;
  background: none;
  color: var(--mut);
  cursor: pointer;
}
.settings-toggle:hover { color: var(--acc); }
.settings-ico { width: 18px; height: 18px; }
.speaker-ico { width: 18px; height: 18px; }
.note-ico { width: 18px; height: 18px; }
/* Settings + Notifications read as one "utility icons" cluster — tighter
   gap than .user-box's own 12px between its other, unrelated children
   (Faith chip, card count, avatar). */
.icon-group { display: flex; align-items: center; gap: 6px; }

/* Notification bell (see notifSidebarHtml() in app.js) — same box model as
   .settings-toggle beside it, plus position:relative so the unread badge
   can anchor to the button itself. */
.notif-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 26px;
  height: 26px;
  padding: 0;
  border: none;
  background: none;
  color: var(--mut);
  cursor: pointer;
}
.notif-toggle:hover { color: var(--acc); }
.bell-ico { width: 18px; height: 18px; }
/* Solid gold fill (not .alpha-badge's outline treatment) so it reads as a
   count to glance at, not a label to read — same gold as the Faith/mana
   accents (#e6c15c), never the legendary-rarity red, to avoid implying
   "legendary" semantics on an unrelated counter. */
.notif-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border-radius: 999px;
  background: #e6c15c;
  color: #1a1408;
  font-family: Helvetica, Arial, sans-serif;
  font-size: 10px;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
/* Wraps the navbar avatar + username so hovering either one highlights
   both — they're two separate elements that both link to the same profile,
   and should read as a single clickable identity, not two independent
   targets. */
.user-identity { display: flex; align-items: center; gap: 8px; }
/* .user-name is a <button> for players (click -> own profile page) but a
   plain <span> for admins (no player profile exists for them) — the
   [data-user] attribute only appears on the clickable button variant, so
   scope the pointer/hover affordance to that rather than the class alone. */
.user-name {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 12px;
  color: var(--mut);
  letter-spacing: .04em;
}
.user-name[data-user] { cursor: pointer; }
.user-identity:hover .user-name[data-user] { color: var(--acc); }
/* Avatar: chosen card artwork (cropped to a circle) or a letter fallback.
   .avatar-nav (navbar) always links to your own profile (data-user); the
   avatar picker itself is reachable only via .avatar-xl's big avatar on
   your own profile page (avatarHtml()'s `clickable` param — see
   profileScreen()). .avatar-sm (leaderboard rows) is always a plain
   read-only badge. Clickability styling is generic — any .avatar carrying
   either the picker's data-act or a profile-link data-user, regardless of
   size class. */
.avatar {
  position: relative;
  flex: none;
  border-radius: 50%;
  overflow: hidden;
  background: var(--chip);
  display: grid;
  place-items: center;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-fallback { font-family: Cinzel, serif; font-weight: 700; color: var(--mut); }
.avatar[data-act="openAvatarPicker"], .avatar[data-user] { cursor: pointer; }
.avatar[data-act="openAvatarPicker"]:hover, .avatar[data-user]:hover { box-shadow: 0 0 0 2px var(--acc); }
/* Navbar identity pair (see .user-identity above) — hovering the username
   also lights up the avatar sitting beside it, not just itself. */
.user-identity:hover .avatar[data-user] { box-shadow: 0 0 0 2px var(--acc); }
.avatar-nav {
  width: 28px;
  height: 28px;
  border: 1px solid var(--line);
  padding: 0;
}
.avatar-nav .avatar-fallback { font-size: 12px; }
.avatar-sm { width: 24px; height: 24px; }
.avatar-sm .avatar-fallback { font-size: 10.5px; }
.avatar-md { width: 32px; height: 32px; }
.avatar-md .avatar-fallback { font-size: 13px; }
.avatar-xl { width: clamp(72px, 16vw, 120px); height: clamp(72px, 16vw, 120px); border: 2px solid var(--line); }
.avatar-xl .avatar-fallback { font-size: clamp(27px, 5vw, 42px); }
a.tab { text-decoration: none; }
.mana-chip { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: #cdb264; letter-spacing: .04em; }
.mana-chip b { color: #e6c15c; font-weight: 700; }
.mana-ico { width: 1em; height: 1em; }
.burger {
  display: none;                 /* shown only on mobile */
  background: transparent;
  border: none;
  color: inherit;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 6px;
}
.btn-primary:disabled, .btn-solid:disabled { opacity: .45; cursor: default; transform: none; }
.btn-primary .mana-ico, .btn-solid .mana-ico { width: 1em; height: 1em; vertical-align: -.15em; }

/* ---- Login gate ---- */
.login-wrap { min-height: 100vh; display: grid; place-items: center; padding: 24px; }
.login-card {
  width: min(360px, 92vw);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 30px 26px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  text-align: center;
}
.login-card .brand { font-size: 20px; }
.login-sub { color: var(--mut); font-size: 13px; margin-bottom: 4px; }
.login-field { text-align: left; }
.login-field label {
  display: block;
  font-size: 11.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--mut);
  margin-bottom: 6px;
}
.login-field input {
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 9px;
  color: var(--ink);
  font-size: 14px;
  padding: 10px 12px;
  font-family: inherit;
}
.login-field input:focus { outline: 1px solid var(--acc); }
.login-err { color: #e0574f; font-size: 13px; margin: 2px 0 0; min-height: 18px; }
/* Change-password form (avatar-picker shell, not .login-card) has no
   built-in flex/gap of its own like .login-card does — same spacing
   applied directly so the two forms read consistently. */
#passwordChangeForm { display: flex; flex-direction: column; gap: 14px; margin-top: 4px; }
#pvpJoinForm { display: flex; flex-direction: column; gap: 12px; margin-top: 16px; }
.pvp-menu-primary { width: 100%; margin-top: 18px; }

/* ---- Mobile header: shrink + burger menu ---- */
@media (max-width: 680px) {
  .header { gap: 10px; padding: 10px 14px; }
  .brand { font-size: 15px; letter-spacing: .16em; }
  .burger { display: inline-flex; margin-left: 4px; }
  .nav {
    position: absolute;
    top: calc(100% + 4px);
    right: 8px;
    left: 8px;
    margin: 0;
    flex-direction: column;
    gap: 4px;
    padding: 8px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: 12px;
    box-shadow: 0 14px 34px rgba(0, 0, 0, .4);
    display: none;
    z-index: 40;
  }
  .header.menu-open .nav { display: flex; }
  .nav .tab { width: 100%; text-align: left; padding: 10px 14px; }
  .user-box { gap: 8px; margin-left: auto; }
  .user-name { display: none; }
  .mana-chip, .count { font-size: 11px; }
  /* Same collapsible "Play" dropdown inside the burger menu's vertical
     list, but in-flow rather than floating (position:static) — a floating
     panel would overlap Ranks/Editor/Logout below it instead of pushing
     them down, which read as broken/overlapping in a vertical menu. The
     trigger spreads "Play" and the caret to opposite edges (full-width
     row, unlike desktop's inline-width button) so the caret sits flush
     right. */
  .tab-dropdown { width: 100%; }
  .tab-dropdown-trigger { width: 100%; justify-content: space-between; }
  /* In-flow (not floating) here, so it needs its own collapse animation —
     opacity/visibility alone (the desktop version) would leave a blank gap
     reserved in the vertical list while closed, since the element still
     occupies layout space at any opacity. max-height animates the actual
     space it takes up; border/padding are trimmed to 0 alongside it so no
     sliver of the tray's edge lingers once collapsed. */
  .tab-dropdown-panel {
    position: static;
    width: auto;
    min-width: 0;
    box-shadow: none;
    max-height: 0;
    margin-top: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-width: 0;
    overflow: hidden;
    transform: none;
    transition: max-height .2s ease, opacity .16s ease, padding .2s ease, margin .2s ease, border-width .2s ease;
  }
  .tab-dropdown.open .tab-dropdown-panel {
    max-height: 200px;
    margin-top: 4px;
    padding-top: 6px;
    padding-bottom: 6px;
    border-width: 1px;
    transform: none;
  }
}

/* ---- Toast (e.g. daily bonus) ---- */
.toast {
  position: fixed;
  top: 74px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 18px;
  border-radius: 999px;
  border: 1px solid rgba(230, 193, 92, .5);
  background: rgba(20, 18, 16, .92);
  color: #e6c15c;
  font-family: Cinzel, serif;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .02em;
  white-space: nowrap;
  box-shadow: 0 12px 30px rgba(0, 0, 0, .35);
  cursor: pointer;
  /* No entrance animation — same reasoning as .screen.match: render() always
     does a full innerHTML replace, and the toast can easily outlive a
     single render (its own 4.5s timer, see showToast() in app.js) while the
     player clicks around (e.g. checking the battle log right after a
     match-ending reward toast appears). Any of those unrelated re-renders
     recreated this element and replayed the fade-in from scratch, which
     read as the toast flashing/reappearing repeatedly. */
}
/* .toast's text (incl. this icon) is inserted as one raw string with plain
   text spaces around the icon (e.g. "+125 [icon] Faith") — Cinzel's wider
   letterform/tracking made that combination of icon size + surrounding
   spaces read as oversized and unevenly gapped once the font changed here.
   Sized down and pulled inward with negative margins instead of relying
   on the source string's plain spaces for consistent gapping. */
.toast .mana-ico {
  width: .82em;
  height: .82em;
  vertical-align: -.05em;
  margin: 0 -1px;
}

/* Persistent "new version deployed" nudge (see initVersionCheck()/
   scheduleVersionCheck() in app.js) — unlike .toast, never auto-dismisses (stays until the player
   reloads or the tab closes) and sits full-width at the very top so it
   reads as a system notice, not a transient celebration. No entrance
   animation, same reasoning as .toast: render() recreates this element on
   every re-render while state.updateAvailable is true, so a keyframe would
   replay/flicker on unrelated renders instead of playing once. */
.update-banner {
  position: sticky;
  /* Docks directly under the header rather than at the viewport's own
     top:0 — .header is ALSO position:sticky/top:0 (see above), so without
     this the two competed for the exact same spot once scrolled: the
     banner ended up sitting behind/under the header instead of stacking
     below it. --header-h is the same JS-measured header height var
     .match already relies on (see updateHeaderHeightVar() in app.js) —
     it varies with viewport width since the nav wraps at narrow widths,
     so this can't be a hardcoded constant. */
  top: var(--header-h, 64px);
  z-index: 65;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 10px 16px;
  background: rgba(98, 198, 207, .16);
  border-bottom: 1px solid rgba(98, 198, 207, .4);
  color: var(--ink);
  font-size: 13px;
  text-align: center;
}
.update-banner .btn-solid { padding: 6px 16px; font-size: 12px; }

/* ---- Footer ---- */
.footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 14px;
  padding: 22px clamp(16px, 4vw, 40px);
  border-top: 1px solid var(--line);
  color: var(--mut);
}
.footer-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: Cinzel, serif;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: .18em;
  color: var(--ink);
}
.footer-version {
  font-family: system-ui, sans-serif;
  font-weight: 400;
  font-size: 10.5px;
  letter-spacing: normal;
  color: var(--mut);
}
.footer-brand .logo-ico { width: 22px; height: 22px; color: var(--acc); flex: none; }
.footer-nav { display: flex; gap: 4px; flex-wrap: wrap; }
.footer-link {
  padding: 6px 12px;
  border-radius: 999px;
  border: none;
  background: transparent;
  color: var(--mut);
  font-size: 12.5px;
  letter-spacing: .03em;
  cursor: pointer;
}
.footer-link:hover { color: var(--ink); }
.footer-link.active { background: var(--chip); color: var(--ink); }
.footer-copy { font-size: 12px; white-space: nowrap; }
@media (max-width: 680px) {
  .footer { flex-direction: column; text-align: center; padding: 20px 14px; }
}

/* ---- Screen animation ---- */
/* No entrance animation here (deliberately — see below). This used to be
   `animation: ptcg-in .4s both`, scoped off only for .screen.match, on the
   assumption every other screen only re-renders on navigation. That
   assumption was wrong: render() always does a full innerHTML replace, and
   plenty of non-match actions call it more than once in a row (busy state
   -> result state on pack-opening, a background match poll landing while a
   confirm dialog is open, etc.) — each call recreates .screen as a brand-new
   element, restarting the fade-from-transparent every time, which read as
   flickering/reloading on the packs screen and behind dialogs. Removed
   globally rather than re-scoped per screen, since the "only re-renders on
   navigation" assumption isn't reliably true anywhere in this app. */
.screen { animation: none; }

/* ---- Packs (landing) ---- */
.packs {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: clamp(36px, 7vh, 72px) 20px 80px;
  text-align: center;
}
.pack-visual {
  width: 206px;
  height: 288px;
  border-radius: 16px;
  background: linear-gradient(160deg, #1e1a15, #0f0d0a);
  border: 1px solid rgba(230, 200, 120, .35);
  box-shadow: 0 26px 54px rgba(0, 0, 0, .35);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  animation: ptcg-float 5.5s ease-in-out infinite;
  margin-bottom: 22px;
}
.pack-seal {
  width: 86px;
  height: 86px;
  border-radius: 50%;
  border: 1px solid rgba(230, 200, 120, .5);
  display: grid;
  place-items: center;
  color: #e2c26a;
  background: repeating-radial-gradient(circle, rgba(230, 200, 120, .07) 0 6px, transparent 6px 14px);
}
.pack-seal .logo-ico { width: 42px; height: 42px; }
.pack-name {
  font-family: Cinzel, serif;
  letter-spacing: .32em;
  color: #efe9dc;
  font-size: 15px;
  font-weight: 600;
}
.pack-sub { font-size: 10px; letter-spacing: .26em; color: #9b9384; }
.pack-seal .pan-ico { width: 42px; height: 42px; }

/* ---- Pantheon Packs (built-in, not yet exposed in any UI — see the plan
   doc) — one recolored variant per pantheon, applied via an additive
   class (e.g. .pack-visual.pack-greek) on top of the existing
   .pack-visual/.opening-pack rules above. Every value below is derived
   from that pantheon's own PANTH [accent, bg] tuple (app.js), following
   the exact same alpha/lighten pattern the base gold design already
   uses: background = a ~15%-lightened bg -> bg two-stop gradient
   (mirrors #1e1a15 -> #0f0d0a), border = accent at .35 alpha (mirrors
   rgba(230,200,120,.35)), seal border = accent at .5 alpha, seal pattern
   = accent at .07 alpha, seal icon = accent at full opacity. Pack-name/
   pack-sub text and the slice-flash gradient are deliberately left
   unchanged (off-white/muted/white-gold read fine against any of these
   dark backgrounds) — see the plan doc's derivation notes.
   .opening-pack variants set background-IMAGE only (not the `background`
   shorthand) — .pack-half's own background-size:100% 300px is what makes
   its top/bottom halves line up into one continuous gradient (paired with
   background-position:top/bottom on .pack-half.top/.bottom); the
   shorthand would silently reset that to `auto` and break the seam. */
.pack-visual.pack-greek { background: linear-gradient(160deg, #39455a, #16243d); border-color: rgba(59, 110, 165, .35); }
.opening-pack.pack-greek .pack-half { background-image: linear-gradient(160deg, #39455a, #16243d); border-color: rgba(59, 110, 165, .35); }
.pack-visual.pack-greek .pack-seal, .opening-pack.pack-greek .pack-seal { border-color: rgba(59, 110, 165, .5); color: #3b6ea5; background: repeating-radial-gradient(circle, rgba(59, 110, 165, .07) 0 6px, transparent 6px 14px); }

.pack-visual.pack-norse { background: linear-gradient(160deg, #363e47, #131c26); border-color: rgba(90, 125, 140, .35); }
.opening-pack.pack-norse .pack-half { background-image: linear-gradient(160deg, #363e47, #131c26); border-color: rgba(90, 125, 140, .35); }
.pack-visual.pack-norse .pack-seal, .opening-pack.pack-norse .pack-seal { border-color: rgba(90, 125, 140, .5); color: #5a7d8c; background: repeating-radial-gradient(circle, rgba(90, 125, 140, .07) 0 6px, transparent 6px 14px); }

.pack-visual.pack-egyptian { background: linear-gradient(160deg, #584a36, #3a2a12); border-color: rgba(185, 138, 58, .35); }
.opening-pack.pack-egyptian .pack-half { background-image: linear-gradient(160deg, #584a36, #3a2a12); border-color: rgba(185, 138, 58, .35); }
.pack-visual.pack-egyptian .pack-seal, .opening-pack.pack-egyptian .pack-seal { border-color: rgba(185, 138, 58, .5); color: #b98a3a; background: repeating-radial-gradient(circle, rgba(185, 138, 58, .07) 0 6px, transparent 6px 14px); }

.pack-visual.pack-japanese { background: linear-gradient(160deg, #4a3336, #2a0f12); border-color: rgba(181, 66, 58, .35); }
.opening-pack.pack-japanese .pack-half { background-image: linear-gradient(160deg, #4a3336, #2a0f12); border-color: rgba(181, 66, 58, .35); }
.pack-visual.pack-japanese .pack-seal, .opening-pack.pack-japanese .pack-seal { border-color: rgba(181, 66, 58, .5); color: #b5423a; background: repeating-radial-gradient(circle, rgba(181, 66, 58, .07) 0 6px, transparent 6px 14px); }

.pack-visual.pack-celtic { background: linear-gradient(160deg, #33423a, #0f2117); border-color: rgba(63, 125, 84, .35); }
.opening-pack.pack-celtic .pack-half { background-image: linear-gradient(160deg, #33423a, #0f2117); border-color: rgba(63, 125, 84, .35); }
.pack-visual.pack-celtic .pack-seal, .opening-pack.pack-celtic .pack-seal { border-color: rgba(63, 125, 84, .5); color: #3f7d54; background: repeating-radial-gradient(circle, rgba(63, 125, 84, .07) 0 6px, transparent 6px 14px); }

.pack-visual.pack-aztec { background: linear-gradient(160deg, #324541, #0e2420); border-color: rgba(47, 143, 126, .35); }
.opening-pack.pack-aztec .pack-half { background-image: linear-gradient(160deg, #324541, #0e2420); border-color: rgba(47, 143, 126, .35); }
.pack-visual.pack-aztec .pack-seal, .opening-pack.pack-aztec .pack-seal { border-color: rgba(47, 143, 126, .5); color: #2f8f7e; background: repeating-radial-gradient(circle, rgba(47, 143, 126, .07) 0 6px, transparent 6px 14px); }

.pack-visual.pack-mesopotamian { background: linear-gradient(160deg, #45372d, #241408); border-color: rgba(138, 90, 51, .35); }
.opening-pack.pack-mesopotamian .pack-half { background-image: linear-gradient(160deg, #45372d, #241408); border-color: rgba(138, 90, 51, .35); }
.pack-visual.pack-mesopotamian .pack-seal, .opening-pack.pack-mesopotamian .pack-seal { border-color: rgba(138, 90, 51, .5); color: #8a5a33; background: repeating-radial-gradient(circle, rgba(138, 90, 51, .07) 0 6px, transparent 6px 14px); }

.packs h1 {
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: clamp(26px, 4.5vw, 40px);
  margin: 0;
  line-height: 1.15;
}
.packs .lede {
  font-family: Cinzel, serif;
  font-style: italic;
  font-weight: 500;
  font-size: clamp(13px, 1.6vw, 15px);
  color: var(--mut);
  margin: 0 0 18px;
  max-width: 420px;
  line-height: 1.5;
  text-wrap: pretty;
}
.btn-primary {
  padding: 14px 30px;
  border-radius: 999px;
  border: none;
  background: var(--ink);
  color: var(--bg);
  font-family: Cinzel, serif;
  font-size: 14px;
  letter-spacing: .08em;
  cursor: pointer;
  font-weight: 600;
}
.btn-primary:hover { transform: translateY(-1px); }
.opened-label { font-size: 12px; color: var(--mut); margin-top: 10px; }

/* Admin-only Pantheon Pack picker (packsScreen()) — a plain select next to
   its own open button, deliberately understated (btn-outline, not
   btn-primary) so it reads as a secondary/admin tool, not the screen's
   main action. Invisible to non-admins — see pantheonPackPickerHtml(). */
.pantheon-pack-picker {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  margin-top: 28px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}
/* Same shell as the main .pack-visual, scaled down — a live preview of
   whichever pantheon is selected below, so all 7 recolored designs
   (pack-visual.pack-<slug>, further up in this file) are actually
   reviewable, not just selectable. */
.pack-visual.pack-preview {
  width: 132px;
  height: 184px;
  gap: 10px;
  animation: none;
}
.pack-preview .pack-seal { width: 56px; height: 56px; }
.pack-preview .pack-seal .pan-ico, .pack-preview .pack-seal .logo-ico { width: 28px; height: 28px; }
.pack-preview .pack-name { font-size: 11px; letter-spacing: .22em; }
.pack-preview .pack-sub { font-size: 8px; }
.pantheon-pack-picker-controls { display: flex; align-items: center; gap: 10px; }
.pantheon-pack-picker .select {
  font-family: Cinzel, serif;
  font-size: 12px;
  letter-spacing: .04em;
  padding: 10px 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
}

/* ---- Pantheon onboarding (see api/onboarding.php, onboardingScreen() in
   app.js) — a one-time, full-screen flow gated in render() before the
   normal shell()/navbar, same "bare full-bleed div" pattern as .login-wrap.
   Values (positions, timings, colors) are pixel-matched to the design
   handoff bundle read this session, not re-derived. */
.onb-wrap {
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  overflow: hidden;
}
.onb-bg {
  position: absolute; inset: 0;
  background-image: url('assets/nebula-bg.png');
  background-size: cover;
  background-position: center;
  pointer-events: none;
}
.onb-scrim {
  position: absolute; inset: 0;
  background: radial-gradient(75% 65% at 50% 45%, rgba(13, 22, 38, .35), rgba(13, 22, 38, .78) 100%);
  pointer-events: none;
}
.onb-glow {
  position: absolute; inset: 0;
  background: radial-gradient(60% 55% at 50% 45%, rgba(63, 125, 84, .20), rgba(13, 22, 38, 0) 70%);
  pointer-events: none;
}
.onb-dots {
  position: absolute; top: 28px; left: 50%; transform: translateX(-50%);
  display: flex; gap: 8px; z-index: 2;
}
.onb-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: rgba(230, 236, 245, .2);
  transition: background .3s;
}
.onb-dot.active { background: #e6c15c; box-shadow: 0 0 8px rgba(230, 193, 92, .8); }

/* Screens 1 & 2 — ring of 7 (or 6) Pantheon icons around centered text.
   Icon left/top % and animation-delay are computed per-icon in JS
   (onbSelectHtml()) — this only carries the static shape/motion rules. */
.onb-select { position: relative; z-index: 1; width: 100%; max-width: 720px; margin: 0 auto; }
.onb-stage { position: relative; width: 100%; aspect-ratio: 1 / 1; max-height: 82vh; margin: 0 auto; }
.onb-center-text {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; pointer-events: none; padding: 0 12%;
}
.onb-lede {
  font-family: Cinzel, serif; font-style: italic; font-weight: 500;
  font-size: clamp(12px, 1.5vw, 14px); color: var(--mut);
  letter-spacing: .04em; margin-bottom: 14px;
}
.onb-title {
  font-family: Cinzel, serif; font-weight: 700; font-size: clamp(24px, 4.4vw, 40px);
  margin: 0; letter-spacing: .04em; line-height: 1.15; color: var(--ink);
}
.onb-icon-btn {
  position: absolute;
  width: clamp(64px, 13vw, 104px);
  transform: translate(-50%, -50%);
  border: none; background: none; padding: 0; cursor: pointer;
  display: flex; flex-direction: column; align-items: center;
}
.onb-disc {
  display: grid;
  place-items: center;
  width: clamp(64px, 13vw, 104px);
  height: clamp(64px, 13vw, 104px);
  border-radius: 50%;
  overflow: hidden;
  background: var(--bg);
  border: 2px solid var(--accent);
  box-shadow: 0 0 34px color-mix(in srgb, var(--accent) 53%, transparent), inset 0 0 20px rgba(0, 0, 0, .45);
}
.onb-disc .pan-ico { width: 46%; height: 46%; color: var(--accent); }
.onb-disc-lg {
  width: 84px; height: 84px;
  margin: 0 auto; /* .onb-disc is display:grid (block-level) — text-align:center on
                     the dialog panel only centers inline content, not this */
  box-shadow: 0 0 40px color-mix(in srgb, var(--accent) 67%, transparent), inset 0 0 20px rgba(0, 0, 0, .45);
}
@keyframes onb-slow-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes onb-ring-in { from { opacity: 0; transform: translate(-50%, -50%) scale(.7); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } }

/* Confirm dialog — bespoke visual (pantheon disc + per-step copy), not a
   reuse of .dialog-overlay/.dialog-panel (those are plain-message-only). */
.onb-dialog-overlay {
  position: fixed; inset: 0; z-index: 80;
  display: grid; place-items: center; padding: 24px;
  background: rgba(6, 11, 20, .7);
  backdrop-filter: blur(4px);
  animation: onb-scrim-in .2s ease both;
}
@keyframes onb-scrim-in { from { opacity: 0; } to { opacity: 1; } }
.onb-dialog-panel {
  width: 100%; max-width: 420px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 34px 30px 28px;
  text-align: center;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .5);
  animation: onb-dialog-in .26s cubic-bezier(.2, .7, .2, 1) both;
}
@keyframes onb-dialog-in { from { opacity: 0; transform: translateY(14px) scale(.96); } to { opacity: 1; transform: none; } }
.onb-dialog-kicker {
  font-family: Cinzel, serif; font-size: 13px; text-transform: uppercase;
  letter-spacing: .14em; color: var(--mut); margin-top: 16px;
}
.onb-dialog-title { font-family: Cinzel, serif; font-weight: 600; font-size: 26px; margin: 8px 0 24px; letter-spacing: .04em; color: var(--ink); }
.onb-dialog-actions { display: flex; flex-direction: column; gap: 12px; }
.onb-btn-accept {
  padding: 14px 30px; border-radius: 999px; border: none;
  background: var(--ink); color: var(--bg);
  font-family: Cinzel, serif; font-weight: 600; font-size: 14px; letter-spacing: .08em;
  cursor: pointer;
}
.onb-btn-decline {
  padding: 13px 26px; border-radius: 999px; border: 1px solid var(--line);
  background: transparent; color: var(--mut);
  font-family: Cinzel, serif; font-size: 13px; letter-spacing: .08em;
  cursor: pointer;
}

/* Screen 3 — reward */
.onb-reward { position: relative; z-index: 1; width: 100%; max-width: 640px; text-align: center; }
.onb-reward-headline {
  font-family: Cinzel, serif; font-weight: 700; font-size: clamp(20px, 3.4vw, 30px);
  color: var(--ink); letter-spacing: .04em; line-height: 1.25; text-wrap: pretty; margin: 0 0 30px;
}
.onb-reward-faith { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.onb-faith-gem {
  color: #e6c15c; width: clamp(84px, 16vw, 128px); height: clamp(84px, 16vw, 128px);
  animation: onb-faith-pulse 2.4s ease-in-out infinite;
}
.onb-faith-gem .mana-ico { width: 100%; height: 100%; }
@keyframes onb-faith-pulse {
  0%, 100% { filter: drop-shadow(0 0 18px rgba(230, 193, 92, .45)); }
  50%      { filter: drop-shadow(0 0 42px rgba(230, 193, 92, .85)); }
}
.onb-faith-count {
  font-family: Helvetica, Arial, sans-serif; font-weight: 700; font-size: clamp(48px, 12vw, 92px);
  color: #e6c15c; font-variant-numeric: tabular-nums; line-height: 1;
}
.onb-faith-caption { font-family: Helvetica, Arial, sans-serif; font-size: 12px; text-transform: uppercase; letter-spacing: .16em; color: var(--mut); }
.onb-enter-wrap { margin-top: 40px; height: 62px; display: flex; align-items: center; justify-content: center; }
.onb-enter-btn {
  padding: 18px 42px; border-radius: 999px;
  border: 1px solid rgba(230, 195, 100, .5);
  background: rgba(230, 195, 100, .1);
  color: #e6c364;
  font-family: Cinzel, serif; font-weight: 600; font-size: 17px; letter-spacing: .06em;
  cursor: default;
  opacity: 0;
  pointer-events: none;
  transition: opacity .5s ease;
}
.onb-enter-btn.shown {
  cursor: pointer;
  opacity: 1;
  pointer-events: auto;
  animation: onb-faith-pulse 2.2s ease-in-out infinite;
}

/* Free onboarding Pantheon Pack tiles (packsScreen()'s onbFreePacksScreen())
   — reuses .pack-visual/.pack-<slug> as the base shell, just tiled + given
   its own "Open · Free" button instead of the single centered pack. */
.onb-free-packs-progress { font-size: 12px; color: var(--mut); margin-top: 10px; }
.onb-free-packs-grid {
  display: flex; flex-wrap: wrap; justify-content: center; gap: 20px;
  margin-top: 28px;
}
.onb-free-pack-tile { animation: none; }
.onb-free-pack-tile .btn-primary { margin-top: 10px; }

@media (prefers-reduced-motion: reduce) {
  .onb-icon-btn, .onb-dialog-overlay, .onb-dialog-panel, .onb-faith-gem, .onb-enter-btn.shown {
    animation-duration: .001s !important;
  }
}

/* ---- Pack opening ---- */
.opening { min-height: 100vh; min-height: 100dvh; padding: clamp(28px, 5vh, 52px) 12px 70px; }
.opening h2 {
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: clamp(19px, 3vw, 26px);
  text-align: center;
  margin: 0 0 28px;
}
.reveal-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(10px, 2vw, 22px);
  max-width: 1100px;
  margin: 0 auto;
}
.flip-card {
  width: clamp(130px, 27vw, 184px);
  aspect-ratio: 5 / 7;
  perspective: 1100px;
  cursor: pointer;
  transition: transform .14s ease-out;
}
.flip-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform .7s cubic-bezier(.25, .7, .3, 1);
}
.flip-inner.revealed { transform: rotateY(180deg); }
.flip-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  border-radius: 14px;
}
.flip-back {
  background: #161310;
  border: 1px solid rgba(230, 200, 120, .32);
  display: grid;
  place-items: center;
  box-shadow: 0 12px 30px rgba(0, 0, 0, .3);
}
.flip-back .seal {
  width: 64%;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 1px solid rgba(230, 200, 120, .35);
  display: grid;
  place-items: center;
  color: #c8a552;
  background: repeating-radial-gradient(circle, rgba(230, 200, 120, .08) 0 6px, transparent 6px 14px);
}
.flip-back .seal .logo-ico { width: 40%; height: 40%; }
.flip-front {
  transform: rotateY(180deg);
  background:
    radial-gradient(140% 110% at 50% 10%, rgba(255, 255, 255, .08), transparent 45%),
    radial-gradient(160% 130% at 50% 108%, rgba(0, 0, 0, .55), transparent 58%),
    #161310;
  padding: 9px;
  display: flex;
  flex-direction: column;
  gap: 7px;
  overflow: hidden;
}

/* Frame depth: a light top edge + a dark inner shadow so the border reads as
   pressed/struck metal instead of a flat colour fill. `--glow` is set inline
   per card (deco().glow) and slots into the shared shadow stack; border-color
   is set inline, border-width is set per view below. .inspect-card is NOT
   part of this rule — it gets its own static/hover-pulse shadow further down,
   since only the inspect view uses a hover-triggered glow. */
.flip-front, .binder-card, .summon-card, .market-card-face {
  box-sizing: border-box;
  border-style: solid;
  box-shadow:
    var(--glow, 0 12px 28px rgba(0, 0, 0, .25)),
    inset 0 1.5px 0 rgba(255, 255, 255, .16),
    inset 0 -4px 9px rgba(0, 0, 0, .45);
}

/* ---- Card face internals (shared: reveal + inspect) ---- */
.card-top {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 6px;
}
/* Pantheon icon + name — now bottom-left, in .card-meta alongside the series
   group (ascension took over the top-left spot in .card-top instead; see
   the card-meta/card-top swap in app.js's card-rendering functions). Both
   rows already center-align their children (.card-top/.card-meta above),
   so moving this here keeps it vertically centered the same way the series
   group on its right always has been — no extra alignment rule needed.
   Colour is set inline per card (deco().labelCol). */
.pan-label { display: inline-flex; align-items: center; gap: .4em; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-family: Cinzel, serif; font-weight: 700; line-height: 1; }
.pan-label .pan-ico { flex: none; }
/* At the smallest match-card sizes, pantheon names ("Mesopotamian") are the
   longest label sharing a row with the series group — hide the text there
   and keep just the icon, the same precedent as .series-txt below. */
.match-slot .pan-txt, .match-hand-card .pan-txt, .match-flip-front .pan-txt { display: none; }
.card-art {
  position: relative;
  flex: 1;
  display: grid;
  place-items: center;
  overflow: hidden;
}
.card-glyph {
  color: rgba(255, 250, 235, .92);
  text-shadow: 0 4px 18px rgba(0, 0, 0, .45);
}
.card-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  -webkit-user-drag: none;
  user-drag: none;
}
.card-name {
  font-family: Cinzel, serif;
  font-weight: 600;
  color: #efe9dc;
  line-height: 1.15;
  text-align: center;
}
/* Divider between the identity block (name/epithet) and the stats line.
   border-top-color is set inline per card (deco().labelCol). */
.card-divider { border: none; border-top: 1px solid; opacity: .5; margin: 0; }
.card-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: .8em;
}
/* Ascension icon + label (left) and Series I + card number (right). Everything
   here is sized in `em` off .card-meta's per-view font-size, so icons and text
   scale together and stay in the same proportion across every card size.
   Colour is set inline per card; the ascension icon's fill is its own inline
   SVG gradient (rarity metal), independent of this text colour. */
/* Ascension USED to be flex:0 0 auto ("never shrinks") on the theory that
   there was always room for both it and .power in .card-top — repeatedly
   proven false on the match board's small card faces, where the two share
   one row and the ascension label ("CREATURE"/"DEITY") is the longer of the
   two: at low width it silently overflowed .card-top and got clipped by the
   card's own overflow:hidden, taking .power down with it since nothing ever
   reserved space for it. Power is the one stat that has to survive at every
   card size the game ever renders, so the priority is now explicit and
   physically enforced, not just convention: .meta-left (icon + ascension
   text) is the flexible side that truncates to an ellipsis first, .power
   below is flex:none and never yields a pixel of its own intrinsic width. */
.meta-left { display: flex; align-items: center; gap: .6em; flex: 1 1 auto; min-width: 0; overflow: hidden; }
/* Ascension now sits top-left (where the pantheon label used to be, see
   card-top below) — Cinzel + bold + a slight size bump visually align it
   with the headline treatment pan-label always had in that spot; icon size
   is untouched (still purely em-driven off the row's own font-size).
   flex:1 1 auto + min-width:0 (not the icon's own flex:none sibling) is what
   actually shrinks first — the ellipsis is the visible sign that happened,
   rather than the text silently vanishing off the edge of the card. */
.meta-left .ascension { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: Cinzel, serif; font-weight: 700; font-size: 1.1em; line-height: 1; }
.asc-ico { width: 1.8em; height: 1.8em; flex: none; display: block; }
/* justify-content:flex-end so that if the group is squeezed, it clips the
   "SERIES I" side on the left and keeps the identifying card number visible. */
.series-group { display: flex; align-items: center; justify-content: flex-end; gap: .4em; flex: 0 1 auto; min-width: 0; overflow: hidden; }
.series-group .dot { opacity: .55; flex: none; }
.series-group .series { display: inline-flex; align-items: center; gap: .25em; opacity: .85; white-space: nowrap; flex: none; }
.series-group .series .series-ico { width: 1.2em; height: 1.2em; flex: none; }
/* On the small card faces the "SERIES I" text is dropped to just the series
   icon (+ card number), so it never crowds out the ascension label; only the
   large inspect view has the room to show the full "SERIES I" wording. */
.series-txt { display: none; }
.inspect-card .series-txt { display: inline; }
.series-group .card-num { opacity: .85; white-space: nowrap; flex: none; font-variant-numeric: tabular-nums; }
/* Power stat — now the prominent number in the header, top-right. */
.power {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  white-space: nowrap;
  font-weight: 800;
  letter-spacing: .01em;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .45);
  line-height: 1;
  /* Protected side of .card-top — see .meta-left above. Never shrinks, never
     truncates, at any card size the game renders. */
  flex: none;
}
.power .pwr-ico { width: 1em; height: 1em; flex: none; display: block; }

/* ---- Alternative Art: light card variant ---- */
.binder-card.alt, .flip-front.alt, .inspect-card.alt, .market-card-face.alt { background: #f4f1ea; }
.binder-card.alt .card-name, .flip-front.alt .card-name, .inspect-card.alt .card-name, .market-card-face.alt .card-name { color: #1c1a16; }
/* pan-label/meta-left/power/diamond colours are set inline per card (deco()
   already returns dark-ink values for alt-art); only the epithet — which has
   no inline colour — needs the muted override here. */
.inspect-card.alt .epithet { color: #6b6456; }
.alt .power { text-shadow: none; }
.alt .card-glyph { color: rgba(28, 26, 22, .6); text-shadow: none; }
.badge-new {
  position: absolute;
  top: 6px;
  left: 6px;
  background: rgba(230, 193, 92, .92);
  color: #141210;
  font-size: 8.5px;
  font-weight: 700;
  letter-spacing: .12em;
  padding: 3px 7px;
  border-radius: 999px;
}
.holo {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: .45;            /* ambient: the foil is visible without the cursor */
  transition: opacity .25s;
  mix-blend-mode: screen;
}
/* Brighten the foil when the card is hovered / "lit". */
.flip-front:hover .holo,
.binder-card:hover .holo,
.inspect-card:hover .holo,
.market-card-face:hover .holo { opacity: 1; }
.sweep {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-size: 240% 100%;
  mix-blend-mode: screen;
}

/* Reveal-card face sizing */
.flip-front { border-width: 3px; }
.flip-front .card-top { font-size: 8.5px; letter-spacing: .18em; }
.flip-front .card-meta .pan-ico { width: 20px; height: 20px; }
.flip-front .card-top .power { font-size: 15px; }
.flip-front .card-art { border-radius: 9px; }
.flip-front .card-glyph { font-size: clamp(38px, 6vw, 52px); }
.flip-front .card-name { font-size: clamp(12px, 1.6vw, 15px); }
.flip-front .card-divider { margin: 1px 0; }
.flip-front .card-meta { font-size: 8px; letter-spacing: .1em; }
.flip-front .holo { border-radius: 14px; }
.flip-front .sweep { border-radius: 14px; }

/* Binder-card face sizing */
.binder-card { border-width: 3px; }
.binder-card .card-top { font-size: 8px; letter-spacing: .16em; }
.binder-card .card-meta .pan-ico { width: 18px; height: 18px; }
.binder-card .card-top .power { font-size: 14px; }
.binder-card .card-art { border-radius: 8px; }
.binder-card .card-glyph { font-size: 34px; text-shadow: 0 4px 16px rgba(0, 0, 0, .45); }
.binder-card .card-name {
  font-size: 12.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.binder-card .card-divider { margin: 1px 0; }
.binder-card .card-meta { font-size: 7.5px; letter-spacing: .08em; }
.binder-card .holo { border-radius: 13px; }

/* ---- Sealed pack: slice-to-open (stage 1) ---- */
.opening.sealed {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  cursor: crosshair;      /* the whole area is sliceable, not just the pack */
  touch-action: none;
  user-select: none;
}
.opening.sealed h2 { margin-bottom: 6px; }
.slice-hint {
  font-family: Cinzel, serif;
  font-style: italic;
  font-weight: 500;
  font-size: clamp(13px, 1.6vw, 15px);
  color: var(--mut);
  margin: 0 0 30px;
}

.opening-pack {
  position: relative;
  width: 214px;
  height: 300px;
  margin-top: 8px;
  cursor: crosshair;
  touch-action: none;
  user-select: none;
  animation: ptcg-float 5.5s ease-in-out infinite;
}
.opening-pack.opened { animation: none; cursor: default; }

.pack-half {
  position: absolute;
  left: 0;
  right: 0;
  height: 50%;
  background: linear-gradient(160deg, #1e1a15, #0f0d0a);
  background-size: 100% 300px;
  border: 1px solid rgba(230, 200, 120, .35);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  overflow: hidden;
  box-shadow: 0 26px 54px rgba(0, 0, 0, .35);
  transition: transform .55s cubic-bezier(.3, .7, .3, 1), opacity .55s;
}
.pack-half.top {
  top: 0;
  background-position: top;
  border-radius: 16px 16px 0 0;
  border-bottom: none;
}
.pack-half.bottom {
  bottom: 0;
  background-position: bottom;
  border-radius: 0 0 16px 16px;
  border-top: none;
}
.opening-pack.opened .pack-half.top { transform: translateY(-130%) rotate(-7deg); opacity: 0; }
.opening-pack.opened .pack-half.bottom { transform: translateY(130%) rotate(7deg); opacity: 0; }

.pack-cut {
  position: absolute;
  left: 6%;
  right: 6%;
  top: 50%;
  height: 1px;
  transform: translateY(-.5px);
  background: linear-gradient(90deg, transparent, rgba(230, 200, 120, .5), transparent);
  pointer-events: none;
}
.slice-flash {
  position: absolute;
  left: -6%;
  right: -6%;
  top: 50%;
  height: 3px;
  transform: translateY(-1.5px) scaleX(.2);
  border-radius: 999px;
  background: linear-gradient(90deg, transparent, #fff, #ffe6a6, #fff, transparent);
  opacity: 0;
  pointer-events: none;
}
.opening-pack.opened .slice-flash { animation: slice-flash .5s ease-out; }
@keyframes slice-flash {
  0% { opacity: 0; transform: translateY(-1.5px) scaleX(.15); }
  30% { opacity: 1; }
  100% { opacity: 0; transform: translateY(-1.5px) scaleX(1.35); }
}

.blade-trail { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
.blade-seg {
  position: absolute;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, .95), rgba(255, 224, 150, .5) 55%, transparent 70%);
  box-shadow: 0 0 12px 4px rgba(255, 224, 150, .55);
  pointer-events: none;
  animation: blade-fade .26s ease-out forwards;
}
@keyframes blade-fade {
  from { opacity: .9; transform: scale(1); }
  to { opacity: 0; transform: scale(.3); }
}

/* Pre-flip glow: rare, legendary & alt-art pulse in their rarity colour
   while face-down */
.flip-card.hint .flip-inner:not(.revealed) .flip-back {
  animation: rarity-pulse 1.6s ease-in-out infinite;
}
/* Rare and alt-art share a near-identical near-white pulse color (by
   design — see deco()/METAL in app.js), which read as indistinguishable
   at a glance. Alt-art gets a slightly slower cadence here so the two are
   still only tell-apart-able side by side (not meant to be obvious on
   its own), rather than changing either color. */
.flip-card.alt.hint .flip-inner:not(.revealed) .flip-back {
  animation-duration: 2.1s;
}
@keyframes rarity-pulse {
  0%, 100% { box-shadow: 0 12px 30px rgba(0, 0, 0, .3); border-color: rgba(230, 200, 120, .32); }
  50% { box-shadow: 0 0 22px 3px var(--rar), 0 12px 30px rgba(0, 0, 0, .3); border-color: var(--rar); }
}
/* Post-reveal celebration: a rare/legendary card, once flipped face-up, keeps
   pulsing in its rarity-metal colour (--rar, set inline on .flip-card). This is
   the only pulsing glow in the app — every other view uses a static glow. */
.flip-card .flip-inner.revealed .flip-front.reveal-glow {
  animation: ptcg-reveal-pulse 1.7s ease-in-out infinite;
}
@keyframes ptcg-reveal-pulse {
  0%, 100% { box-shadow: 0 0 16px 1px var(--rar), inset 0 1.5px 0 rgba(255, 255, 255, .16), inset 0 -4px 9px rgba(0, 0, 0, .45); }
  50%      { box-shadow: 0 0 34px 6px var(--rar), inset 0 1.5px 0 rgba(255, 255, 255, .16), inset 0 -4px 9px rgba(0, 0, 0, .45); }
}
@media (prefers-reduced-motion: reduce) {
  .flip-card .flip-inner.revealed .flip-front.reveal-glow { animation: none; }
}

/* Staggered card entrance — only on the first render after the pack opens */
.reveal-grid.entering .flip-card { animation: card-in .45s both; animation-delay: calc(var(--i, 0) * 60ms); }
@keyframes card-in {
  from { opacity: 0; transform: translateY(16px) scale(.96); }
  to { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .opening-pack, .pack-half, .slice-flash, .blade-seg, .reveal-grid .flip-card {
    animation: none !important;
    transition: none !important;
  }
  .flip-card.hint .flip-inner:not(.revealed) .flip-back {
    animation: none;
    box-shadow: 0 0 16px 2px var(--rar);
    border-color: var(--rar);
  }
}

/* ---- Opening actions ---- */
.opening-actions {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin-top: 34px;
  flex-wrap: wrap;
}
.btn-solid {
  padding: 13px 26px;
  border-radius: 999px;
  border: none;
  background: var(--ink);
  color: var(--bg);
  font-family: Cinzel, serif;
  font-size: 13px;
  letter-spacing: .08em;
  cursor: pointer;
  font-weight: 600;
}
.btn-solid:hover { transform: translateY(-1px); }
.btn-outline {
  padding: 13px 26px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: inherit;
  font-family: Cinzel, serif;
  font-size: 13px;
  letter-spacing: .08em;
  cursor: pointer;
}
.btn-outline:hover { opacity: .7; }
.btn-ghost {
  padding: 11px 22px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--mut);
  font-family: Cinzel, serif;
  font-size: 12px;
  letter-spacing: .08em;
  cursor: pointer;
}
.btn-ghost:hover { opacity: .7; }

/* Mobile: pack-opening actions move under the heading and stay on one row. */
@media (max-width: 680px) {
  .opening { display: flex; flex-direction: column; }
  .opening h2 { order: 0; margin-bottom: 14px; }
  .opening-actions {
    order: 1;
    margin: 0 0 22px;
    flex-wrap: nowrap;
    gap: 8px;
  }
  .reveal-grid { order: 2; }
  .opening-actions .btn-solid,
  .opening-actions .btn-outline,
  .opening-actions .btn-ghost {
    flex: 1 1 0;
    min-width: 0;
    padding: 9px 10px;
    font-size: 10.5px;
    letter-spacing: .02em;
    text-align: center;
  }
  .opening-actions .btn-solid .mana-ico { width: .9em; height: .9em; }
  .summon h2 { order: 0; }
  .summon .inspect-card { order: 2; }
}

/* ---- Binder ---- */
.binder {
  padding: 26px clamp(14px, 4vw, 40px) 80px;
  max-width: 1180px;
  margin: 0 auto;
}
.binder-head {
  display: flex;
  align-items: baseline;
  gap: 16px;
  flex-wrap: wrap;
}
.binder-head h2 {
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: 26px;
  margin: 0;
}
.binder-head .collected { font-size: 13px; color: var(--mut); }
.binder-head-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
.progress {
  height: 4px;
  border-radius: 999px;
  background: var(--chip);
  margin: 14px 0 18px;
  overflow: hidden;
}
.progress-bar {
  height: 100%;
  border-radius: 999px;
  background: var(--acc);
  transition: width .5s;
}
/* Three stacked rows now (pantheon/ascension/rarity) — every row needs its
   own bottom margin, not just the first, or the later rows crowd together. */
.chips { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 9px; }
.chip-pan, .chip-asc, .chip-rar {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border-radius: 999px;
  border: 1px solid var(--line);
  cursor: pointer;
  letter-spacing: .03em;
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.chip-pan { padding: 7px 14px; font-size: 12px; }
.chip-asc, .chip-rar { padding: 6px 12px; font-size: 11px; }
.chip-pan:hover, .chip-asc:hover, .chip-rar:hover { opacity: .8; }
.chip-pan .pan-ico { width: 14px; height: 14px; flex: none; }
.chip-asc .asc-ico { width: 14px; height: 14px; flex: none; }
.chip-rar .rar-ico { width: 12px; height: 12px; flex: none; border-radius: 50%; }
.binder-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: 14px;
  margin-top: 22px;
}
.binder-card {
  position: relative;
  aspect-ratio: 5 / 7;
  border-radius: 13px;
  background:
    radial-gradient(140% 110% at 50% 10%, rgba(255, 255, 255, .08), transparent 45%),
    radial-gradient(160% 130% at 50% 108%, rgba(0, 0, 0, .55), transparent 58%),
    #161310;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  transition: transform .14s ease-out, filter .3s, box-shadow .22s ease;
  overflow: hidden;
}
/* Owned-count badge: bottom-right inside the art area (card-art is positioned),
   so it no longer overlaps the rarity mark in the card header. */
.binder-card .card-art .badge-count {
  position: absolute;
  bottom: 5px;
  right: 5px;
  background: rgba(230, 193, 92, .92);
  color: #141210;
  border-radius: 999px;
  font-size: 9.5px;
  font-weight: 700;
  padding: 3px 7px;
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
/* At-a-glance "some copies are deck-reserved" pin inside the owned-count
   badge — see binderCard(). Full breakdown lives in inspectOverlay(). Dark,
   bold-stroked so it stays legible against the badge's gold background
   without a second background shape competing with the pill itself. */
.badge-deck-pin { display: inline-flex; align-items: center; flex: none; color: #141210; }
.badge-deck-pin svg { width: 9px; height: 9px; stroke-width: 3; display: block; }

/* ---- Deck builder ---- */
.deck {
  padding: 26px clamp(14px, 4vw, 40px) 80px;
  max-width: 1180px;
  margin: 0 auto;
}
.deck-loading { color: var(--mut); padding: 40px 0; text-align: center; }
.deck-head { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.deck-head h2 { font-family: Cinzel, serif; font-weight: 600; font-size: 26px; margin: 0; }
.deck-slots { display: flex; gap: 7px; flex-wrap: wrap; margin-left: auto; }
.deck-slot-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: inherit;
  font-family: Cinzel, serif;
  font-size: 12.5px;
  cursor: pointer;
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.deck-slot-btn:hover { opacity: .82; }
.deck-slot-btn.sel { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.deck-slot-btn .dot { font-size: 11px; line-height: 1; }
.deck-slot-btn.active .dot { color: var(--acc); }
.deck-slot-btn.ready .dot { color: #6fbf73; }
.deck-slot-btn.invalid .dot { color: #e0574f; }
.deck-slot-btn.draft .dot { color: var(--mut); }
.deck-slot-btn.sel.active .dot, .deck-slot-btn.sel.ready .dot,
.deck-slot-btn.sel.invalid .dot, .deck-slot-btn.sel.draft .dot { color: inherit; }

.deck-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 18px 0 12px;
}
.deck-name-input {
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--ink);
  font-size: 14px;
  font-family: inherit;
  padding: 8px 12px;
  min-width: 180px;
}
.deck-name-input:focus { outline: 1px solid var(--acc); }
.deck-counter { font-size: 15px; color: var(--mut); font-variant-numeric: tabular-nums; }
.deck-counter b { color: var(--ink); font-weight: 700; }
.deck-counter.full b { color: #6fbf73; }
.deck-counter.over b { color: #e0574f; }
.deck-badges { display: inline-flex; gap: 6px; flex-wrap: wrap; }
.deck-badge {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .06em;
  padding: 3px 9px;
  border-radius: 999px;
}
.deck-badge.active { background: rgba(98, 198, 207, .16); color: var(--acc); border: 1px solid rgba(98, 198, 207, .5); }
.deck-badge.invalid { background: rgba(224, 87, 79, .14); color: #e0574f; border: 1px solid rgba(224, 87, 79, .5); }
.deck-badge.dirty { background: var(--chip); color: var(--mut); }
.deck-bar-spacer { flex: 1 1 auto; }

.deck-problems {
  border: 1px solid rgba(224, 87, 79, .5);
  background: rgba(224, 87, 79, .12);
  color: #e0574f;
  border-radius: 12px;
  padding: 12px 16px;
  font-size: 13px;
  margin-bottom: 14px;
}
.deck-problems b { display: block; margin-bottom: 6px; }
.deck-problems ul { margin: 0; padding-left: 18px; }
.deck-problems li { margin: 2px 0; }

.deck-tray {
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  padding: 10px 12px;
  margin-bottom: 16px;
}
.deck-tray-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.deck-tray-title { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--mut); }
.deck-tray-toggle { display: none; background: none; border: none; color: var(--acc); font-size: 12px; cursor: pointer; }
.deck-tray-cards { display: flex; flex-wrap: wrap; gap: 8px; }
.deck-empty { color: var(--mut); font-size: 13px; padding: 6px 2px; }
.deck-mini {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 150px;
  padding: 5px 8px 5px 5px;
  border-radius: 9px;
  border-left: 3px solid;
  background: var(--chip);
  cursor: pointer;
  transition: transform .1s ease, background .2s;
}
.deck-mini:hover { transform: translateY(-1px); background: rgba(224, 87, 79, .12); }
.deck-mini-art {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 5px;
  display: grid;
  place-items: center;
  overflow: hidden;
}
.deck-mini-img { width: 100%; height: 100%; object-fit: cover; }
.deck-mini-glyph { font-size: 14px; color: rgba(255, 250, 235, .9); }
.deck-mini-name {
  flex: 1;
  min-width: 0;
  font-size: 11.5px;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.deck-mini-count { font-size: 11px; font-weight: 700; color: var(--acc); flex: none; }

.deck-pool-card { cursor: pointer; }
.deck-pool-card .card-art .deck-count {
  background: rgba(20, 18, 16, .82);
  color: rgba(240, 236, 228, .85);
}
.deck-pool-card .card-art .deck-count.has { background: rgba(98, 198, 207, .92); color: #0d1626; }
.deck-pool-card.maxed { filter: grayscale(.6) opacity(.5); cursor: default; }
.deck-pool-card.maxed:hover { transform: none; }

@media (max-width: 680px) {
  .deck-slots { margin-left: 0; width: 100%; }
  .deck-tray { position: sticky; bottom: 8px; z-index: 20; max-height: 38vh; overflow-y: auto; box-shadow: 0 -8px 24px rgba(0, 0, 0, .35); }
  .deck-tray.collapsed { max-height: 46px; overflow: hidden; }
  .deck-tray-toggle { display: inline; }
  .deck-mini { width: calc(50% - 4px); }
  .deck-bar-spacer { display: none; }
  .deck-name-input { flex: 1 1 100%; }
}

/* ---- Leaderboards ---- */
.leaderboard {
  padding: 26px clamp(14px, 4vw, 40px) 80px;
  max-width: 1100px;
  margin: 0 auto;
}
.lb-loading { color: var(--mut); padding: 40px 0; text-align: center; }
.lb-head h2 { font-family: Cinzel, serif; font-weight: 600; font-size: 26px; margin: 0; }
/* Battle Leaderboards (Bot Leaderboard) and Economy Leaderboard are two
   independently-sortable tables stacked as separate sections, not one big
   table — each gets its own title + player-count line. */
.lb-section { margin-top: 30px; }
.lb-section:first-of-type { margin-top: 22px; }
.lb-section-title {
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: 17px;
  margin: 0;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Leaderboard section-header icons (Battle/Collector/Market/Economy) and the
   profile page's rank-icon row — sized/positioned like the other small
   currentColor icons (.mana-ico/.pan-ico) so they inherit whatever color
   context they're placed in. Base size is for the profile rank row's
   smaller badges; .lb-section-title's own icons run larger since they sit
   next to a 17px heading, not a 13px rank badge. */
.lb-ico { width: 20px; height: 20px; flex: none; }
.lb-section-title .lb-ico { width: 26px; height: 26px; }
.lb-sub { font-size: 12.5px; color: var(--mut); margin-top: 4px; }
.lb-scroll { overflow-x: auto; margin-top: 14px; }
.lb-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  white-space: nowrap;
}
.lb-table th, .lb-table td { padding: 10px 12px; border-bottom: 1px solid var(--line); }
.lb-table thead th {
  position: sticky;
  top: 0;
  color: var(--mut);
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
  background: var(--bg);
}
.lb-table th.lb-num { cursor: pointer; user-select: none; }
.lb-table th.lb-num:hover { color: var(--ink); }
.lb-num { text-align: right; font-variant-numeric: tabular-nums; }
.lb-rank { width: 2.5em; text-align: right; color: var(--mut); font-variant-numeric: tabular-nums; }
.lb-user { font-weight: 600; }
/* Row's whole avatar+name cell is one clickable button -> that player's profile. */
.lb-user-link {
  display: flex;
  align-items: center;
  gap: 9px;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-weight: inherit;
  color: inherit;
  cursor: pointer;
}
.lb-user-link:hover span { color: var(--acc); }
.lb-table th.sorted, .lb-table td.sorted { color: var(--acc); }
.lb-table td.sorted { font-weight: 700; }
.lb-table tbody tr:hover { background: var(--chip); }
.lb-table tr.lb-me { background: color-mix(in srgb, var(--acc) 14%, transparent); }
.lb-table tr.lb-me td { color: var(--ink); }
/* Attached to the username span itself, not the .lb-user cell — the cell's
   .lb-user-link button is display:flex (a block-level box), so appending
   "(you)" after it pushed the text onto its own line below the name instead
   of sitting inline right after it. */
.lb-table tr.lb-me .lb-username::after { content: ' (you)'; color: var(--acc); font-weight: 400; font-size: .85em; }

/* ---- Profile page (#/user/<username>) ---- */
.profile {
  padding: 26px clamp(14px, 4vw, 40px) 80px;
  max-width: 1180px;
  margin: 0 auto;
}
.profile-head { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; }
.profile-info { display: flex; flex-direction: column; gap: 8px; }
.profile-name { font-family: Cinzel, serif; font-weight: 600; font-size: 26px; margin: 0; }
/* .online-dot's default 6px margin (styles.css ~229) reads fine at the
   leaderboard's small row-text size, but sits too close to the much
   larger profile-name heading here — more breathing room specifically
   in this context. */
.profile-name .online-dot { margin-left: 12px; }
.profile-rank-row { display: flex; align-items: center; flex-wrap: wrap; gap: 16px; font-size: 13px; color: var(--mut); }
.profile-rank-item { display: inline-flex; align-items: center; gap: 9px; }
/* Same gold/silver/bronze read as the card-rarity metal palette. */
.profile-medal {
  display: inline-flex;
  flex: none;
  vertical-align: middle;
  align-items: center;
  justify-content: center;
  /* Fixed integer px, not em/aspect-ratio — a fractional box (e.g. 22.09px)
     can rasterize its border-radius clip a hair off-square on some
     browsers/zoom levels, showing up as a "sliced" circle. */
  width: 22px;
  height: 22px;
  border-radius: 50%;
  overflow: hidden;
  border: 2px solid;
  font-weight: 700;
  font-size: 13px;
}
.profile-medal.gold   { color: #7c5a16; background: linear-gradient(145deg, #f7e7ae, #d4a643); border-color: #d4a643; }
.profile-medal.silver { color: #3a3f46; background: linear-gradient(145deg, #ffffff, #c6d0dc); border-color: #c6d0dc; }
.profile-medal.bronze { color: #3a2410; background: linear-gradient(145deg, #ecb27c, #b3763e); border-color: #b3763e; }
/* Rank 4+ (no medal) — the rank row now shows icon+badge only, no label
   text, so this needs its own small pill rather than relying on trailing
   "#N" text to carry the number visually. */
.profile-rank-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--mut);
  font-weight: 600;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.profile-stats { display: flex; gap: 24px; flex-wrap: wrap; margin-top: 2px; }
.profile-stat { display: flex; flex-direction: column; gap: 2px; }
.profile-stat b { font-size: 18px; font-variant-numeric: tabular-nums; }
.profile-stat span { font-size: 11px; color: var(--mut); text-transform: uppercase; letter-spacing: .04em; }
/* Binder/Decks tab strip below Card Display (see profileScreen()). Own
   visual identity rather than reusing the navbar .tab/.tab.active rules —
   those carry navbar-specific spacing/hover assumptions that don't fit an
   in-page section switch. */
.profile-tabs {
  display: flex;
  gap: 6px;
  margin: 30px 0 0;
  border-bottom: 1px solid var(--line);
}
.profile-tab {
  padding: 10px 4px;
  margin-right: 18px;
  border: none;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--mut);
  font-family: Cinzel, serif;
  font-size: 14px;
  letter-spacing: .03em;
  cursor: pointer;
}
.profile-tab:hover { color: var(--ink); }
.profile-tab.active { color: var(--ink); border-bottom-color: var(--acc); }

.profile-binder-head {
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: 16px;
  color: var(--mut);
  margin: 30px 0 14px;
}

/* Decks tab — accordion of complete+valid saved decks (see
   profileDecksTabHtml() / profileDeckRowHtml() in app.js). No indicator of
   which deck is active anywhere here, by design — see plan doc. */
.profile-decks-empty { color: var(--mut); font-size: 13px; padding: 20px 2px; }
.profile-deck-list { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; }
.profile-deck-row {
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--panel);
  overflow: hidden;
}
.profile-deck-title {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px 14px;
  border: none;
  background: none;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.profile-deck-name {
  flex: none;
  font-family: Cinzel, serif;
  font-size: 14px;
  font-weight: 600;
}
.profile-deck-chips {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 11px;
}
/* Reuses the .chip-pan/.chip-asc/.chip-rar color language from the binder
   filter chips (border+text tinted to the pantheon accent / rarity metal,
   transparent fill) — same "everything is color-coded" convention, just
   non-interactive (no hover/cursor, these are pure stats not filters). */
.profile-deck-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink);
  white-space: nowrap;
}
.profile-deck-chip.chip-asc { border-color: var(--line); color: var(--mut); }
.profile-deck-chip .pan-ico, .profile-deck-chip .asc-ico { width: 12px; height: 12px; flex: none; }
.profile-deck-chip .rar-ico { width: 11px; height: 11px; flex: none; border-radius: 50%; }
/* Expand/collapse transition — see profileDeckRowHtml()'s own comment for
   why this wrapper is always in the DOM rather than conditionally
   rendered. max-height is capped generously above any real deck's
   rendered height (5-column grid of card faces, largest legal deck_size)
   rather than measured, since this is plain CSS with no JS height probe;
   .open swaps it in, collapsed state clips to 0 via overflow:hidden. */
.profile-deck-cards-wrap {
  max-height: 0;
  overflow: hidden;
  transition: max-height .3s ease;
}
.profile-deck-cards-wrap.open { max-height: 2000px; }
/* Full card faces (binderCard()), not the compact deck-tray tile — fixed 5
   columns per row regardless of container width, per design. */
.profile-deck-cards {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 10px;
  padding: 0 14px 14px;
}
@media (max-width: 560px) {
  .profile-deck-cards { grid-template-columns: repeat(2, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  .profile-deck-cards-wrap { transition-duration: .001s; }
}
/* Third child of .profile-head (flex-wrap already there) — claims the
   spare width to the right of avatar+info on desktop, wraps to its own
   row below them on narrow viewports. */
/* Cards here reuse binderCard()'s face verbatim — its type sizes are tuned
   for .binder-grid's own 148px floor (minmax(148px, 1fr) above), so this
   panel uses that exact same width per card rather than inventing a
   smaller one; shrinking it further clips text/icons that were never
   designed to scale down. flex (not grid) + justify-content:flex-end so
   1 or 2 cards still hug the right edge instead of a fixed 3-track grid
   leaving reserved empty space where missing cards would be. */
.profile-display { flex: 1 1 480px; min-width: 480px; margin-left: auto; }
.profile-display-head { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-bottom: 10px; }
.profile-display-head h3 { font-family: Cinzel, serif; font-weight: 600; font-size: 14px; color: var(--mut); margin: 0; text-transform: uppercase; letter-spacing: .04em; }
.profile-display-edit { font-size: 11px; padding: 4px 12px; }
.profile-display-grid { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 10px; }
.profile-display-grid .binder-card { width: 148px; flex: none; }
.profile-display-empty { font-size: 12.5px; color: var(--mut); text-align: right; }
/* On narrow viewports .profile-head's flex-wrap drops this panel onto its
   own row below avatar+info — the desktop-only min-width/right-alignment
   above (meant to hug the spare width next to the avatar) then just
   crams everything into the right edge with no margin. Below the point
   .profile-head itself wraps (~560px, avatar+info's natural width), reset
   to a normal full-width, left-aligned block. */
@media (max-width: 560px) {
  .profile-display { min-width: 0; width: 100%; margin-left: 0; flex-basis: 100%; }
  .profile-display-head { justify-content: space-between; }
  .profile-display-grid { justify-content: flex-start; }
  .profile-display-empty { text-align: left; }
}

/* ---- Core Game (PvP matches) ---- */
.matches {
  padding: 26px clamp(14px, 4vw, 40px) 80px;
  max-width: 900px;
  margin: 0 auto;
}
/* The live board fills exactly the viewport below the header and never
   scrolls the page — every game-relevant element (both HP bars, both
   fields, hand, End Turn) has to stay on screen at once. --header-h is
   measured in JS (updateHeaderHeightVar()) since the header's real height
   varies with viewport width (the nav wraps at narrow widths), so it can't
   be a hardcoded constant here. Wider than .matches on purpose — see
   .match-field below, which trades the extra width for shorter cards
   instead of letting them stack into more vertical space than fits. */
.match {
  display: flex;
  flex-direction: column;
  height: calc(100vh - var(--header-h, 64px));
  height: calc(100dvh - var(--header-h, 64px));
  max-width: 1200px;
  margin: 0 auto;
  padding: 14px clamp(14px, 3vw, 40px) 10px;
  /* auto, not hidden — the layout below is engineered to fit exactly, but if
     some pathological state ever doesn't (e.g. many passive buttons wrapping
     to several rows), this scrolls just the board internally as a fallback
     rather than silently clipping End Turn out of reach. */
  overflow-y: auto;
  overflow-x: hidden;
}
.matches h2 { font-family: Cinzel, serif; font-weight: 600; font-size: 26px; margin: 0 0 4px; }
.duel-eyebrow {
  font-family: Cinzel, serif;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--mut);
  margin: 0 0 18px;
}
.duel-matches-heading { font-family: Cinzel, serif; font-weight: 600; font-size: 19px; margin: 36px 0 14px; }
.duel-hero-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; }
/* Opponent-select cards. object-fit:cover fills the card fully from the
   card-N.jpg art (tall enough source images that top-center cropping
   never letterboxes), so no blur/mask is needed to hide empty margins.
   mix-blend-mode:color on a solid accent overlay (.duel-hero-tint below)
   imposes the same hue/saturation on every image while preserving each
   one's own luminance detail — a CSS filter duotone was tried first and
   rejected because it's sensitive to each image's own histogram and gave
   a visibly different shade per card. */
.duel-hero {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 5;
  border-radius: 22px;
  border: 1px solid color-mix(in srgb, var(--acc) 22%, transparent);
  background: color-mix(in srgb, var(--panel) 12%, transparent);
  cursor: pointer;
  overflow: hidden;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  transition: box-shadow .35s ease, transform .2s ease, border-color .35s ease;
}
.duel-hero-content { position: absolute; inset: 0; opacity: .58; }
.duel-hero-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  border-radius: inherit;
  -webkit-user-drag: none;
  user-drag: none;
  pointer-events: none;
}
.duel-hero-tint {
  position: absolute;
  inset: 0;
  background: var(--acc);
  mix-blend-mode: color;
  opacity: .78;
  transition: opacity .4s ease;
}
.duel-hero:not(:disabled):not(.locked):hover .duel-hero-tint,
.duel-hero:not(:disabled):not(.locked):focus-visible .duel-hero-tint { opacity: 0; }
.duel-hero:not(:disabled):not(.locked):hover,
.duel-hero:not(:disabled):not(.locked):focus-visible {
  box-shadow: 0 0 0 1px var(--acc), 0 0 36px 3px color-mix(in srgb, var(--acc) 50%, transparent);
  transform: translateY(-3px);
}
.duel-hero:disabled { cursor: default; opacity: .55; }
.duel-hero.locked { cursor: not-allowed; }
.duel-hero.locked .duel-hero-tint { opacity: .9; background: color-mix(in srgb, var(--acc) 60%, #000); }
.duel-hero-label {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.duel-hero-name {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: 19px;
  letter-spacing: .03em;
  color: #efe9dc;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .85), 0 1px 10px rgba(0, 0, 0, .5);
}
.duel-hero-lock-ico { width: 16px; height: 16px; flex: none; opacity: .8; }
.duel-hero-sub {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--acc);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .85), 0 1px 10px rgba(0, 0, 0, .5);
}
.duel-hero-sub .mana-ico { width: 1em; height: 1em; }
.match-list { display: flex; flex-direction: column; gap: 8px; }
/* A grid, not flex space-between — space-between only pins the first/last
   column to the row's edges and leaves the middle (status) column floating
   at whatever position the two neighbors' own widths push it to, which
   drifted a different amount on every row since opponent names vary a lot
   in length ("Training Bot" vs "Pantheon Bot (Mesopotamian)"). Fixed
   column widths here make status and HP line up into real columns instead. */
.match-list-row {
  display: grid;
  grid-template-columns: 1fr 84px 120px;
  align-items: center;
  gap: 12px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px 16px;
  cursor: pointer;
  font: inherit;
  color: inherit;
  text-align: left;
  width: 100%;
}
.match-list-row:hover { border-color: var(--acc); }
.match-list-row.pending { border-style: dashed; opacity: .85; }
.match-list-opp { font-family: Cinzel, serif; font-weight: 600; font-size: 14.5px; letter-spacing: .02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.match-list-status { color: var(--mut); font-size: 13px; text-align: center; }
.match-list-code { display: block; font-variant-numeric: tabular-nums; letter-spacing: .06em; color: var(--acc); font-size: 12px; margin-top: 2px; }
.match-list-hp { font-variant-numeric: tabular-nums; color: var(--mut); font-size: 13px; text-align: right; }
.match-pending {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  max-width: 360px;
  margin: 40px auto;
  text-align: center;
}
.match-pending .pvp-code-display { width: 100%; }
.match-pending h2 { font-family: Cinzel, serif; font-weight: 600; font-size: 26px; margin: 0; }
@media (max-width: 480px) {
  .match-list-row { grid-template-columns: 1fr 64px; }
  .match-list-status { display: none; }
}
@media (max-width: 620px) {
  .duel-hero-row { grid-template-columns: repeat(2, 1fr); }
}
.match-list-empty { color: var(--mut); font-size: 13px; padding: 20px 0; }

.match-topbar { flex: none; display: flex; align-items: center; gap: 16px; }
.match-hp { flex: 1; display: flex; align-items: center; gap: 10px; font-size: 13px; }
.match-hp span { flex: none; width: 4.5em; color: var(--mut); }
/* .me now sits on the left, .opp on the right (see matchScreen()/
   matchAnimScreen() in app.js) — each label points to its own outer edge
   (You: far left, opponent: far right) with the HP number pointing inward
   toward the round indicator, so .opp is the one that needs the reversed
   internal order + right-aligned label now. */
.match-hp.opp { flex-direction: row-reverse; }
.match-hp.opp span { text-align: right; }
.match-hp-bar { flex: 1; height: 8px; border-radius: 999px; background: var(--chip); overflow: hidden; }
.match-hp-fill { height: 100%; background: var(--acc); transition: width .4s; }
.match-hp b { flex: none; width: 2.5em; text-align: center; font-variant-numeric: tabular-nums; }
.match-round {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  font-family: Cinzel, serif;
  font-weight: 600;
  color: var(--mut);
  font-size: 13px;
}
/* Round timer — inherits .match-round's Cinzel font-family. Tabular-nums so
   the "M:SS" text doesn't jitter in width as digits tick down every second
   (would otherwise nudge the Log/Undo lines below it sideways slightly). */
.match-round-timer { font-variant-numeric: tabular-nums; font-size: 12px; }
.match-round-timer.urgent { color: #ff5a4a; }
.match-log-toggle {
  border: none;
  background: none;
  padding: 0;
  font-family: inherit;
  font-weight: 400;
  font-size: 10px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--mut);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.match-log-toggle:hover { color: var(--acc); }
.match-log-toggle:disabled { opacity: .5; cursor: default; }
.match-log-toggle:disabled:hover { color: var(--mut); }
/* Undo — a real small button (not another text link like .match-log-toggle
   above it), with its own margin-top so it reads as a clearly separate
   control rather than a second line of the same link, not just relying on
   .match-round's small 3px gap. There's ample width in this topbar column
   for a proper button; the whole point is not needing to squeeze in next
   to .match-actions-row further down. */
.match-round-undo-slot { display: block; }
.match-undo-btn {
  margin-top: 6px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--chip);
  padding: 3px 10px;
  font-family: inherit;
  font-weight: 600;
  font-size: 10px;
  letter-spacing: .03em;
  text-transform: uppercase;
  color: var(--ink);
  cursor: pointer;
}
.match-undo-btn:hover { border-color: var(--acc); color: var(--acc); }
.match-undo-btn:disabled { opacity: .5; cursor: default; }
.match-undo-btn:disabled:hover { border-color: var(--line); color: var(--ink); }

/* Battle log overlay — same position:fixed/backdrop/click-outside-closes
   pattern as .inspect-overlay, so it never disturbs the fixed-viewport
   match layout underneath (see .match above) regardless of how long the
   log gets; the panel scrolls internally instead. */
.match-log-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 9, 7, .58);
  backdrop-filter: blur(7px);
  display: grid;
  place-items: center;
  z-index: 60;
  padding: 20px;
  /* No entrance animation — this can stay open through several unrelated
     render() calls (e.g. a match poll landing while it's up), each of which
     recreates it from scratch; see the .screen comment above for the same
     root cause. */
}
.match-log-panel {
  width: min(480px, 100%);
  max-height: min(600px, 80vh);
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
  overflow: hidden;
}
.match-log-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 18px;
  border-bottom: 1px solid var(--line);
}
.match-log-head h3 { margin: 0; font-family: Cinzel, serif; font-size: 17px; }
.match-log-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 14px 18px 18px; }
.match-log-empty { color: var(--mut); font-size: 13px; }
.match-log-round { margin-bottom: 16px; }
.match-log-round:last-child { margin-bottom: 0; }
.match-log-round-title { font-family: Cinzel, serif; font-weight: 600; color: var(--acc); font-size: 13px; margin-bottom: 6px; }
.match-log-lines { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }
.match-log-lines li { font-size: 12.5px; color: var(--ink); line-height: 1.4; }

/* App-wide replacement for window.confirm()/alert() (see confirmDialog()/
   alertDialog() in app.js) — same overlay/panel visual language as the
   battle log and passive-info popups above (.match-log-overlay's dark
   backdrop, .match-log-panel's rounded dark card), just without the
   scrollable body/header split those need and this doesn't. Used from
   every screen, not just the match one, despite living next to the
   match-log rules here for locality. */
.dialog-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 9, 7, .58);
  backdrop-filter: blur(7px);
  display: grid;
  place-items: center;
  z-index: 70;
  padding: 20px;
  /* No entrance animation — a confirm dialog (e.g. sacrifice-card) often
     sits open through a background match poll's render() call, which
     recreates this element from scratch each time and was retriggering the
     fade-in, reading as the dialog flickering/reloading while it's up. */
}
.dialog-panel {
  width: min(380px, 100%);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 20px;
}
.dialog-message { margin: 0 0 18px; font-size: 14px; line-height: 1.5; color: var(--ink); white-space: pre-line; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 10px; }

/* Notification sidebar (see notifSidebarHtml() in app.js) — a new,
   full-height panel pattern (existing dropdowns/dialogs are compact and
   anchored, not full-height). UNLIKE .dialog-overlay above, this element is
   ALWAYS present in the DOM — visibility is toggled purely via the .open
   class (exact same pattern as .tab-dropdown/.tab-dropdown-panel further
   up this file), never by conditionally including it in render()'s output.
   That's what makes both the slide-in AND slide-out actually animate: a
   transition needs a real from-state and to-state on the same element: if
   the element is torn down and rebuilt via innerHTML on open/close (as an
   earlier version of this did, keyed off state.notifSidebarOpen in
   render()'s output), there's nothing to transition FROM/TO — it just
   appears or vanishes instantly, and a keyframe animation bound to a
   one-shot class became unreliable depending on exactly when the
   synchronous innerHTML write landed relative to the browser's next paint.
   Background polling/marking-read/deleting still rebuild this markup from
   scratch on every render(), but since the .open class itself doesn't
   change on those renders, the transition never re-fires. */
.notif-sidebar-scrim {
  position: fixed;
  inset: 0;
  z-index: 75;
  background: rgba(10, 9, 7, 0);
  backdrop-filter: blur(0);
  display: flex;
  justify-content: flex-end;
  visibility: hidden;
  pointer-events: none;
  transition: background-color .22s ease, backdrop-filter .22s ease, visibility 0s linear .22s;
}
.notif-sidebar-scrim.open {
  background: rgba(10, 9, 7, .58);
  backdrop-filter: blur(7px);
  visibility: visible;
  pointer-events: auto;
  transition: background-color .22s ease, backdrop-filter .22s ease;
}
.notif-sidebar-panel {
  width: min(380px, 100vw);
  height: 100vh;
  height: 100dvh;
  background: var(--panel);
  border-left: 1px solid var(--line);
  box-shadow: -14px 0 34px rgba(0, 0, 0, .4);
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform .24s cubic-bezier(.2, .7, .2, 1);
}
.notif-sidebar-scrim.open .notif-sidebar-panel {
  transform: translateX(0);
}
@media (prefers-reduced-motion: reduce) {
  .notif-sidebar-scrim, .notif-sidebar-scrim.open, .notif-sidebar-panel, .notif-sidebar-scrim.open .notif-sidebar-panel {
    transition-duration: .001s;
  }
}
.notif-sidebar-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 18px 20px;
  border-bottom: 1px solid var(--line);
}
.notif-sidebar-head h3 { margin: 0; font-family: Cinzel, serif; font-weight: 600; font-size: 16px; }
.notif-sidebar-list { flex: 1; overflow-y: auto; padding: 6px 0; }
.notif-empty { padding: 30px 20px; text-align: center; color: var(--mut); font-size: 13px; }
.notif-row {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 20px;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
}
.notif-row:hover { background: var(--chip); }
.notif-row.unread { background: color-mix(in srgb, var(--acc) 8%, transparent); }
.notif-row.unread::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--acc);
}
.notif-row-icon { flex: none; width: 20px; height: 20px; color: var(--mut); margin-top: 1px; }
.notif-row-icon svg { width: 100%; height: 100%; }
.notif-row-body { flex: 1; min-width: 0; }
.notif-row-text { font-size: 13px; line-height: 1.4; color: var(--ink); }
.notif-row-time { margin-top: 3px; font-size: 11px; color: var(--mut); }
.notif-row-delete {
  flex: none;
  width: 20px; height: 20px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: none;
  color: var(--mut);
  font-size: 16px; line-height: 1;
  cursor: pointer;
}
.notif-row-delete:hover { color: var(--acc); }

.match-passive-info-panel { width: min(380px, 100%); }
.match-passive-info-panel .match-log-head h3 { display: flex; align-items: center; gap: 8px; font-size: 15px; }
.match-passive-info-icon { display: inline-flex; flex: none; }
.match-passive-info-icon .pan-ico { width: 18px; height: 18px; }
.match-passive-info-desc { margin: 0 0 10px; font-size: 13.5px; line-height: 1.5; color: var(--ink); }
.match-passive-info-kind { margin: 0; font-size: 12px; color: var(--mut); font-style: italic; }

/* Ancient Wisdom peek overlay (see peekOverlay()) — reuses .match-log-overlay/
   .match-log-panel for the modal chrome, wider than the passive-info panel
   since it needs to fit two full card faces side by side. Cards render at a
   fixed size here (unlike .match-slot's --field-card-w, which is sized for
   the live board and would be too small to read in a modal). */
.match-peek-panel { width: min(420px, 100%); }
.match-peek-cards { display: flex; justify-content: center; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
.match-peek-card { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.match-peek-card .match-slot { width: 110px; margin: 0; }
.match-peek-choice-row { display: flex; flex-direction: column; gap: 6px; width: 110px; }
.match-peek-choice-btn {
  padding: 6px 8px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--mut);
  font-size: 11.5px;
  cursor: pointer;
}
.match-peek-choice-btn:hover { border-color: var(--acc); color: var(--ink); }
.match-peek-choice-btn.active { border-color: var(--acc); background: color-mix(in srgb, var(--acc) 16%, transparent); color: var(--ink); font-weight: 600; }
.match-peek-panel .btn-primary { display: block; margin: 0 auto; }
/* The win/loss message, centered over the board and fading on its own after
   a few seconds — position:absolute so it never takes part in .match's flex
   layout at all, unlike the old inline row it replaced (which reserved/
   released a whole row's height exactly when the match ended, the single
   biggest layout jump in this screen). pointer-events:none so it can't
   block the "Back to Matches" button underneath even while still visible.
   No CSS animation here — runMatchResultFade() (app.js) drives the opacity
   directly every frame from elapsed wall-clock time instead, specifically
   because a @keyframes animation restarts from 0% every time render()
   recreates this element (a full innerHTML replace, same as everywhere else
   in this app), which is what made the message appear to flash/reappear
   repeatedly if anything else caused a re-render while it was up. Plain
   text with a glow, not a panel — a background box read as more chrome than
   this needed. */
.match-result-overlay {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  z-index: 5;
}
.match-result-text {
  font-family: Cinzel, serif;
  font-weight: 700;
  font-size: clamp(28px, 5vw, 52px);
  color: #cdb264;
  text-align: center;
  text-shadow:
    0 0 16px rgba(255, 255, 255, .85),
    0 0 40px rgba(255, 255, 255, .5),
    0 4px 20px rgba(0, 0, 0, .6);
}

/* Battle-phase / next-round announcement (see runBattleAnimationSteps()'s
   'phaseAnnounce'/'nextRoundAnnounce' stages and maybeAnnounceRoundOne() in
   app.js) — same centered-overlay layout as .match-result-overlay above,
   but a @keyframes fade instead of a wall-clock-driven one: each of these
   stages is a single fresh render() with a fixed cgSleep() duration (not a
   multi-second overlay that has to survive unrelated re-renders the way the
   win/loss message does), so a self-contained CSS animation is simpler and
   correct here — it only ever plays once, for the lifetime of that one
   freshly-mounted element. */
.match-phase-announce {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  z-index: 6;
}
@keyframes ptcg-phase-announce { 0% { opacity: 0; transform: scale(.94); } 12% { opacity: 1; transform: scale(1); } 85% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(1.02); } }
.match-phase-announce-text {
  font-family: Cinzel, serif;
  font-weight: 700;
  font-size: clamp(22px, 4.2vw, 40px);
  text-align: center;
  /* Matches runBattleAnimationSteps()'s cgSleep(2200) for 'phaseAnnounce'. */
  animation: ptcg-phase-announce 2.2s ease-in-out both;
}
/* Alarm variant (battle phase begins) — red, matching the existing damage
   accent (.match-dmg-float) but darker/stronger for an "alert" read.
   Next-round variant reuses the app's own gold accent — a plain
   continuation, not an alarm. */
.match-phase-announce.alert .match-phase-announce-text {
  color: #ff5a4a;
  text-shadow: 0 0 16px rgba(255, 90, 74, .75), 0 0 40px rgba(255, 90, 74, .4), 0 4px 20px rgba(0, 0, 0, .6);
}
.match-phase-announce.next-round .match-phase-announce-text {
  color: var(--acc);
  text-shadow: 0 0 16px rgba(255, 255, 255, .85), 0 0 40px rgba(255, 255, 255, .5), 0 4px 20px rgba(0, 0, 0, .6);
}
/* Matches maybeAnnounceRoundOne()'s / runBattleAnimationSteps()'s cgSleep(1000) for 'nextRoundAnnounce'. */
.match-phase-announce.next-round .match-phase-announce-text { animation-duration: 1s; }
/* Passive-event variant (Aztec/Norse.../interactive passives triggering in
   the 'passives' stage) — a third, neutral tone distinct from both the red
   alarm and the gold round-transition, matched to this stage's own
   cgSleep(1600) pacing in app.js. */
.match-phase-announce.passive-event .match-phase-announce-text {
  font-size: clamp(17px, 3vw, 26px);
  color: #62c6cf;
  text-shadow: 0 0 16px rgba(255, 255, 255, .7), 0 0 36px rgba(255, 255, 255, .35), 0 4px 20px rgba(0, 0, 0, .6);
  animation-duration: 1.6s;
}
/* Last-5-seconds round timer countdown — reuses .match-phase-announce's
   overlay positioning but with its own short, punchy per-second animation
   (startMatchTimerTicker() remounts a fresh element for each digit, so this
   replays once per second rather than once for the whole 5s). */
.match-phase-announce.timer-urgent .match-phase-announce-text {
  font-size: clamp(48px, 10vw, 96px);
  color: #ff5a4a;
  text-shadow: 0 0 20px rgba(255, 90, 74, .8), 0 0 50px rgba(255, 90, 74, .45), 0 4px 20px rgba(0, 0, 0, .6);
  animation: ptcg-timer-urgent .9s ease-out both;
}
@keyframes ptcg-timer-urgent { 0% { opacity: 0; transform: scale(1.4); } 25% { opacity: 1; transform: scale(1); } 80% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(.9); } }

/* Which pantheon's passive is currently active, for both sides at once —
   opponent on the left mirroring their HP bar's position in the topbar
   above, mine on the right. Automatic passives (Norse/Aztec/Celtic/Japanese)
   had no visible indicator anywhere before this; the three interactive ones
   already show as an action button on my own side while usable, so this
   chip skips those specifically to avoid saying the same thing twice (see
   cgPassiveInfoChip()'s skipIfButtonShown in app.js). */
.match-passive-status { flex: none; display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 6px; min-height: 22px; }
.match-passive-status-side { display: flex; }
.match-passive-status-side.opp { justify-content: flex-end; }
/* A <button>, not a plain label — clicking it opens the explainer overlay
   (passiveInfoOverlay() in app.js), so it needs to shed the browser's
   default button chrome (font/background) while keeping the pill look. */
.match-passive-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1px solid;
  border-radius: 999px;
  padding: 3px 10px 3px 8px;
  font: inherit;
  font-size: 11px;
  white-space: nowrap;
  background: none;
  cursor: pointer;
}
.match-passive-chip:hover { background: color-mix(in srgb, currentColor 12%, transparent); }
/* An interactive passive already used this round — still shown (a support
   card being present is the constant this row relies on to never resize
   itself away, see cgPassiveInfoChip() in app.js), just dimmed to signal
   "not actionable right now" without removing it. */
.match-passive-chip.used { opacity: .45; }
.match-passive-chip .pan-ico { width: 12px; height: 12px; }

/* Holds both field rows, centered in whatever space is left over after the
   other fixed-height rows (topbar, hand, piles, actions). Field cards are a
   fixed clamp()-based size (see .match-slot below), NOT stretched to fill
   available height — a flex-grown height combined with a width cap silently
   broke the 5:7 card ratio whenever a row had more vertical room than the
   cap allowed for (a short-wide viewport, or simply fewer competing elements
   on screen at that moment), and the ratio must never distort. On a very
   short viewport where the leftover space is less than what the fixed card
   size needs, .match's own overflow-y:auto (see above) is the fallback —
   a rare internal scroll, never a squashed card. */
.match-board {
  position: relative; /* anchors .match-result-overlay, which centers over the board without taking part in this layout at all */
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 16px;
  margin-top: 12px;
  /* visible, not hidden — the clash-stage pulse glow (.match-slot.clashing,
     filter:drop-shadow) legitimately extends past a card's own box, and
     hidden was clipping it off at the board's edge. updateFieldCardSizeVar()
     (app.js) now measures the real layout and shrinks cards with exact
     math (not a guessed formula), so the overflow this was originally
     guarding against — justify-content:center spilling content both upward
     into the topbar and downward into the row below — shouldn't happen in
     practice anymore; .match's own overflow-y:auto is still there as an
     outer fallback (a rare internal scroll, not a visual clip) if some
     untested state ever does overflow this after all. */
  overflow: visible;
}
.match-field { display: flex; justify-content: center; gap: 14px; }
.match-slot-wrap { display: flex; flex-direction: column; align-items: center; gap: 6px; }
/* One shared header row between the two field rows (see cgFieldLabelsRow()
   in app.js) instead of one above each — "Support"/"Battle 1" etc. mean the
   same column in both rows, so a duplicate label above each was a whole
   extra row of height saying the same thing twice. Column widths must match
   .match-slot's exactly (same --field-card-w var, same gap) or the labels
   drift out of alignment with the slots above/below them. */
.match-field-labels { display: flex; justify-content: center; gap: 14px; }
.match-field-labels .match-slot-label {
  flex: none;
  width: var(--field-card-w, clamp(85px, 15dvh, 155px));
  text-align: center;
  font-size: 9px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--mut);
}
.match-field-labels .match-slot-label.support { color: var(--acc); font-weight: 700; }
/* Support plays by different rules than the three battle slots (no Devotion
   cost, never fights, drives the pantheon passive) and should read as a
   distinct role at a glance. outline (not border/padding) so it can't
   change the wrap's box size and throw off the other three columns'
   alignment in the row — an occupied slot's own border already carries its
   card's pantheon color (inline style, can't be overridden from here
   without losing that), so this frames the card instead of fighting its
   border for the same property. Tight (offset near 0, no background tint)
   — the shared label above already carries most of the distinction now. */
.match-slot-wrap.support {
  outline: 1px solid color-mix(in srgb, var(--acc) 32%, transparent);
  outline-offset: 1px;
  border-radius: 11px;
}
.match-slot.empty.support { border-color: color-mix(in srgb, var(--acc) 55%, var(--line)); }
/* A fixed size (clamp, not flex-grown) is what keeps both the 5:7 card
   ratio and the card face's internal proportions (art crop, text sizing)
   honest in every context — the normal round view AND the battle-reveal/
   clash animation share this exact rule (.match-flip-slot below matches it),
   so a card is never a different shape depending on which view is showing
   it. --field-card-w is set in JS (updateFieldCardSizeVar()) only when the
   clamp() default doesn't actually fit the current row of optional content
   (devotion/passives row, pile strip) below it; width still only ever
   shrinks, never stretches to fill, so the ratio can't break either way. */
.match-slot {
  position: relative;
  flex: none;
  width: var(--field-card-w, clamp(85px, 15dvh, 155px));
  aspect-ratio: 5 / 7;
  border-radius: 10px;
  /* Dashed = genuinely empty/playable; occupied/hidden switch to a solid
     border below so "nothing here" reads distinctly from "something here,
     just concealed" (§15 Hidden Information) rather than looking identical. */
  border: 2px dashed var(--line);
  background: var(--chip);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.match-slot.empty { display: grid; place-items: center; }
.match-slot.own { cursor: pointer; }
.match-slot.own:hover { border-color: var(--acc); }
.match-slot.targetable { border-color: var(--acc); box-shadow: 0 0 0 2px color-mix(in srgb, var(--acc) 35%, transparent); cursor: pointer; }

/* Occupied slots + hand cards render the exact same full card face every
   other view uses (.card-top/.card-art/.card-name/.card-divider/.card-meta/
   .holo — see binderCard()/bigCardHtml() in app.js) rather than a stripped
   custom face, so Ascension/pantheon/rarity all show here too. Background +
   shadow mirror .summon-card (the closest existing footprint) exactly. */
/* container-type:inline-size turns this box into a query container — every
   chrome size below (padding, gap, font-sizes, icon sizes) is expressed in
   cqw (% of this container's own width) instead of fixed px, so a hand
   card, a field card, and a desktop-vs-mobile card all resolve to the
   exact same proportions regardless of their absolute width. This directly
   fixes a real, reported bug: .card-art is `flex:1` (fills whatever's left
   after the fixed chrome rows), so when chrome was fixed-px it ate a much
   bigger relative share of a small hand card than a big field card, giving
   .card-art a different effective aspect ratio — and since it's
   `object-fit:cover`, a different ratio meant a visibly different crop of
   the same source image between hand and field cards on the same screen.
   Percentages derived from this project's own live desktop rendering
   (129px, the unshrunk CSS default at a typical window height) so nothing
   changes at the size nobody complained about; every other size now
   matches it instead of drifting.
   Padding/gap themselves live on .match-card-inner (below), NOT here on
   the element that declares container-type — cq units only resolve
   against the NEAREST ANCESTOR container, never against the element
   establishing containment itself; padding in cqw set directly here fell
   back to resolving against the *viewport* (no other container above it in
   the tree), producing padding many times too large and collapsing the
   whole card to nothing (caught live, not by inspection — every card on
   the board went blank). .match-card-inner is a genuine child (see
   cgCardFaceInner() in app.js), so its own cqw padding/gap correctly
   resolve against this box instead. .match-flip-front (below) mirrors the
   same split on its own container-type, since it renders the identical
   card face. */
.match-slot.occupied, .match-hand-card {
  container-type: inline-size;
  box-sizing: border-box;
  border: 2px solid var(--line);
  border-radius: 10px;
  background:
    radial-gradient(140% 110% at 50% 10%, rgba(255, 255, 255, .08), transparent 45%),
    radial-gradient(160% 130% at 50% 108%, rgba(0, 0, 0, .55), transparent 58%),
    #161310;
  box-shadow:
    var(--glow, 0 12px 28px rgba(0, 0, 0, .25)),
    inset 0 1.5px 0 rgba(255, 255, 255, .16),
    inset 0 -4px 9px rgba(0, 0, 0, .45);
  overflow: hidden;
}
.match-slot.occupied.alt, .match-hand-card.alt, .match-flip-front.alt { background: #f4f1ea; }
.match-slot.occupied.alt .card-name, .match-hand-card.alt .card-name, .match-flip-front.alt .card-name { color: #1c1a16; }
/* gap matches .inspect-card's own proportion (16px of its 460px reference
   width = 3.48%) exactly, not the smaller value used before — verified
   live against the actual "big single view" card: with the old, smaller
   gap, .card-art (still `flex:1`, fills whatever's left over) claimed
   64.7% of card height vs .inspect-card's 60.1%, a 4.6-point gap that
   matches 4 gaps x (3.48-2.33)% to the decimal. That's what was skewing
   .card-art's own aspect ratio landscape-vs-portrait relative to the
   reference (measured 0.955 here vs 1.044 on .inspect-card) — not a
   font-size problem, so font sizes are untouched here to protect
   legibility on an already-small card face. */
.match-card-inner { box-sizing: border-box; width: 100%; height: 100%; padding: 4.65cqw; display: flex; flex-direction: column; gap: 3.48cqw; }
/* max(), not plain cqw — cqw alone is exactly proportional, which is what
   fixed the crop-ratio bug above, but a real phone's *actual* usable
   height (Safari reserves real space for its own chrome, especially
   before any page scroll ever happens — no headless test viewport this
   session ever modeled that) can be meaningfully less than every viewport
   this was tested against, and at the smaller resulting card sizes plain
   cqw text goes below any readable size (confirmed live: 3.3px at a
   390x650 viewport). Floors are this project's own pre-cqw sizes for
   these exact selectors — already-accepted absolute minimums, not new
   guesses. Below the size where the floor engages, chrome stops shrinking
   in lockstep with the card and .card-art's crop drifts slightly off the
   .inspect-card reference again — a deliberate trade-off, legibility over
   perfect crop match at the extreme end, same one every other card view
   in this app already makes via its own clamp() floor. */
.match-slot .card-top, .match-hand-card .card-top, .match-flip-front .card-top { font-size: max(7px, 5.43cqw); letter-spacing: .1em; }
.match-slot .card-meta .pan-ico, .match-hand-card .card-meta .pan-ico, .match-flip-front .card-meta .pan-ico { width: max(13px, 10.08cqw); height: max(13px, 10.08cqw); }
.match-slot .card-top .power, .match-hand-card .card-top .power, .match-flip-front .card-top .power { font-size: max(11px, 8.53cqw); }
.match-slot .card-art, .match-hand-card .card-art, .match-flip-front .card-art { border-radius: 7px; }
.match-slot .card-glyph, .match-hand-card .card-glyph, .match-flip-front .card-glyph { font-size: 20.16cqw; }
.match-slot .card-name, .match-hand-card .card-name, .match-flip-front .card-name { font-size: max(9.5px, 7.36cqw); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.match-slot .card-divider, .match-hand-card .card-divider, .match-flip-front .card-divider { margin: 0; }
.match-slot .card-meta, .match-hand-card .card-meta, .match-flip-front .card-meta { font-size: max(6px, 4.65cqw); letter-spacing: .03em; }
.match-slot .asc-ico, .match-hand-card .asc-ico, .match-flip-front .asc-ico { width: 1.6em; height: 1.6em; }

/* Sealed-card texture — same repeating-radial-gradient as the pack-back seal
   (.flip-back .seal), so a hidden field card reads as "concealed" using
   visual language the player already learned from opening packs, not as an
   empty/broken slot. */
.match-slot.hidden-card { border-style: solid; border-color: rgba(230, 200, 120, .32); }
.match-card-back {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  background: repeating-radial-gradient(circle, rgba(230, 200, 120, .08) 0 6px, transparent 6px 14px), #161310;
}
.match-card-back .logo-ico { width: 38%; height: 38%; color: #c8a552; }

.match-buff {
  position: absolute;
  top: 4px;
  right: 4px;
  z-index: 1;
  background: var(--acc);
  color: var(--bg);
  font-size: 9px;
  font-weight: 700;
  border-radius: 999px;
  padding: 1px 5px;
}

/* Devotion (left), the End Turn button (center), and interactive passive
   controls (right) share one row instead of stacking as three separate
   pieces — saves a whole row of vertical height, spent back on bigger
   field/hand cards (see the widened clamp()s on .match-slot/
   .match-hand-card). This row's height no longer needs a phantom-element
   trick to stay stable as devotion/passives come and go, the way the old
   separate row above the hand did — .match-actions-center always has a
   real button in it (End Turn, Back to Matches, or the anim hint sharing
   this same slot — see cgActionsRowHtml() in app.js), which anchors the
   height on its own regardless of what the side columns are showing. */
.match-actions-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.match-actions-side { flex: 1 1 0; min-width: 0; display: flex; align-items: center; flex-wrap: wrap; row-gap: 4px; }
.match-actions-side.right { justify-content: flex-end; }
.match-actions-center { flex: none; }
/* Holds Back to Matches + Next Match side by side once a bot match has
   finished (see cgMatchFinishedActionsHtml() in app.js). Must stay
   horizontal at every breakpoint, including the <480px grid below —
   .match-actions has a min-height tuned to exactly one button's rendered
   height (see its own comment above); stacking these two vertically would
   grow that cell taller than the battle-animation view's hint text
   occupies in the same slot and reintroduce the board-resize jitter that
   min-height was added to fix. */
.match-post-actions { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 10px; width: 100%; }
.match-devotion-row { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; }
.match-devotion-label { font-size: 11px; color: var(--mut); text-transform: uppercase; letter-spacing: .06em; }
/* Devotion is the central resource this whole game is spent on (Deity/God
   plays gate directly on it), so its count needs to read at a glance, not
   blend in as one more quiet outlined pill among devotion/passive chips —
   a filled background (not just an outline) and a bigger, bold, high-
   contrast number are what make the number itself the thing that pops. */
.match-devotion-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1.5px solid;
  border-radius: 999px;
  padding: 4px 11px 4px 8px;
  font-size: 12px;
  background: color-mix(in srgb, currentColor 16%, transparent);
}
.match-devotion-chip .pan-ico { width: 15px; height: 15px; }
.match-devotion-chip b { font-size: 15px; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }

.match-passives { max-width: 100%; display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 8px; }
/* cgPassiveControls() only ever renders a button when that passive is
   actually usable right now (active support + not yet used this round) —
   so every button here needs to read as "available," not blend into the
   .btn-ghost default (plain grey, easy to miss on a busy board). Once armed,
   the button itself is swapped out for .match-hint (see cgPassiveControls()
   in app.js) rather than shown alongside it, so there's no "armed" variant
   of this button to style. */
/* A pill-shaped button that wraps its label to two lines reads as broken,
   not as text that just wraps — nowrap + ellipsis makes it clip cleanly
   (as a last resort; see .lbl-short below for the actual mobile fix) rather
   than balloon into a mis-shapen blob (the original Greek Olympian Strategy
   report). */
.match-passive-btn { display: inline-flex; align-items: center; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Two label variants always render (see cgPassiveControls() in app.js);
   only one is shown at a time, picked by viewport width — the short one
   (max-width:480px below) is short enough to fit this pill on one line at
   mobile sizes without shrinking the font to the point of illegibility. */
.match-passive-btn .lbl-short { display: none; }
.match-passive-btn:hover { opacity: .85; }
.match-hint { max-width: 100%; font-size: 12.5px; color: var(--mut); display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 8px; }
/* Two hint-text variants always render (see cgPassiveControls() in app.js),
   same pattern as .match-passive-btn .lbl-full/.lbl-short — only one shown
   per viewport. */
.match-hint .hint-short { display: none; }

.match-hand {
  flex: none;
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: center;
  gap: 10px;
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  /* No min-height needed — cgHandRowHtml() (app.js) always renders a real
     .match-hand-card in this row (a hidden phantom when the hand is empty),
     so its height is the actual aspect-ratio computation every time, not a
     separately-guessed floor that could drift from it by a sub-pixel. */
  /* Safety net, not the common case: a hand large enough (round_hand_size is
     admin-tunable up to 20) to not fit even at the smallest clamp width
     scrolls sideways in its own row instead of wrapping to a second row,
     which would blow the fixed vertical budget this whole layout relies on. */
  overflow-x: auto;
}
/* position:relative is load-bearing, not decorative — cgCardFaceInner()'s
   .holo div is `position:absolute; inset:0`, and without a positioned
   ancestor here it escapes all the way up to the viewport as its containing
   block (no other ancestor up to #app declares `position`), rendering as a
   huge page-covering glow instead of a small per-card shine. Every other
   card view (.binder-card, .summon-card, .match-slot) already declares this;
   this one was missed when match cards adopted the shared card template.
   Width scales with viewport height (dvh), not just a flat px value, so the
   hand shrinks along with the fields on a short viewport instead of forcing
   the page to grow past it. */
.match-hand-card { position: relative; width: var(--hand-card-w, clamp(66px, 15dvh, 118px)); aspect-ratio: 5 / 7; cursor: pointer; flex: none; user-select: none; -webkit-user-select: none; touch-action: none; }
.match-hand-card:hover { border-color: var(--acc); }
.match-hand-card.armed { border-color: var(--acc); transform: translateY(-8px); box-shadow: 0 10px 20px rgba(0, 0, 0, .4); }
/* The phantom card is a real .match-hand-card (same width/aspect-ratio), so
   it takes up real space in the row — this wrapper is that reserved space's
   positioning context, with the actual "No cards" text absolutely
   positioned and centered over it instead of sitting in flow itself, so it
   reads as one empty slot rather than a card next to a text label. */
.match-hand-empty-wrap { position: relative; flex: none; }
.match-hand-phantom { visibility: hidden; }
.match-hand-empty-wrap .match-hand-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  text-align: center;
}
.match-hand-empty { color: var(--mut); font-size: 13px; padding: 4px 0; }

/* A capped, horizontally-scrolling strip — without this, a long discard pile
   (any game past a few rounds) would wrap to more rows and keep growing the
   page's required height, defeating the whole no-scroll layout. This one
   small strip scrolls sideways instead so everything around it stays fixed. */
.match-piles { flex: none; margin-top: 10px; }
/* A <button>, not a plain label — clicking it toggles the thumbnail strip
   below (see cgPilesHtml() in app.js), collapsed by default so the tray's
   images don't cost a row of height every round when nobody's looking at
   them; the counts in the title stay visible either way. */
.match-pile-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  border: none;
  background: none;
  padding: 0;
  margin-bottom: 4px;
  font: inherit;
  font-size: 11px;
  color: var(--mut);
  text-align: left;
  cursor: pointer;
}
.match-pile-title:hover { color: var(--ink); }
.match-pile-caret { flex: none; display: inline-flex; }
.match-pile-caret .pile-caret-ico { width: 13px; height: 13px; transition: transform .18s ease; }
.match-pile-caret.open .pile-caret-ico { transform: rotate(90deg); }
/* No min-height needed any more — the strip only exists in the DOM at all
   while open (see cgPilesHtml()), so there's no "Empty" state to guard a
   collapse against. */
.match-pile-strip { display: flex; flex-wrap: nowrap; align-items: center; gap: 6px; overflow-x: auto; padding-bottom: 2px; }
.match-pile-empty { color: var(--mut); font-size: 12.5px; }
.match-mini-card {
  flex: none;
  width: 32px;
  aspect-ratio: 5 / 7;
  border-radius: 5px;
  border: 1.5px solid var(--line);
  overflow: hidden;
  cursor: pointer;
}
.match-mini-card:hover { border-color: var(--acc); }
.match-mini-card .card-art { width: 100%; height: 100%; }

/* No longer sticky — .match itself is height-capped to the viewport and
   never scrolls (see .match above), so End Turn is just the last item in
   that fixed layout instead of needing to dock itself over scrolling
   content. */
.match-actions {
  flex: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 8px;
  margin-top: 12px;
  padding: 10px 0 4px;
  border-top: 1px solid var(--line);
  /* box-sizing:border-box, so this has to be the *total* box height: the
     End Turn button's own rendered height (~45px, from .btn-primary's 14px
     vertical padding + its line-height) plus this element's own padding+
     border above (measured empirically at 1366x768 — a first attempt at
     just the button's height alone, forgetting to add this container's own
     padding on top, undershot it and left the animation view's board with
     more room than the round view's, which showed up as field cards
     visibly changing size between the two). Matching it keeps this row the
     same height whether it's showing the button (round view) or just the
     shorter anim-hint text (matchAnimScreen() renders the hint in this
     exact same slot specifically so this doesn't need two different
     heights at all), which is what keeps everything above it — the whole
     board — from having a different amount of leftover space to work with. */
  min-height: 60px;
}
.match-waiting { text-align: center; color: var(--mut); font-size: 13px; }

/* ---- Battle-phase animation (reveal -> clash, see runBattleAnimation() in
   app.js) ---- */
/* Rendered inside .match-actions, in the exact spot the End Turn button
   occupies in the round view (see matchAnimScreen() in app.js) — no margin
   of its own, .match-actions already centers it and supplies the padding. */
.match-anim-hint {
  flex: none;
  text-align: center;
  color: var(--mut);
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: 12.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
}
/* A small 3D flip-card, same mechanism as the pack-opening reveal
   (.flip-inner.revealed { transform: rotateY(180deg) }), just sized for a
   field slot instead of a full card. */
.match-flip-slot {
  position: relative;
  flex: none;
  width: var(--field-card-w, clamp(85px, 15dvh, 155px));
  aspect-ratio: 5 / 7;
  perspective: 900px;
}
.match-flip-slot .flip-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform .6s cubic-bezier(.25, .7, .3, 1);
}
.match-flip-slot .flip-inner.revealed { transform: rotateY(180deg); }
.match-flip-slot .flip-face { position: absolute; inset: 0; backface-visibility: hidden; border-radius: 10px; overflow: hidden; }
.match-flip-back { display: grid; place-items: center; border: 2px solid rgba(230, 200, 120, .32); }
.match-flip-front {
  /* Same container-type as .match-slot.occupied/.match-hand-card above
     (kept as a separate declaration, not merged into that selector, since
     this element differs in a few unrelated ways — transform, ownership of
     border-radius/overflow via .flip-face). Padding/gap live on the shared
     .match-card-inner rule, not here — see the long comment on
     .match-slot.occupied above for why. */
  container-type: inline-size;
  transform: rotateY(180deg);
  box-sizing: border-box;
  border: 2px solid var(--line);
  background:
    radial-gradient(140% 110% at 50% 10%, rgba(255, 255, 255, .08), transparent 45%),
    radial-gradient(160% 130% at 50% 108%, rgba(0, 0, 0, .55), transparent 58%),
    #161310;
  box-shadow:
    var(--glow, 0 12px 28px rgba(0, 0, 0, .25)),
    inset 0 1.5px 0 rgba(255, 255, 255, .16),
    inset 0 -4px 9px rgba(0, 0, 0, .45);
}

/* Clash: a defeated card fades/shrinks out in place (no fly-to-discard —
   real scope creep for the payoff) and a floating "-N" pops over whichever
   slot took the damage. Both are self-contained @keyframes, not transitions,
   since this stage always starts from a freshly-mounted render(). */
@keyframes ptcg-match-defeat { 0% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(.82); } }
.match-slot-defeated { animation: ptcg-match-defeat .6s ease-in both; animation-delay: .3s; }
/* A card being SACRIFICED (opponent's hand-phase replay in the battle
   animation, and the player's own hand-phase sacrifice — see .match-slot-
   sacrificing below) reads as a deliberate, sudden loss rather than a
   passive fade: a sharp red flash first, then the same shrink-fade
   .match-slot-defeated already used. Distinct from clash's .clashing pulse
   (drop-shadow only, card itself stays put) — this one tints the card's own
   surface red via a filter, since drop-shadow alone read as too subtle at
   the smaller field-slot size. */
@keyframes ptcg-match-sacrifice {
  0%   { opacity: 1; transform: scale(1); filter: brightness(1) saturate(1) drop-shadow(0 0 0 rgba(255, 74, 58, 0)); }
  20%  { opacity: 1; transform: scale(1.04); filter: brightness(1.35) saturate(1.8) drop-shadow(0 0 22px rgba(255, 74, 58, .95)); }
  55%  { opacity: 1; transform: scale(1); filter: brightness(1.1) saturate(1.3) drop-shadow(0 0 10px rgba(255, 74, 58, .55)); }
  100% { opacity: 0; transform: scale(.82); filter: brightness(1) saturate(1) drop-shadow(0 0 0 rgba(255, 74, 58, 0)); }
}
/* This class (and its @keyframes) is only used for the OPPONENT's
   sacrifice fade during the battle-replay animation now — the player's
   own sacrifice uses cgSacrificeFlashCss()/runSacrificeFlash() in app.js
   instead (elapsed-time-driven inline styles, not a CSS animation),
   since a @keyframes animation restarts from 0% on every fresh DOM node
   render() mounts, which happened often enough during the own-sacrifice
   flow (matchAction()'s immediate render() before its request even
   completes) to read as the card flashing back to full opacity mid-fade. */
.match-slot-sacrificed { animation: ptcg-match-sacrifice .8s ease-in both; }
/* Resting state for a lane the sequential clash has already stepped past —
   same endpoint as .match-slot-defeated but with no animation, since a fresh
   render() on every clash step would otherwise replay the fade-out (flashing
   back to full opacity) on every lane that already resolved earlier. */
.match-slot-defeat-done { opacity: 0; transform: scale(.82); }
/* The lane currently resolving during the clash stage gets a brief glow so
   it reads as "this one, right now" while earlier/later lanes stay quiet. */
@keyframes ptcg-match-pulse { 0%, 100% { filter: drop-shadow(0 0 0 rgba(255, 138, 122, 0)); } 50% { filter: drop-shadow(0 0 14px rgba(255, 138, 122, .65)); } }
.match-slot.clashing { animation: ptcg-match-pulse .9s ease-in-out; }
@keyframes ptcg-match-dmg { 0% { opacity: 0; transform: translateY(4px) scale(.9); } 25% { opacity: 1; transform: translateY(-6px) scale(1.05); } 80% { opacity: 1; transform: translateY(-22px) scale(1); } 100% { opacity: 0; transform: translateY(-30px) scale(1); } }
.match-dmg-float {
  position: absolute;
  top: 40%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  color: #ff8a7a;
  font-family: Cinzel, serif;
  font-weight: 700;
  font-size: 20px;
  /* A real stroke, not just a soft shadow, so the number stays legible
     against any card art color/brightness underneath it — paint-order
     draws the stroke behind the fill so it reads as an outline, not a
     double-printed letter. text-shadow stays as an added depth cue. */
  -webkit-text-stroke: 1.5px rgba(0, 0, 0, .85);
  paint-order: stroke fill;
  text-shadow: 0 2px 6px rgba(0, 0, 0, .5);
  animation: ptcg-match-dmg 1.1s ease-out both;
  pointer-events: none;
}
/* Heal (Celtic Sacred Grove) variant — same float mechanism and keyframe
   as the base -N float, only the color changes so it never reads as
   ordinary clash damage. */
.match-dmg-float.heal { color: #6fd68a; }
/* Valhalla bonus-damage variant — same mechanism/keyframe again, but
   positioned on the HP bar that actually receives the bonus (see
   matchAnimScreen()'s valhallaMeHtml/valhallaOppHtml in app.js), not over
   the defeated card's slot the way .heal is; .hp-bar-float below overrides
   the positioning half of .match-dmg-float for that reason. */
.match-dmg-float.valhalla { font-size: 15px; color: #9fc2cc; }
/* The Norse pan-ico SVG (currentColor stroke) inherits .valhalla's color
   above with no extra work — just needs a size matched to this row's
   15px font-size, since .pan-ico has no global default. */
.match-dmg-float.valhalla .pan-ico { width: 13px; height: 13px; vertical-align: -2px; }

/* Valhalla's HP-bar anchor point. Deliberately NOT nested inside
   .match-hp-bar (which has overflow:hidden to clip .match-hp-fill's
   corners to the pill shape — a rising/fading float in there would be cut
   off after ~8px); anchored to the outer .match-hp instead, which has no
   overflow clipping. Positioned with explicit left/right rather than flex
   order so .match-hp.opp's flex-direction:row-reverse can't relocate it —
   the same trap a previous "-N pending" text badge hit and was removed
   for (see .match-hp-pending's comment below). */
.match-hp { position: relative; }
.match-dmg-float.hp-bar-float {
  top: -18px;
  left: auto;
  right: 8px;
  transform: none;
}
.match-hp.opp .match-dmg-float.hp-bar-float { right: auto; left: 8px; }

/* Passive support-card pulse (Teil 3a/3b/3c) — same pulse mechanism as
   .match-slot.clashing above, but tinted per-pantheon instead of a fixed
   red, via one modifier class per pantheon (see PANTH in app.js for the
   matching accent colors used elsewhere). */
@keyframes ptcg-passive-pulse { 0%, 100% { filter: drop-shadow(0 0 0 rgba(0, 0, 0, 0)); } 50% { filter: drop-shadow(0 0 14px var(--pp-glow)); } }
.match-slot-passive-pulse { animation: ptcg-passive-pulse .9s ease-in-out; }
.match-slot-passive-pulse.pp-Greek        { --pp-glow: rgba(59, 110, 165, .7); }
.match-slot-passive-pulse.pp-Norse        { --pp-glow: rgba(90, 125, 140, .7); }
.match-slot-passive-pulse.pp-Egyptian     { --pp-glow: rgba(185, 138, 58, .7); }
.match-slot-passive-pulse.pp-Japanese     { --pp-glow: rgba(181, 66, 58, .7); }
.match-slot-passive-pulse.pp-Celtic       { --pp-glow: rgba(63, 125, 84, .7); }
.match-slot-passive-pulse.pp-Aztec        { --pp-glow: rgba(47, 143, 126, .7); }
.match-slot-passive-pulse.pp-Mesopotamian { --pp-glow: rgba(138, 90, 51, .7); }

/* Pending damage marker (Teil 0) — a pulsing yellow segment trailing the
   normal HP fill, sized/positioned via inline left/width (computed in
   matchScreen(), app.js) to exactly cover the outstanding amount. Sits
   inside the same .match-hp-bar as .match-hp-fill, so it visually reads as
   "this much more is about to come off," not a separate bar. Pulses
   (rather than sitting at a flat opacity like the rest of the bar) because
   an 8px-tall sliver worth ~7% of the bar's width is easy to miss
   otherwise. (A separate "-N pending" text badge used to sit next to the
   HP number too, but .match-hp.opp's flex-direction:row-reverse mirrored
   its DOM position along with everything else, landing it between the HP
   number and the bar instead of its intended spot and breaking the
   mirrored name/number alignment the row-reverse trick depends on —
   removed; the pulsing bar segment alone is the pending-damage signal.) */
@keyframes ptcg-hp-pending-pulse { 0%, 100% { opacity: .5; } 50% { opacity: .95; } }
.match-hp-pending {
  position: absolute;
  top: 0;
  bottom: 0;
  background: #e6c85a;
  animation: ptcg-hp-pending-pulse 1.3s ease-in-out infinite;
  pointer-events: none;
}
.match-hp-bar { position: relative; }

/* New-hand deal (Teil 3, 'handDeal' stage) — a brief fade/rise-in per card,
   staggered by DOM order so they don't all pop at once. Self-contained
   @keyframes (like the clash-stage effects above), since this stage is
   always a freshly-mounted render(). */
@keyframes ptcg-hand-deal { 0% { opacity: 0; transform: translateY(10px) scale(.94); } 100% { opacity: 1; transform: translateY(0) scale(1); } }
.match-hand-card.dealt-in { animation: ptcg-hand-deal .4s ease-out both; }
.match-hand-card.dealt-in:nth-child(1) { animation-delay: 0s; }
.match-hand-card.dealt-in:nth-child(2) { animation-delay: .08s; }
.match-hand-card.dealt-in:nth-child(3) { animation-delay: .16s; }
.match-hand-card.dealt-in:nth-child(4) { animation-delay: .24s; }
.match-hand-card.dealt-in:nth-child(5) { animation-delay: .32s; }
.match-hand-card.dealt-in:nth-child(n+6) { animation-delay: .4s; }

/* ---- Mobile portrait: tighter gaps + a 2-row actions layout ---- */
/* Field/hand card widths themselves are computed in JS now (see
   updateFieldCardSizeVar() in app.js — it checks *width* fit, not just
   height, specifically for this case), so this block only needs to (a)
   free up a bit more of that width by shrinking the gaps between cards,
   and (b) fix the actions row: the 3-column devotion/End-Turn/passives
   layout above works fine wide, but on a narrow phone the passive
   button's own hint text ("Olympian Strategy · +5 to a battle card") has
   nowhere left to wrap once the fixed-width End Turn button and the
   devotion chip have both taken their share of the row — it collapsed
   into an unreadable wrapped blob. Below this width, devotion and
   passives share one row on their own (still left/right, same pairing as
   before) and End Turn becomes a full-width row underneath instead of
   competing with them for the same line. */
@media (max-width: 480px) {
  /* On a narrow phone, the field's 4-column width is the binding size
     constraint (confirmed live: field cards land at exactly
     (available width - 3 gaps) / 4 — vertical space is comparatively
     abundant), so reclaiming width directly grows the cards. .match's
     desktop padding is tuned for a much wider board with room to spare;
     4px is still a safe touch-margin against the screen edge on mobile. */
  .match { padding-left: 4px; padding-right: 4px; }
  .match-field, .match-field-labels { gap: 4px; }
  .match-hand { gap: 4px; }
  .match-board { gap: 10px; }

  .match-actions-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-areas: "devotion passives" "endturn endturn";
    row-gap: 8px;
    align-items: center;
  }
  .match-actions-row .match-actions-side { grid-area: devotion; }
  .match-actions-row .match-actions-side.right { grid-area: passives; }
  .match-actions-row .match-actions-center { grid-area: endturn; width: 100%; }
  .match-actions-row .match-actions-center .btn-primary,
  .match-actions-row .match-actions-center .btn-outline { width: 100%; }
  /* Overrides the blanket width:100% rule above for the two-button Back to
     Matches / Next Match pair — without this, each would independently
     claim 100% width and wrap/overflow instead of sharing the row. Wins
     over the general rule by specificity (one more class in the chain),
     regardless of source order. */
  .match-actions-row .match-actions-center .match-post-actions { display: flex; flex-direction: row; gap: 8px; width: 100%; }
  .match-actions-row .match-actions-center .match-post-actions .btn-primary,
  .match-actions-row .match-actions-center .match-post-actions .btn-outline { width: auto; flex: 1 1 0; min-width: 0; }
  /* The battle-reveal animation swaps this exact slot's content from the
     End Turn button to .match-anim-hint, a plain text line (see
     cgActionsRowHtml() in app.js — same slot, on purpose, so this row's
     height doesn't change between the round view and the animation). That
     equal-height guarantee only held with the desktop's single-row layout;
     on this mobile 2-row grid the "endturn" row shrank down to the hint's
     own ~1-line text height instead of the button's full padded height the
     moment the animation started, growing the field/hand cards into that
     newly-freed space and then shrinking them straight back the instant the
     button reappeared — read as the whole board pulsing every round. Giving
     the hint the exact same padding as .btn-primary keeps this row's height
     identical in both states. */
  .match-actions-row .match-actions-center .match-anim-hint {
    width: 100%;
    box-sizing: border-box;
    padding: 14px 0;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  /* Player name above the HP bar, not sharing a row with it — the desktop
     layout gives the name a fixed 4.5em column before the bar even starts,
     which on a narrow phone either truncates/wraps a longer opponent name
     ("Pantheon Bot (Greek)" wrapped across three lines in testing) and
     leaves the bar itself short. Grid instead of the desktop flex row: the
     name becomes its own full-width line, the bar and HP number share the
     row below (so the number doesn't cost the bar a whole separate row),
     and .opp mirrors it with the HP number kept on the bar's inner side
     (toward the round indicator) the same way the desktop row-reverse did. */
  .match-hp, .match-hp.opp {
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-areas: "name name" "bar num";
    column-gap: 6px;
    row-gap: 2px;
    align-items: center;
  }
  .match-hp.opp { grid-template-columns: auto 1fr; grid-template-areas: "name name" "num bar"; }
  .match-hp span {
    grid-area: name;
    width: auto;
    font-size: 10px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .match-hp-bar { grid-area: bar; }
  .match-hp b { grid-area: num; width: auto; font-size: 11px; }
  /* .match-dmg-float.hp-bar-float's desktop `top:-18px` is relative to
     .match-hp's full box, which here spans both the name row AND the
     bar row (see the grid-template-areas above) instead of just the bar
     — left as the desktop value it would sit up near the name line. Grid
     auto-placement would otherwise slot this plain (non-grid-placed)
     child into "bar num" alongside the fill/number, so it's pinned to
     its own row via grid-row explicitly, then nudged up just enough to
     clear that row without reaching into the name line above it. */
  .match-hp .match-dmg-float.hp-bar-float {
    grid-row: 2;
    top: -14px;
  }
  .match-round { font-size: 11px; gap: 2px; }

  /* The passive-status chips (top of board) read as oversized next to the
     shrunk cards below them — same info, smaller footprint. */
  .match-passive-chip { font-size: 9.5px; padding: 2px 8px 2px 6px; gap: 4px; }
  .match-passive-chip .pan-ico { width: 10px; height: 10px; }

  /* At field/hand card widths this small there's no real room for the
     ascension label text next to power even as an ellipsis (see .meta-left
     .ascension in the shared card rules, which now truncates rather than
     overflows at every size, but a 1-2 character ellipsis remnant still
     isn't worth the space it costs here) — power is the more load-bearing
     stat mid-match, so ascension goes icon-only, the same treatment
     .pan-txt already gets on these same card faces. Card number is dropped
     outright — identifying a specific print is not something a live match
     needs. */
  .match-slot .meta-left .ascension, .match-hand-card .meta-left .ascension { display: none; }
  /* .card-meta (pantheon icon + series icon + card number) and the divider
     above it drop entirely on mobile match cards, not just the number —
     the pantheon icon is redundant with the card's own border color
     (deco().frameCol is already pantheon-tinted), the series icon carries
     no information at all (every card is currently "Series I"), and a
     hairline divider has none either. Removing the whole row, not just
     thinning it, gives that height back to .card-art (still `flex:1`,
     claims whatever's left) and to the remaining rows (.card-top,
     .card-name) directly — the actual lever for both the art-crop ratio
     and text legibility on a card this small, not something further
     cqw/floor tuning on the *surviving* rows alone could reach. */
  .match-slot .card-divider, .match-hand-card .card-divider,
  .match-slot .card-meta, .match-hand-card .card-meta { display: none; }

  /* Short passive-button label (see CG_PASSIVE_LABEL_SHORT in app.js) — the
     full label already can't wrap (.match-passive-btn is nowrap+ellipsis
     everywhere now), but at mobile widths ellipsis-truncating it down to
     nothing recognisable was little better than the old wrapped blob. The
     short variant actually fits and stays legible. */
  .match-passive-btn .lbl-full { display: none; }
  .match-passive-btn .lbl-short { display: inline; }
  .match-passive-btn { font-size: 10.5px; padding: 7px 12px; }

  /* Same swap for the armed-passive hint text — the full wording ("Click
     one of your battle cards to buff it") still wrapped to 2 lines in the
     passives column here, blowing past the reserved row height below the
     moment a passive got armed. */
  .match-hint .hint-full { display: none; }
  .match-hint .hint-short { display: inline; }
  .match-hint { font-size: 11px; }
  /* The generic .btn-ghost padding (11px 22px) is sized for a standalone
     button, not a small "cancel" sharing a tight mobile column with hint
     text — its width was the difference between fitting the short hint on
     one line and wrapping to two, varying by a few px between browser font
     engines (confirmed: Chromium wrapped it, WebKit didn't, same content). */
  .match-hint .btn-ghost { padding: 5px 10px; font-size: 10px; }

  /* Devotion chips and the passive button/hint come and go turn to turn
     (a chip appears the moment a second same-pantheon card hits the field;
     the button appears/disappears as its passive becomes available or gets
     used) — on the wide desktop row that only ever changed the row's
     *content*, but on this stacked mobile layout it was changing the row's
     *height* under the player mid-game, which .match-board's leftover space
     (and therefore the field/hand card size) reacts to on every render,
     reading as the whole board constantly resizing while playing. Reserving
     a fixed height for both cells regardless of whether they're empty this
     turn keeps that row (and everything sized off the space left over for
     it) stable for the whole match instead of just the current turn. */
  /* minmax, not a flat 28px — devotion chips genuinely can wrap to a second
     line with several pantheons in play at once, and a hard cap would clip
     that rather than just letting the row grow for it (a rare case, but
     clipping content is worse than an inch of jitter on the rare turn it
     happens). The floor is what actually kills the common empty<->1-line
     jitter. */
  .match-actions-row { grid-template-rows: minmax(28px, auto) auto; }
  .match-actions-side, .match-actions-side.right { min-height: 28px; }
}

/* ---- Drag-and-drop (additive to click-to-arm — see the pointer wiring in
   wire()) ---- */
/* Applied to <body> for the duration of a drag so text/elements elsewhere on
   the board can't get selected while the pointer sweeps across them. */
body.match-dragging, body.match-dragging * { user-select: none !important; -webkit-user-select: none !important; }
.match-hand-card.dragging-source { opacity: .35; }
.match-drag-ghost {
  position: fixed;
  z-index: 60;
  width: 96px;
  transform: translate(-50%, -50%) rotate(-4deg) scale(1.06);
  pointer-events: none;
  box-shadow: 0 16px 32px rgba(0, 0, 0, .5);
}
.match-slot.drag-over { border-color: var(--acc); background: color-mix(in srgb, var(--acc) 16%, var(--chip)); }
/* A drop on an occupied own slot sacrifices the incumbent — a distinct
   warning color (vs. the plain accent highlight above) so mid-drag it reads
   as "this will replace what's here," not "this will just land here." */
.match-slot.drag-replaceable { box-shadow: inset 0 0 0 1px color-mix(in srgb, #ff8a5b 40%, transparent); }
.match-slot.drag-replaceable.drag-over { border-color: #ff8a5b; background: color-mix(in srgb, #ff8a5b 18%, var(--chip)); }

/* ---- Avatar picker ---- */
.avatar-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 9, 7, .58);
  backdrop-filter: blur(7px);
  display: grid;
  place-items: center;
  z-index: 55;
  padding: 20px;
  /* No entrance animation — same rationale as .dialog-overlay above: an
     unrelated render() while this is open recreates it and would retrigger
     the fade-in otherwise. */
}
.avatar-picker {
  width: min(480px, 92vw);
  max-height: 82vh;
  overflow-y: auto;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 22px;
}
.avatar-picker-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.avatar-picker-head h3 { font-family: Cinzel, serif; font-weight: 600; font-size: 18px; margin: 0; }
.avatar-picker-close {
  background: none;
  border: none;
  color: var(--mut);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
}
.avatar-picker-close:hover { color: var(--ink); }
.avatar-picker-hint { color: var(--mut); font-size: 12.5px; margin: 6px 0 16px; }

/* Hades' Offer modal — same background-photo treatment as #app itself
   (radial glow + luminosity-blended art, styles.css ~39-53), scaled down
   to this panel's own box instead of the full viewport. A dark scrim
   layer (plain rgba, not another blend mode) sits between the two so the
   headline/lede/buttons stay readable regardless of the source photo's
   own brightness — #app doesn't need one since its glow gradient is
   already sparse/dark by design, but this panel is small enough that the
   art fills the whole thing. */
.hades-offer-panel {
  position: relative;
  background-color: var(--panel);
  background-image:
    linear-gradient(rgba(10, 9, 7, .55), rgba(10, 9, 7, .55)),
    radial-gradient(ellipse at 50% 0%, rgba(181, 66, 58, .35), transparent 62%),
    url('uploads/hades-offer-bg.png');
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
  background-blend-mode: normal, normal, luminosity;
}
.hades-offer-panel .avatar-picker-head h3 {
  font-family: Cinzel, serif;
  font-weight: 700;
  font-size: clamp(24px, 4vw, 32px);
  letter-spacing: .04em;
}
.hades-offer-panel .lede {
  font-family: Cinzel, serif;
  font-style: italic;
  font-weight: 500;
  font-size: clamp(13px, 1.6vw, 15px);
  color: var(--mut);
  margin: 6px 0 16px;
  line-height: 1.5;
}
/* Opened from within the still-open Settings modal (z-index 56) — needs to
   stack visually on top of it, not just come after it in the DOM. */
.password-overlay { z-index: 57; }

/* ---- Settings modal ---- */
/* Same backdrop/panel/header/close-button shell as .avatar-overlay/
   .avatar-picker above — z-index 56, one above the avatar picker's own 55
   (both are simple centered modals of the same visual weight). No entrance
   animation, same rationale as every other overlay in this file: an
   unrelated render() while this is open recreates the DOM node from
   scratch and would otherwise retrigger a fade-in, reading as flicker. */
.settings-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 9, 7, .58);
  backdrop-filter: blur(7px);
  display: grid;
  place-items: center;
  z-index: 56;
  padding: 20px;
}
.settings-panel {
  width: min(420px, 92vw);
  max-height: 82vh;
  overflow-y: auto;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 22px;
}
.settings-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.settings-panel-head h3 { font-family: Cinzel, serif; font-weight: 600; font-size: 18px; margin: 0; }
.settings-panel-close {
  background: none;
  border: none;
  color: var(--mut);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
}
.settings-panel-close:hover { color: var(--ink); }
.settings-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 18px;
}
.settings-row:first-of-type { margin-top: 20px; }
.settings-mute-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 26px;
  height: 26px;
  padding: 0;
  border: none;
  background: none;
  color: var(--mut);
  cursor: pointer;
}
.settings-mute-btn:hover { color: var(--acc); }
.settings-row-label {
  flex: none;
  width: 44px;
  font-size: 13px;
  color: var(--mut);
}
/* Native range-input chrome, tinted via accent-color — matches the only
   other <input type=range> in the codebase (editor/editor.css's
   .range-row input[type=range]) rather than introducing custom
   ::-webkit-slider-thumb/::-moz-range-thumb styling. */
.settings-slider {
  flex: 1;
  accent-color: var(--acc);
}
.settings-row-val {
  flex: none;
  width: 40px;
  text-align: right;
  font-size: 12.5px;
  color: var(--mut);
  font-variant-numeric: tabular-nums;
}
.settings-pw-btn {
  display: block;
  width: 100%;
  margin-top: 26px;
  padding-top: 8px;
  font-size: 12.5px;
  text-align: center;
}

.pvp-code-display {
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: 26px;
  letter-spacing: .12em;
  text-align: center;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 14px;
  color: var(--acc);
  font-variant-numeric: tabular-nums;
  margin-top: 4px;
}
.pvp-code-copy { width: 100%; margin-top: 14px; }
.avatar-pick-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); gap: 10px; }
.avatar-pick {
  position: relative;
  aspect-ratio: 1;
  border-radius: 12px;
  overflow: hidden;
  border: 2px solid transparent;
  padding: 0;
  cursor: pointer;
  background: var(--chip);
}
.avatar-pick img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-pick .card-glyph { width: 100%; height: 100%; display: grid; place-items: center; font-size: 28px; }
/* Border color and the active/alt-art box-shadows are both set inline per
   card (pickerCardStyle() in app.js) — an inline style always wins outright
   over a class rule for the same property, so a CSS-side box-shadow rule
   here would just silently lose to it; hover stays class-based since
   nothing inline touches filter. */
.avatar-pick:hover { filter: brightness(1.2); }
.avatar-empty { color: var(--mut); font-size: 13px; padding: 10px 0; }
.avatar-clear { margin-top: 16px; width: 100%; }
/* Card Display picker's multi-select order badge (1/2/3) — .avatar-pick's
   own position:relative anchors it, shared with the (single-select) avatar
   picker above since neither renders it there. */
.display-pick-order {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--acc);
  color: var(--bg);
  font-size: 10px;
  font-weight: 700;
  display: grid;
  place-items: center;
}

/* ---- Inspect overlay ---- */
.inspect-overlay {
  position: fixed;
  inset: 0;
  background: rgba(10, 9, 7, .58);
  backdrop-filter: blur(7px);
  display: grid;
  place-items: center;
  z-index: 50;
  padding: 20px;
  /* Fallback once even the height-capped card (below) plus its footer
     still don't fit a very short/zoomed window — scrolls the overlay
     itself rather than clipping the buy/sacrifice/list controls out of
     reach with no way to get to them. */
  overflow-y: auto;
  /* No entrance animation — same rationale as .dialog-overlay above: an
     unrelated render() while this is open recreates it and would retrigger
     the fade-in otherwise. */
}
.inspect-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  /* Keeps centering correct if the overlay above ends up scrolling. */
  margin: auto;
}
.inspect-card {
  position: relative;
  box-sizing: border-box;
  border-style: solid;
  /* Width was previously capped only by viewport WIDTH (90vw) — on a
     short, wide viewport (a typical laptop screen, especially with
     browser chrome eating into it) that let the 5:7 card render taller
     than the available height, pushing the buy/sacrifice/list footer
     below the visible area with no indication it existed. Also capping
     by viewport height (accounting for the footer + gap + overlay
     padding taking up roughly the other 30%) keeps the whole stack
     on-screen without needing to scroll in the common case. */
  width: min(460px, 90vw, 50vh);
  aspect-ratio: 5 / 7;
  border-radius: 24px;
  border-width: 6px;
  background:
    radial-gradient(140% 110% at 50% 10%, rgba(255, 255, 255, .08), transparent 45%),
    radial-gradient(160% 130% at 50% 108%, rgba(0, 0, 0, .55), transparent 58%),
    #161310;
  padding: clamp(12px, 4.3vw, 22px);
  display: flex;
  flex-direction: column;
  gap: clamp(8px, 3.13vw, 16px);
  overflow: hidden;
  transition: transform .14s ease-out;
  /* Static rarity glow (colour from --metal-glow; common is transparent so it
     shows nothing). The pulsing variant lives on pack-reveal cards only — an
     always-on pulse here flickered against the overlay's backdrop-filter. */
  box-shadow: var(--rest-shadow), inset 0 1.5px 0 rgba(255, 255, 255, .16), inset 0 -4px 9px rgba(0, 0, 0, .45), 0 0 22px 2px var(--metal-glow);
}
/* Chrome sizes below scale with the card's own width formula (90vw below
   ~511px, capped at 460px above) so .card-art's leftover space — and
   therefore the artwork crop — stays identical across all viewports. */
.inspect-card .card-top { font-size: clamp(7px, 2.74vw, 14px); letter-spacing: .2em; }
.inspect-card .card-meta .pan-ico { width: clamp(16px, 5.87vw, 30px); height: clamp(16px, 5.87vw, 30px); }
.inspect-card .card-top .power { font-size: clamp(16px, 5.87vw, 30px); }
.inspect-card .card-art { border-radius: 16px; }
.inspect-card .card-glyph { font-size: clamp(50px, 21.13vw, 108px); text-shadow: 0 6px 26px rgba(0, 0, 0, .5); }
.inspect-card .card-name { font-size: clamp(15px, 5.87vw, 30px); }
.inspect-card .epithet {
  font-size: clamp(10px, 3.52vw, 18px);
  font-family: Cinzel, serif;
  font-weight: 500;
  font-style: italic;
  color: #9b9384;
  line-height: 1.4;
  text-align: center;
}
.inspect-card .card-meta { font-size: clamp(8px, 2.74vw, 14px); letter-spacing: .12em; }
.inspect-foot {
  display: flex;
  align-items: center;
  gap: 14px;
  color: rgba(240, 236, 228, .85);
  font-size: 14px;
}
.inspect-foot .sep { opacity: .5; }
.inspect-foot .hint { opacity: .7; }
/* Heavier items than the recycle/list text controls (avatar+name button,
   headline price, full-size Buy/Cancel button) need room to wrap rather
   than being squeezed onto one tight row. */
.market-inspect-foot { flex-wrap: wrap; justify-content: center; row-gap: 10px; }
.market-inspect-foot .market-seller { color: rgba(240, 236, 228, .85); }
.market-inspect-foot .market-price { font-size: 20px; }
.recycle-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid rgba(230, 193, 92, .5);
  background: rgba(230, 193, 92, .15);
  color: #e6c15c;
  font-size: 12px;
  cursor: pointer;
  font-family: inherit;
}
.recycle-btn:hover { background: rgba(230, 193, 92, .28); }
.inspect-recycle { display: inline-flex; align-items: center; gap: 8px; }
/* Never lets the value go below 1 or above owned-1 (also enforced by the
   listener in wire() as the player types, and by the server regardless) —
   a specific card's spares can be trimmed without an all-or-nothing choice. */
.recycle-qty-input {
  /* Wide enough for a 2-digit value plus the native spinner arrows — 3.2em
     was clipping e.g. "11" as soon as a card had double-digit duplicates. */
  width: 4.4em;
  padding: 5px 4px;
  border-radius: 8px;
  border: 1px solid rgba(230, 193, 92, .4);
  background: rgba(0, 0, 0, .25);
  color: #efe9dc;
  font: inherit;
  font-size: 12px;
  text-align: center;
}
.recycle-qty-input:focus { outline: none; border-color: rgba(230, 193, 92, .8); }

/* "List for sale" — same shape as .recycle-btn/.recycle-qty-input, but in
   the page's accent color rather than the sacrifice button's gold/red, so
   "sell to another player" reads as visually distinct from "melt for Faith". */
.inspect-list { display: inline-flex; align-items: center; gap: 8px; }
.list-price-input {
  /* Wide enough for a 5-digit price (market_max_price defaults to 5000
     but is admin-tunable up to 100000) plus the native spinner arrows —
     4.4em was fine for recycle-qty-input's 1-2 digit values but clipped a
     price like "12000" as soon as an admin raised the cap. */
  width: 6.4em;
  padding: 5px 6px;
  border-radius: 8px;
  border: 1px solid color-mix(in srgb, var(--acc) 40%, transparent);
  background: rgba(0, 0, 0, .25);
  color: #efe9dc;
  font: inherit;
  font-size: 12px;
  text-align: center;
}
.list-price-input:focus { outline: none; border-color: color-mix(in srgb, var(--acc) 80%, transparent); }
.list-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--acc) 50%, transparent);
  background: color-mix(in srgb, var(--acc) 15%, transparent);
  color: var(--acc);
  font-size: 12px;
  cursor: pointer;
}
.list-btn:hover { background: color-mix(in srgb, var(--acc) 25%, transparent); }
.list-btn .lb-ico { width: 1.1em; height: 1.1em; }

@media (max-width: 680px) {
  .inspect-overlay { padding: 14px; }
  .inspect-stack { gap: 10px; }
  .inspect-foot { gap: 8px; font-size: 12px; flex-wrap: wrap; justify-content: center; }
  .recycle-btn { padding: 4px 10px; font-size: 11px; }
  .list-btn { padding: 4px 10px; font-size: 11px; }
}

/* ---- Marketplace (#/market) ---- */
/* Same shell as .binder/.leaderboard (max-width + centered + matching
   padding) — this was missing entirely, so the market grid was rendering
   full viewport width with no side padding instead of respecting the
   site's usual content column. */
.market {
  padding: 26px clamp(14px, 4vw, 40px) 80px;
  max-width: 1180px;
  margin: 0 auto;
}
.market .binder-head { margin-bottom: 6px; }
/* "Your listings" now follows the Browse grid (not the page top like
   before), so it needs its own top margin — margin-bottom alone left no
   gap at all above it once the section order changed. */
.market-mine { margin-top: 34px; margin-bottom: 34px; }
.market-listable { margin-top: 34px; }
.market-mine .lb-section-title, .market-listable .lb-section-title { margin-bottom: 12px; }
.market-listable .lb-sub { margin-bottom: 16px; }
/* "Browse listings" heading + its sort controls share one row, so sort
   reads as scoped to this section rather than a page-wide control. */
.market-sort-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px 20px;
  margin: 30px 0 4px;
}
.market-browse-title { margin: 0; }
.market-sort { display: flex; align-items: center; gap: 8px; }
/* Plain caption, not a chip — establishes "these are sort controls" as
   distinct from the filter chips directly below, which change what's
   shown rather than what order it's shown in. */
.market-sort-label {
  color: var(--mut);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: .04em;
}

/* A listing card is intentionally bigger/more prominent than a binder
   card (220px vs 148px min — between binder and the 460px inspect card,
   the only other "larger" precedent in the game) since a listing is
   something special: another player's card, for sale. Each grid cell is
   the card face (.market-card-face, its own class — not bare .binder-card
   reuse, since .binder-card[data-binder] is a live wire() selector for
   the owned-card recycle/list overlay and must never match a market
   card by accident) plus a separate info row below it — nothing overlaid
   on the card itself. */
.market-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 24px 18px;
  margin-top: 14px;
}
.market-card-face {
  position: relative;
  aspect-ratio: 5 / 7;
  border-radius: 18px;
  border-width: 4px;
  background:
    radial-gradient(140% 110% at 50% 10%, rgba(255, 255, 255, .08), transparent 45%),
    radial-gradient(160% 130% at 50% 108%, rgba(0, 0, 0, .55), transparent 58%),
    #161310;
  padding: 11px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  overflow: hidden;
}
.market-card-face .card-top { font-size: 11px; letter-spacing: .16em; }
.market-card-face .card-meta .pan-ico { width: 24px; height: 24px; }
.market-card-face .card-top .power { font-size: 19px; }
.market-card-face .card-art { border-radius: 11px; }
.market-card-face .card-glyph { font-size: 46px; text-shadow: 0 4px 16px rgba(0, 0, 0, .45); }
.market-card-face .card-name { font-size: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.market-card-face .card-divider { margin: 1px 0; }
.market-card-face .card-meta { font-size: 10px; letter-spacing: .08em; }
.market-card-face .holo { border-radius: 17px; }
/* Seller sits above the card, price centered below it, buy/cancel as its
   own full-width row below that — matching the mockup's stacked layout
   rather than cramming seller+price+button into one tight info block. */
.market-listing { display: flex; flex-direction: column; align-items: stretch; gap: 10px; cursor: pointer; }
/* Avatar + clickable name -> seller's profile, same shape as .lb-user-link
   (leaderboard rows) — full-strength Cinzel text, not muted body text,
   since the seller is meant to read as a real presence, not incidental
   metadata. */
.market-seller {
  display: flex;
  align-items: center;
  gap: 9px;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  min-width: 0;
}
.market-seller:hover .market-seller-name { color: var(--acc); }
.market-seller-name {
  font-family: Cinzel, serif;
  font-weight: 600;
  letter-spacing: .02em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
/* Price as a standalone headline figure, centered under the card — extra
   vertical breathing room plus a soft matching-color glow behind it, so
   it reads as a small moment of its own rather than just another line of
   text sandwiched between the card and the buy button. */
.market-price {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 0;
  font-size: 24px;
  font-weight: 700;
  font-family: Cinzel, serif;
  color: #f0d98c;
  text-shadow: 0 0 18px rgba(240, 217, 140, .55);
}
.market-price .mana-ico { width: 1em; height: 1em; }
.market-buy-btn, .market-cancel-btn {
  padding: 10px 12px;
  font-size: 13px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  width: 100%;
}
.market-buy-btn .mana-ico { width: 1em; height: 1em; }
.market-buy-btn:disabled { opacity: .5; cursor: not-allowed; }
.market-empty { grid-column: 1 / -1; color: var(--mut); font-size: 13px; padding: 30px 0; text-align: center; }
/* Bring the minimum down from 220px (which would force an awkward
   single column on narrow phones) closer to — but still above — the
   148px binder baseline, so the "bigger and special" treatment still
   reads without breaking mobile usability. */
@media (max-width: 680px) {
  .market-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 18px 12px; }
  .market-card-face { padding: 8px; }
  .market-price { font-size: 18px; }
}

/* ---- Arcade (coinflip) ---- */
.arcade {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 14px;
  padding: clamp(28px, 5vh, 52px) 16px 70px;
}
/* Matches .oracle h2 / .oracle .lede (styles.css ~3231) — same Cinzel
   display scale and italic narration-style lede across both minigame
   screens, instead of Coinflip's smaller/plain-style heading reading as
   a different tier of screen. */
.arcade h2 { font-family: Cinzel, serif; font-weight: 600; font-size: clamp(24px, 4vw, 32px); margin: 0; }
.arcade .lede {
  font-family: Cinzel, serif;
  font-style: italic;
  font-weight: 500;
  font-size: clamp(13px, 1.6vw, 15px);
  color: var(--mut);
  max-width: 480px;
  margin: 0 auto 4px;
  line-height: 1.5;
}
.coin { width: 116px; height: 116px; perspective: 600px; }
.coin-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
}
.coin-face {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  display: grid;
  place-items: center;
  backface-visibility: hidden;
  border: 2px solid rgba(255, 240, 190, .5);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .35), inset 0 2px 6px rgba(255, 255, 255, .5);
}
.coin-face.front {                             /* heads = win */
  background: radial-gradient(circle at 35% 30%, #f4dd8f, #b8892f 70%);
  color: #3a2a08;
}
.coin-face.back {                              /* tails = lose */
  background: radial-gradient(circle at 35% 30%, #a9bccb, #3f5666 70%);
  border-color: rgba(210, 225, 235, .5);
  color: #14222c;
  transform: rotateY(180deg);
}
.coin-face .mana-ico { width: 56px; height: 56px; }
.coin-seal { font-size: 52px; line-height: 1; }
.coin-inner.spin-win  { animation: coin-win 1s cubic-bezier(.2, .7, .2, 1) forwards; }
.coin-inner.spin-lose { animation: coin-lose 1s cubic-bezier(.2, .7, .2, 1) forwards; }
@keyframes coin-win  { from { transform: rotateY(0); } to { transform: rotateY(1080deg); } }  /* lands heads */
@keyframes coin-lose { from { transform: rotateY(0); } to { transform: rotateY(1260deg); } }  /* lands tails */
.flip-result { font-size: 15px; min-height: 22px; font-weight: 600; }
.flip-result.win { color: #57b98a; }
.flip-result.lose { color: #e0574f; }
.flip-result.idle { color: var(--mut); font-weight: 400; }
.wager-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: center; }
.wager-label { color: var(--mut); font-size: 13px; }
.wager-chip {
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: inherit;
  cursor: pointer;
  font-size: 13px;
}
.wager-chip.active { background: var(--ink); color: var(--bg); }
.wager-input {
  width: 92px;
  padding: 8px 10px;
  border-radius: 9px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--ink);
  font-size: 14px;
  font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
  .coin-inner.spin-win, .coin-inner.spin-lose { animation: none; }
}

/* ---- Oracle's Trial (higher/lower push-your-luck minigame) ---- */
/* Layout mirrors .arcade (flex column, centered, natural scroll) —
   .oracle-betting is the stake-picking screen (no open trial),
   .oracle-active is the live-trial screen. Backdrop is a plain
   multi-layer `background` shorthand directly on each stage section (no
   ::before pseudo-element — that was tried earlier and was buggier: a
   position:fixed layer needs its own pointer-events:none to avoid
   swallowing every click, and stacking a photo behind it needs careful
   z-ordering). `background` accepts several comma-separated layers where
   the FIRST one painted is on TOP: layer 1 is the rarity-coloured radial
   glow, layer 2 is the stage photo (uploads/oracle-bg-<stage>.jpg)
   underneath it, shown at full opacity — no dimming/vignette. */
.oracle {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 24px;
  /* min-height, not a hard height cap — this screen scrolls like every
     other full-screen view in the game (.opening, .arcade) if its
     content is taller than the viewport. An earlier version capped this
     to height:100vh to force a "single screen, no scroll" layout, which
     instead repeatedly left content (most notably the balance footer)
     landing past an unreachable fold with no visible way to scroll to
     it. Content is never forced to shrink to fit here. */
  min-height: 100vh;
  min-height: 100dvh;
  padding: clamp(24px, 5vh, 52px) 16px 70px;
  box-sizing: border-box;
  transition: background .5s ease;
}
/* Betting screen: everything centers as one group. Base gap covers the
   tightly-related pairs (h2↔lede, wager-row↔begin-button); the two
   functionally distinct group boundaries (ladder→stake picker,
   button→balance) get their own margin on top of it below. */
.oracle-betting { gap: 18px; }
.oracle-betting .oracle-ladder { margin: 4px 0; }
.oracle-betting .opened-label { margin-top: 4px; }
/* Scoped, not a change to the shared .wager-row (also used by Arcade) —
   Oracle's larger overall scale left the stock 8px chip gap looking
   cramped next to everything else on this screen. */
.oracle-betting .wager-row { gap: 12px; }
.oracle-active { gap: 22px; }
.oracle-active .opened-label { margin-top: 4px; }
.oracle-stage-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}
.oracle-duel {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  width: 100%;
}
/* Title/flavour/ladder are supporting, secondary elements — the cards +
   power numbers below are the dominant thing on this screen, so these
   stay compact rather than competing for the same visual weight (Arcade's
   own h2/lede scale is the reference point here, not the reveal-grid's
   card-focused sizing). Still Cinzel throughout, matching every other
   display heading in the game. */
.oracle h2 {
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: clamp(24px, 4vw, 32px);
  margin: 0;
}
.oracle .lede {
  font-family: Cinzel, serif;
  font-style: italic;
  font-weight: 500;
  font-size: clamp(13px, 1.6vw, 15px);
  color: var(--mut);
  max-width: 480px;
  margin: 0 auto;
  line-height: 1.5;
}
.oracle-stage-title {
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: clamp(20px, 3.2vw, 27px);
  letter-spacing: .01em;
  margin: 0 0 4px;
}
.oracle-stage-flavor {
  font-family: Cinzel, serif;
  font-style: italic;
  font-weight: 500;
  font-size: clamp(13px, 1.6vw, 15px);
  color: var(--mut);
  max-width: 440px;
  margin: 0 0 14px;
  line-height: 1.4;
}
/* Glow colour per stage matches this exact card's own rarity-glow palette
   (deco()'s METAL table in app.js — common/uncommon/rare/legendary) so the
   escalation across Creature->God reads as the same visual language the
   rest of the game already uses for "this is getting rarer/stronger",
   just applied to ascension stage instead of card rarity. Photo sits
   underneath at full opacity, no vignette. .oracle-betting (pre-trial)
   shows the Creature stage's own backdrop — the first stage a trial will
   actually enter — rather than a photo-less glow-only screen. */
.oracle-betting {
  background:
    radial-gradient(ellipse at 50% 0%, rgba(120, 100, 200, .22), transparent 62%),
    url('uploads/oracle-bg-creature.jpg') center / cover no-repeat;
}
.oracle-stage-creature {
  background:
    radial-gradient(ellipse at 50% 0%, rgba(131, 135, 141, .30), transparent 62%),
    url('uploads/oracle-bg-creature.jpg') center / cover no-repeat;
}
.oracle-stage-demi {
  background:
    radial-gradient(ellipse at 50% 0%, rgba(179, 118, 62, .32), transparent 62%),
    url('uploads/oracle-bg-demi.jpg') center / cover no-repeat;
}
.oracle-stage-deity {
  background:
    radial-gradient(ellipse at 50% 0%, rgba(198, 208, 220, .34), transparent 64%),
    url('uploads/oracle-bg-deity.jpg') center / cover no-repeat;
}
.oracle-stage-god {
  background:
    radial-gradient(ellipse at 50% 0%, rgba(212, 166, 67, .38), transparent 66%),
    url('uploads/oracle-bg-god.jpg') center / cover no-repeat;
}
/* Mobile portrait: same background-size:200% zoom treatment as #app's own
   backdrop (styles.css ~46-51) instead of `cover` — the landscape-shaped
   photos otherwise crop too tightly at narrow/tall aspect ratios. Must
   physically sit AFTER the five shorthand `background:` rules above (a
   shorthand always overrides longhands declared earlier in the
   file, media query or not), which is why this isn't grouped with #app's
   own portrait override further up the file. */
@media (orientation: portrait) and (max-width: 680px) {
  .oracle-betting,
  .oracle-stage-creature,
  .oracle-stage-demi,
  .oracle-stage-deity,
  .oracle-stage-god {
    background-position: left top;
    background-repeat: no-repeat;
    background-size: 200%;
  }
}
/* Success-flash reuses this same glow: a brief, stronger pulse of it
   rather than a separate white full-screen overlay (which read as too
   harsh/disconnected from the scene in earlier attempts). Scoped to
   .oracle-active so it only ever layers onto a stage that already has a
   glow+photo background of its own. */
@keyframes oracle-glow-flash {
  0%   { filter: brightness(1) saturate(1); }
  40%  { filter: brightness(1.55) saturate(1.4); }
  100% { filter: brightness(1) saturate(1); }
}
.oracle-active.flashing {
  animation: oracle-glow-flash .6s ease-in-out both;
}
@media (prefers-reduced-motion: reduce) {
  .oracle-active.flashing { animation: none; }
}

/* Multiplier ladder — always visible, 4 segments, current stage
   highlighted, passed stages marked done. Kept compact/secondary (see the
   typography comment above) since the cards below are the focal point. */
.oracle-ladder {
  display: flex;
  gap: 8px;
  width: 100%;
  max-width: 440px;
}
.oracle-rung {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 6px;
  border-radius: 10px;
  border: 1px solid var(--line);
  background: var(--panel);
  opacity: .5;
  transition: opacity .3s ease, border-color .3s ease, background .3s ease;
}
.oracle-rung-name { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--mut); }
.oracle-rung-mult { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }
.oracle-rung.done {
  opacity: 1;
  border-color: rgba(230, 195, 100, .5);
  background: rgba(230, 195, 100, .08);
}
.oracle-rung.done .oracle-rung-mult { color: #e6c364; }
.oracle-rung.active {
  opacity: 1;
  border-color: var(--acc);
  background: var(--panel);
  box-shadow: 0 0 0 1px var(--acc) inset;
}

/* Cards row — the dominant element on screen. Two large flip-cards side
   by side, same width class as pack-opening's .flip-card (styles.css
   ~546) so the existing 3D-flip CSS applies unmodified. */
.oracle-cards-row {
  display: flex;
  /* .oracle-card is a column (flip-frame, then the power badge below
     it) that's always the same total height regardless of reveal state
     (the badge slot is always reserved — see .oracle-power-badge below),
     so plain center lines up both card FACES correctly without any
     manual offset. */
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 4vw, 32px);
  flex-wrap: wrap;
  width: 100%;
}
.oracle-vs {
  font-family: Cinzel, serif;
  font-weight: 700;
  font-size: 16px;
  color: var(--mut);
  letter-spacing: .08em;
}
/* .oracle-card: a plain flex column holding .oracle-flip-frame (the
   actual 3D flip-card) and .oracle-power-badge stacked below it —
   deliberately two separate elements, not one, so the aspect-ratio that
   sizes the flip-card doesn't also try to squeeze the badge's text
   underneath it into that same fixed box. */
.oracle-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: left;
}
/* Sized up from the base .flip-card (clamp(130px,27vw,184px)) — this is
   the clear visual centerpiece of the screen (requirement: cards + power
   numbers dominate), with real room to grow now that the layout isn't
   fighting a single-screen height cap. vw-based, not vh-based — the
   earlier vh-based sizing was specifically a "must fit one screen
   vertically" hack that no longer applies now that this screen scrolls
   like every other one in the game. */
.oracle-flip-frame.flip-card { width: clamp(150px, 30vw, 230px); }
/* text-align:left reset — .oracle (the whole screen) sets text-align:center
   for its own headings/lede, which as an inherited CSS property cascades
   all the way down into the card face too. .ascension (styles.css ~694)
   has flex:1 1 auto, so it stretches to fill .meta-left's full width —
   combined with the inherited center alignment, "CREATURE" would centre
   itself inside that wide box instead of sitting flush left next to its
   icon like every other card in the game (where no ancestor ever sets
   text-align:center). Set on .oracle-card above (inherited by the card
   face); every card-face rule below already positions its own children
   via flex, not text-align. */
/* Face-internal sizing (name/power/ascension/pan-label/series text, icon
   sizes, border width) is otherwise inherited verbatim from the shared
   `.flip-front ...` rules (styles.css ~768-778), tuned for the
   130-184px reveal-grid card — scaled here for the larger card size
   above, keeping the exact same rule shapes/units so this stays a pure
   scale-up, not a redesign. */
.oracle-card .flip-front { border-width: 4px; }
.oracle-card .flip-front .card-top { font-size: 12px; letter-spacing: .16em; }
.oracle-card .flip-front .card-meta .pan-ico { width: 26px; height: 26px; }
.oracle-card .flip-front .card-top .power { font-size: 21px; }
.oracle-card .flip-front .card-art { border-radius: 12px; }
.oracle-card .flip-front .card-glyph { font-size: clamp(52px, 8vw, 76px); }
/* text-align:center here specifically — .card-name is the one card-face
   element that's genuinely meant to be centered (matches every other
   card in the game), unlike .ascension above; the reset on .oracle-card
   would otherwise leave it flush left too. */
.oracle-card .flip-front .card-name { font-size: clamp(16px, 2.2vw, 21px); text-align: center; }
.oracle-card .flip-front .card-divider { margin: 2px 0; }
.oracle-card .flip-front .card-meta { font-size: 11px; letter-spacing: .08em; }
/* Large, high-contrast power number, placed BELOW the card (a sibling of
   .oracle-flip-frame within .oracle-card, not inside .flip-front) — the
   number IS the game, so it reads as its own standalone stat distinct
   from the small in-card .power figure on the flip-front's top row,
   rather than being folded into the card face itself. Always rendered
   (oracleCardHtml() in app.js emits a non-breaking-space placeholder for
   the face-down slot too) so both cards reserve the exact same slot
   height via this fixed min-height, regardless of reveal state —
   .oracle-cards-row's align-items:center then lines up both card FACES
   correctly without any manual offset. Only the actual number's
   visibility toggles with reveal state. */
.oracle-power-badge {
  margin-top: 10px;
  width: 100%;
  min-height: 1.1em;
  text-align: center;
  font-family: Cinzel, serif;
  font-weight: 700;
  font-size: clamp(26px, 5vw, 38px);
  font-variant-numeric: tabular-nums;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .5);
}
.oracle-card:has(.flip-inner:not(.revealed)) .oracle-power-badge { visibility: hidden; }

/* Fixed-height slot around the guess/claim/continue buttons, always
   present in the markup (oracleScreen() renders it empty during
   'resolving'/'flash'/'transitioning-lose') so the cards row never jumps
   up/down as the buttons appear and disappear between phases — reserves
   the tallest of the two possible button rows' real heights up front
   rather than a guessed round number. */
.oracle-action-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Tallest real content: .oracle-guess-btn's stacked icon+label (36px
     vertical padding + 26px icon + 6px gap + ~20px text line ≈ 88px). */
  min-height: 96px;
}
.oracle-guess-row, .oracle-choice-row {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
  justify-content: center;
}
/* Guess buttons — the game's two central, decision-defining controls, sized
   and set in Cinzel accordingly (matching every other display heading in
   the game) rather than the small generic .btn-solid scale. Icon sits
   above the label (a vertical arrow next to vertical text reads better
   stacked than side-by-side) — .lesser points down, .greater points up,
   replacing the previous left/right chevrons that implied the wrong axis
   for a "will the number go up or down" prediction. */
.oracle-guess-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 18px 40px;
  border-radius: 20px;
  border: none;
  background: var(--ink);
  color: var(--bg);
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: 17px;
  letter-spacing: .08em;
  cursor: pointer;
  transition: transform .12s ease, opacity .2s ease;
}
.oracle-guess-btn:hover:not(:disabled) { transform: translateY(-2px); }
.oracle-guess-btn:disabled { opacity: .4; cursor: default; transform: none; }
.oracle-guess-btn .oracle-arrow-ico { width: 26px; height: 26px; }
/* Tint each guess direction toward its own outcome colour — lesser cools
   toward blue, greater warms toward gold — so the two controls read as
   distinct choices at a glance, not just mirrored duplicates. */
.oracle-guess-btn.lesser  { background: linear-gradient(180deg, #3a4a5a, #26323f); }
.oracle-guess-btn.greater { background: linear-gradient(180deg, #6b5326, #4a3a1c); color: #f4e6c4; }

/* Claim/Continue — the payoff moment, same elevated Cinzel scale as the
   guess buttons above. */
.oracle-choice-btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 18px 36px;
  border-radius: 999px;
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: 17px;
  letter-spacing: .06em;
  cursor: pointer;
  transition: transform .12s ease, opacity .2s ease;
}
.oracle-choice-btn:hover:not(:disabled) { transform: translateY(-2px); }
.oracle-choice-btn:disabled { opacity: .4; cursor: default; transform: none; }
.oracle-choice-btn .mana-ico { width: 1em; height: 1em; vertical-align: -.1em; }
.oracle-choice-btn .pile-caret-ico { width: 20px; height: 20px; }
.oracle-choice-btn.claim {
  border: 1px solid rgba(230, 195, 100, .5);
  background: rgba(230, 195, 100, .1);
  color: #e6c364;
}
/* Max win (God stage cleared, no Continue left) — a pulsing gold glow
   marks this claim as the ceiling payout, same pulsing-box-shadow
   mechanism as .continue's nudge-to-push-on glow below, just gold instead
   of white since there's no further choice to nudge toward here. */
.oracle-choice-btn.claim.max-win {
  animation: oracle-max-win-glow 2.2s ease-in-out infinite;
}
@keyframes oracle-max-win-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(230, 195, 100, 0); }
  50%      { box-shadow: 0 0 22px 4px rgba(230, 195, 100, .55); }
}
.oracle-choice-btn.continue {
  border: none;
  background: var(--ink);
  color: var(--bg);
  /* Tempting, gently pulsing white glow — Continue is the push-your-luck
     option (stake stays at risk for a bigger multiplier), so it gets the
     more inviting treatment; Claim (the safe cash-out) deliberately stays
     static beside it, so the contrast itself nudges toward pushing on. */
  animation: oracle-continue-glow 2.2s ease-in-out infinite;
}
@keyframes oracle-continue-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
  50%      { box-shadow: 0 0 22px 4px rgba(255, 255, 255, .55); }
}
@media (prefers-reduced-motion: reduce) {
  .oracle-choice-btn.continue,
  .oracle-choice-btn.claim.max-win { animation: none; }
}

/* ==================================================================
   Achievements (see achievementsScreen() in app.js) — ADMIN-ONLY for
   this initial rollout. Composed almost entirely from existing classes
   (.progress/.progress-bar, .oracle-choice-btn.claim, .match-pile-caret,
   the .profile-deck-row-style always-in-DOM accordion) — only the
   handful of .achv-* rules below are genuinely new.
   ================================================================== */
/* Page-width container — same clamp()'d side padding, capped max-width,
   and centering every other screen root uses (.binder/.leaderboard/etc.)
   so page content doesn't stretch edge-to-edge on wide viewports. */
.achievements {
  padding: 26px clamp(14px, 4vw, 40px) 80px;
  max-width: 1100px;
  margin: 0 auto;
}
.achv-lede { color: var(--mut); font-size: 14px; margin: 6px 0 22px; }
.achv-group { border: 1px solid var(--line); border-radius: 12px; background: var(--panel); overflow: hidden; margin-bottom: 12px; }
.achv-group-head {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 14px 16px;
  border: none;
  background: none;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}
.achv-group-name { font-family: Cinzel, serif; font-weight: 600; font-size: 15px; flex: none; }
.achv-group-count { flex: 1; color: var(--mut); font-size: 12px; text-align: right; margin-right: 4px; }
/* Locked (Tutorial-gated) group teaser — no expand/collapse, not a button. */
.achv-group-locked .achv-group-head { cursor: default; opacity: .6; }
.achv-group-lock-ico { width: 18px; height: 18px; flex: none; color: var(--mut); }
.achv-group-hint { flex: 1; color: var(--mut); font-size: 12px; text-align: right; margin-right: 4px; }
/* Expand/collapse — same always-in-DOM max-height transition as
   .profile-deck-cards-wrap, so the wrapper never gets torn down/rebuilt
   mid-animation by an unrelated re-render. */
.achv-group-body-wrap { max-height: 0; overflow: hidden; transition: max-height .3s ease; }
.achv-group-body-wrap.open { max-height: 4000px; }
.achv-group-body { display: flex; flex-direction: column; gap: 10px; padding: 0 16px 16px; }
.achv-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--chip);
}
.achv-row-icon { flex: none; width: 30px; height: 30px; color: var(--acc); }
.achv-row-icon svg { width: 100%; height: 100%; }
/* Locked: icon dimmed, same treatment binderCard() uses for an unowned card. */
.achv-row-locked .achv-row-icon { filter: grayscale(1); opacity: .42; }
.achv-row-body { flex: 1; min-width: 0; }
.achv-row-name { font-family: Cinzel, serif; font-weight: 600; font-size: 13.5px; }
.achv-row-desc { color: var(--mut); font-size: 12px; margin-top: 2px; }
.achv-progress { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
.achv-progress .progress { flex: 1; margin: 0; }
.achv-progress-text { flex: none; color: var(--mut); font-size: 11px; font-variant-numeric: tabular-nums; }
.achv-row-action { flex: none; }
.achv-claim-btn { padding: 10px 20px; font-size: 13px; gap: 7px; }
.achv-claim-btn .mana-ico { width: .95em; height: .95em; }
.achv-reward-hint { display: inline-flex; align-items: center; gap: 5px; color: var(--mut); font-size: 12px; }
.achv-reward-hint .mana-ico { width: .95em; height: .95em; }
/* Claimed row: subtle accent tint reads as "done", inverse of an
   unread-notification highlight. */
.achv-row-claimed { background: color-mix(in srgb, var(--acc) 10%, var(--chip)); border-color: color-mix(in srgb, var(--acc) 30%, var(--line)); }
.achv-claimed-badge { color: var(--acc); font-size: 12px; font-weight: 600; white-space: nowrap; }
.achv-group-bonus-row { display: flex; justify-content: center; padding-top: 4px; }
.achv-group-bonus-btn { padding: 12px 26px; font-size: 14px; }
.achv-group-bonus-claimed { display: inline-flex; align-items: center; gap: 6px; color: var(--acc); font-size: 12px; font-weight: 600; }
.achv-group-bonus-claimed .mana-ico { width: .95em; height: .95em; }
.achv-ico { width: 100%; height: 100%; }
@media (max-width: 680px) {
  .achv-row { flex-wrap: wrap; }
  .achv-row-action { width: 100%; display: flex; justify-content: flex-end; }
}

/* "Elevator" stage transition (Continue -> next stage) / fade-to-black
   (wrong guess). Plain @keyframes on each section's own root node, mounted
   fresh by a render() that already carries the final, resolved state (see
   oracleGuess()/oracleContinueTrial() in app.js) — no intervening
   render()/request happens during any of them, the same "one render, then
   only cgSleep" safety condition already established for the battle
   system's clash-loop effects.

   .oracle-elevator takes over .oracle's own box (same min-height/flow
   position .oracle itself would occupy — deliberately NOT position:fixed,
   so the transition stays anchored wherever the page is currently
   scrolled instead of jumping to a full-viewport overlay). Both the
   outgoing and incoming stage sections are absolutely positioned to this
   exact same box, so at every animation frame their combined footprint
   has zero seam and #app's own background-image is never exposed between
   them (each section keeps its own .oracle-stage-<name> backdrop). */
.oracle-elevator {
  position: relative;
  min-height: 100vh;
  min-height: 100dvh;
  overflow: hidden;
}
.oracle-elevator .oracle-active {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
}
/* Both keyframes share the same duration + easing token so the two
   sections stay exactly one box-height apart at every timestep (incoming's
   bottom edge == outgoing's top edge, always) — two different easing
   curves would break that invariant even with matching start/end values. */
@keyframes oracle-elevator-in {
  0%   { transform: translateY(-100%); }
  100% { transform: translateY(0); }
}
@keyframes oracle-elevator-out {
  0%   { transform: translateY(0); }
  100% { transform: translateY(100%); }
}
.oracle-elevator-in {
  animation: oracle-elevator-in .7s cubic-bezier(.65, 0, .35, 1) both;
  z-index: 2;
}
.oracle-elevator-out {
  animation: oracle-elevator-out .7s cubic-bezier(.65, 0, .35, 1) both;
  z-index: 1;
  pointer-events: none; /* belt-and-suspenders — the outgoing snapshot already omits all data-act attributes, see oracleStageSectionHtml() in app.js */
}

/* Defeat: a slow, deliberate fade to black — no downward motion (an
   earlier version also translated the section down; removed, a pure fade
   reads as more composed/ceremonial for "the vision was wrong" than a
   literal fall). position:fixed so it covers the full visible viewport
   regardless of scroll position when the loss lands. */
@keyframes oracle-blackout { 0% { opacity: 0; } 100% { opacity: .92; } }
.oracle-active.fading::after {
  content: '';
  position: fixed;
  inset: 0;
  background: #000;
  opacity: 0;
  animation: oracle-blackout 2.2s ease-in both;
  pointer-events: none;
  z-index: 5;
}

@media (prefers-reduced-motion: reduce) {
  /* animation:none leaves both sections at rest (translateY(0), i.e.
     stacked exactly on top of each other) — the static z-index rules
     above (not animation-fill-mode) are what keep the incoming section on
     top and instantly visible in that resting state. */
  .oracle-elevator-in,
  .oracle-elevator-out,
  .oracle-active.fading::after { animation: none; }
}

/* Mobile — every other full-screen view in the game has a 680px
   breakpoint (Market, Summon, Pack-opening, ...); this one previously had
   none. Cards keep their side-by-side "duel" framing as long as they
   physically fit (flex-wrap on .oracle-cards-row already falls back to a
   vertical stack on anything narrower than ~360px, no forced
   flex-direction override needed). */
@media (max-width: 680px) {
  .oracle { padding: clamp(16px, 4vh, 32px) 12px 70px; gap: 18px; }
  .oracle h2 { font-size: 24px; }
  .oracle-stage-title { font-size: 21px; }
  .oracle-stage-flavor { font-size: 12px; max-width: 300px; }
  .oracle-ladder { max-width: 100%; gap: 6px; }
  .oracle-rung { padding: 6px 4px; }
  .oracle-rung-name { font-size: 9px; }
  .oracle-rung-mult { font-size: 12px; }
  .oracle-cards-row { gap: 10px; }
  .oracle-flip-frame.flip-card { width: 40vw; max-width: 155px; }
  .oracle-vs { font-size: 13px; }
  .oracle-power-badge { font-size: 22px; margin-top: 6px; }
  /* At this card width, "CREATURE"/"MESOPOTAMIAN" no longer fit their row
     and were truncating to "CR…"/"MESOPOT…" — same precedent as
     .match-slot/.match-hand-card (styles.css ~632/2607): drop the text,
     keep just the icon, rather than show a half-cut word. */
  .oracle-card .meta-left .ascension, .oracle-card .pan-txt { display: none; }
  .oracle-guess-btn, .oracle-choice-btn { padding: 14px 22px; font-size: 14px; }
  .oracle-guess-row, .oracle-choice-row { gap: 10px; }
}

/* ---- Summon (case-opening reel) ---- */
.summon {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  min-height: 100vh;
  min-height: 100dvh;
  padding: clamp(24px, 5vh, 52px) 12px 70px;
}
.summon h2 {
  font-family: Cinzel, serif;
  font-weight: 600;
  font-size: clamp(19px, 3vw, 26px);
  margin: 0 0 24px;
}
.summon-hero { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.summon-hero h1 { font-family: Cinzel, serif; font-weight: 600; font-size: clamp(26px, 4.5vw, 40px); margin: 6px 0 0; }
.summon-hero .lede {
  font-family: Cinzel, serif;
  font-style: italic;
  font-weight: 500;
  font-size: clamp(13px, 1.6vw, 15px);
  color: var(--mut);
  max-width: 420px;
  margin: 0 0 18px;
  line-height: 1.5;
}
.free-banner {
  display: inline-block;
  padding: 5px 14px;
  margin: -6px 0 14px;
  border-radius: 999px;
  border: 1px solid rgba(230, 193, 92, .5);
  background: rgba(230, 193, 92, .15);
  color: #e6c15c;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .04em;
}
.free-tag {
  display: inline-block;
  vertical-align: middle;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .06em;
  color: #06222a;
  background: #e6c15c;
  border-radius: 6px;
  padding: 2px 7px;
}

.summon-reel-frame {
  position: relative;
  width: min(920px, 96vw);
  height: 210px;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--panel);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.summon-marker {
  position: absolute;
  left: 50%; top: 0; bottom: 0;
  width: 2px;
  transform: translateX(-50%);
  background: linear-gradient(180deg, transparent, var(--acc), transparent);
  z-index: 3;
  pointer-events: none;
}
.summon-marker::before, .summon-marker::after {
  content: '';
  position: absolute; left: 50%;
  transform: translateX(-50%);
  border-left: 7px solid transparent;
  border-right: 7px solid transparent;
}
.summon-marker::before { top: -1px; border-top: 9px solid var(--acc); }
.summon-marker::after { bottom: -1px; border-bottom: 9px solid var(--acc); }
.summon-reel {
  display: flex;
  gap: 10px;
  padding: 14px;
  height: 100%;
  align-items: center;
  will-change: transform;
}
.summon-card {
  position: relative;
  flex: 0 0 auto;
  width: 120px;
  aspect-ratio: 5 / 7;
  border-radius: 12px;
  border-width: 2px;
  background:
    radial-gradient(140% 110% at 50% 10%, rgba(255, 255, 255, .08), transparent 45%),
    radial-gradient(160% 130% at 50% 108%, rgba(0, 0, 0, .55), transparent 58%),
    #161310;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  overflow: hidden;
}
.summon-card .card-top { font-size: 7.5px; letter-spacing: .12em; }
.summon-card .card-meta .pan-ico { width: 15px; height: 15px; }
.summon-card .card-top .power { font-size: 12px; }
.summon-card .card-art { border-radius: 8px; }
.summon-card .card-glyph { font-size: 30px; }
.summon-card .card-name { font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.summon-card .card-divider { margin: 0; }
.summon-card .card-meta { font-size: 6.5px; letter-spacing: .04em; }
.summon-card.alt { background: #f4f1ea; }
.summon-card.alt .card-name { color: #1c1a16; }

@media (max-width: 680px) {
  .summon-reel-frame { height: 168px; }
  .summon-card { width: 94px; }
}
