* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  height: 100%;
  overflow: hidden;
  background: #0a0710;
  color: #f4e9ec;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  user-select: none; -webkit-user-select: none;
  cursor: crosshair;
  /* mobile: no rubber-band / pull-to-refresh yanking the page mid-hunt, and no
     grey tap flash or long-press callout on the game surface */
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
}

/* the game surface eats all touch gestures itself (the joystick), so the
   browser never pans, zooms or double-tap-zooms over it */
#game { display: block; width: 100vw; height: 100vh; height: 100dvh; touch-action: none; }

/* Form controls do NOT inherit font-family from the page — the UA hands them its own
   (Arial here). That left 25 controls off-theme against a system-ui page, including the
   whole death screen and the hero Play button. The rule was already being applied by hand
   in five scattered places (.partyToggle, #invite, the shop buttons); this states it once
   so a new control is themed by default instead of by remembering. */
button, input, select, textarea { font-family: inherit; }

/* Nothing styled ::selection, so the prefilled name — focused and select-all'd on load,
   the first thing on a crimson-on-black screen — landed under a stock Windows-blue block. */
::selection { background: rgba(255, 91, 120, .45); color: #fff; }
::-moz-selection { background: rgba(255, 91, 120, .45); color: #fff; }

.hidden { display: none !important; }
.sub { opacity: .55; font-size: 13px; }

/* ---------- HUD ---------- */
/* v3.7 — height:100dvh tracks the VISIBLE viewport as the mobile URL bar slides,
   so bottom-pinned children (#touch thumbs, #stats, #abilityWrap) never hide under
   the browser chrome (100vh is the taller layout viewport; dvh is what's on screen). */
#hud { position: fixed; top: 0; left: 0; right: 0; bottom: auto; height: 100vh; height: 100dvh; pointer-events: none; }

/* kill feed — top left */
#killfeed {
  position: absolute; left: 16px; top: 14px;
  list-style: none; max-width: 340px;
  font-size: 13px; letter-spacing: .2px;
  /* CLAMPED for the same reason #ticker below is: #contract is pinned at top:46%, and
     a feed of 6 rows is ~174px tall. On a LANDSCAPE PHONE (390px of height) 46% is only
     179px, so a full feed ran straight through the Pact contract card — two blocks of
     left-aligned text on the same pixels, with no z-index between them to decide it.
     Portrait never showed it (46% of 844px is 388px, miles clear), which is why it
     survived the mobile pass. Same max() floor as #ticker: bare calc() goes negative on
     a short viewport, and an invalid max-height is DROPPED, restoring the bug exactly.
     Clipping the bottom is the correct end — feed() prepends, so the newest kill is at
     the top and only the oldest rows are ever cut. */
  max-height: max(0px, calc(46vh - 22px)); overflow: hidden;
}
#killfeed li {
  padding: 4px 10px; margin-bottom: 4px;
  background: rgba(18, 8, 20, .6);
  border-left: 3px solid #d31b3e;
  border-radius: 6px;
  text-shadow: 0 1px 4px #000;
  transition: opacity .6s;
}
#killfeed li.stake { border-left-color: #ff2e57; background: rgba(60, 6, 20, .7); }
#killfeed li.holy { border-left-color: #e8c86a; }
#killfeed li.coven { border-left-color: #5ae68c; }
#killfeed li.coven b { color: #93e6a8; }
#killfeed li.fade { opacity: 0; }
#killfeed b { color: #ff8ea0; font-weight: 700; }
#killfeed li.holy b { color: #e8c86a; }

/* WHAT'S HAPPENING — the world's live state, left column between the kill feed
   and the contract card. Clamped to 46vh so it can never grow into either. */
#ticker {
  position: absolute; left: 16px; top: 196px;
  /* max() floors it at 0: on a short viewport calc() alone goes negative, which is
     an invalid max-height — the declaration would be dropped and the strip would
     grow straight through the contract card below it. */
  width: 236px; max-height: max(0px, calc(46vh - 212px)); overflow: hidden;
  display: flex; flex-direction: column; gap: 5px;
}
#ticker .pill {
  padding: 5px 10px; border-radius: 8px;
  background: rgba(18, 8, 20, .72);
  border: 1px solid rgba(255, 91, 120, .25);
  border-left: 3px solid #d31b3e;
  text-shadow: 0 1px 4px #000;
}
#ticker .pT {
  display: block; font-size: 12.5px; font-weight: 700; letter-spacing: .2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#ticker .pT b { font-variant-numeric: tabular-nums; opacity: .85; font-weight: 700; }
#ticker .pH {
  display: block; font-size: 10.5px; letter-spacing: .3px; opacity: .5;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#ticker .moon  { border-left-color: #ff2e57; color: #ff8ea0; }
#ticker .king  { border-left-color: #e8c86a; color: #e8c86a; }
#ticker .tomb  { border-left-color: #a02fd0; color: #d8b0ff; }
#ticker .inq,
#ticker .hunt  { border-left-color: #e8c86a; color: #f4ecd8; }
#ticker .siege { border-left-color: #ffb054; color: #ffb054; }
#ticker .cvn   { border-left-color: #ff5b78; color: #ffd0d8; }
#ticker .omen  { border-left-color: #bfa8d4; color: #bfa8d4; }
/* the race is on — only the timed relic pulses. 1s period so it restarts in step
   with the countdown rewrite instead of stuttering mid-fade. */
#ticker .tomb { animation: pulse 1s ease-in-out infinite; }

/* Pact contract — left side, under the kill feed */
#contract {
  position: absolute; left: 16px; top: 46%;
  width: 210px; padding: 10px 14px;
  background: rgba(30, 10, 18, .78);
  border: 1px solid rgba(211, 27, 62, .45);
  border-left: 3px solid #d31b3e;
  border-radius: 10px;
}
#contract .cTitle { font-size: 13px; font-weight: 800; color: #ff8ea0; letter-spacing: .4px; }
#contract #cDesc { margin: 3px 0 6px; }
#cBar { height: 5px; border-radius: 999px; background: rgba(255,91,120,.15); overflow: hidden; }
#cFill { height: 100%; width: 0%; background: linear-gradient(90deg, #d31b3e, #ff5b78); transition: width .3s; }
#cMeta { margin-top: 4px; font-variant-numeric: tabular-nums; }

/* announcements — top center */
#announce {
  position: absolute; left: 50%; top: 16%; transform: translateX(-50%) translateY(-10px);
  /* v3.7 — height-aware sizing: vmin shrinks with the SHORTER dimension, so a long
     line can never fill a short landscape phone (the old max-width:760 rule missed
     landscape, which is ~844 wide but only ~390 tall). */
  font-size: clamp(16px, 4.6vmin, 26px); font-weight: 800; letter-spacing: .5px; text-align: center;
  max-width: min(84vw, 620px);
  text-shadow: 0 2px 16px #000, 0 0 30px rgba(211, 27, 62, .5);
  opacity: 0; transition: opacity .3s, transform .3s;
  white-space: normal; line-height: 1.2;
  /* This is the headline channel, but it had no z-index and lost on DOM order: on a
     390px phone an 84vw banner reaches x=293 while #mapWrap starts at x=257, so long
     lines were CLIPPED BY THE MINIMAP mid-word ("The Stake ha"). It is transient and
     already carries a heavy text-shadow for exactly this, so let it win over the
     ambient panels — still under #touch (8) and the modals (10/11). */
  z-index: 7;
}
#announce.shown { opacity: 1; transform: translateX(-50%) translateY(0); }
#announce.big { font-size: clamp(20px, 6.4vmin, 36px); }
#announce.blood { color: #ff5b78; }
#announce.gold  { color: #e8c86a; text-shadow: 0 2px 16px #000, 0 0 34px rgba(232, 200, 106, .6); }
#announce.holy  { color: #f4ecd8; text-shadow: 0 2px 16px #000, 0 0 34px rgba(191, 224, 255, .6); }
#announce.stake { color: #ff2e57; }
#announce.grey  { color: #c9c2d4; }

/* Crimson Trial ticker */
#trial {
  position: absolute; left: 50%; top: 8%; transform: translateX(-50%);
  font-size: 15px; font-weight: 700; color: #e8c86a;
  background: rgba(30, 12, 8, .75);
  border: 1px solid rgba(232, 200, 106, .4);
  padding: 6px 18px; border-radius: 999px;
  text-shadow: 0 1px 6px #000;
  white-space: nowrap; max-width: 92vw; overflow: hidden; text-overflow: ellipsis;
}
/* 92vw is measured against the VIEWPORT, but this pill sits in the same horizontal band
   as the leaderboard. A 3-entrant trial renders ~1080px wide and #board owns the rightmost
   ~200px, so the tail slid UNDER an opaque panel that comes LATER in the HUD and therefore
   paints over it — the right end of the trial vanished mid-word with no ellipsis to admit
   it, the same failure #announce had against the minimap. The pill already truncates
   itself; it just had no idea where the furniture was.
   Scoped to the media query that MIRRORS #board's own visibility (it is display:none below
   761px and under body.touch), because the subtraction must never go negative: at the 761px
   floor it is still 301px, but at 390px it resolves to a NEGATIVE max-width, which Chrome
   CLAMPS TO 0px — measured, and it collapsed the whole trial ticker to nothing on a phone.
   Same trap the #ticker rule above documents, from the other direction.
   Clearing #board and not the kill feed is deliberate: #trial paints OVER the feed, so that
   side stays readable and only dims transient chatter. */
@media (min-width: 761px) {
  #trial { max-width: min(92vw, calc(100vw - 460px)); }
}
body.touch #trial { max-width: 92vw; }   /* touch hides #board at ANY width — take it back */

/* Wrath meter */
#wrathWrap {
  position: absolute; left: 50%; top: 12px; transform: translateX(-50%);
  width: min(340px, 92vw); text-align: center;
}
#kingName {
  font-size: clamp(11px, 3.2vmin, 14px); font-weight: 800; color: #e8c86a;
  text-shadow: 0 1px 8px #000; margin-bottom: 4px; letter-spacing: 1px;
}
#wrathBar {
  height: 10px; border-radius: 999px; overflow: hidden;
  background: rgba(18, 8, 20, .7);
  border: 1px solid rgba(232, 200, 106, .45);
}
#wrathFill {
  height: 100%; width: 0%;
  background: linear-gradient(90deg, #e8c86a, #fff6d8);
  box-shadow: 0 0 12px rgba(232, 200, 106, .8);
  transition: width .4s;
}
.wrathLabel {
  margin-top: 3px; font-size: 9px; letter-spacing: 3px; color: #e8c86a; opacity: .8;
}

/* leaderboard */
#board {
  position: absolute; right: 16px; top: 16px;
  min-width: 180px;
  background: rgba(18, 8, 20, .72);
  border: 1px solid rgba(255, 91, 120, .25);
  border-radius: 12px;
  padding: 10px 12px 12px;
  backdrop-filter: blur(6px);
}
#board h3 {
  font-size: 12px; text-transform: uppercase; letter-spacing: 2px;
  color: #ff8ea0; margin-bottom: 8px; font-weight: 700;
}
#board ol { list-style: none; }
#board li {
  display: flex; justify-content: space-between; gap: 12px;
  font-size: 14px; padding: 3px 0;
  border-bottom: 1px solid rgba(255,255,255,.05);
}
#board li:last-child { border-bottom: 0; }
#board li span:last-child { color: #ff9db0; font-variant-numeric: tabular-nums; }

/* stats + evolution progress — bottom left */
#stats {
  position: absolute; left: 16px; bottom: 16px;
  font-size: 20px; letter-spacing: .3px;
  text-shadow: 0 2px 8px #000;
}
#stats #evoName { color: #ff5b78; font-weight: 700; }
#stats #mass { font-weight: 700; font-variant-numeric: tabular-nums; }
#stats .sub { opacity: .55; font-size: 14px; }
#stats .dot { opacity: .4; margin: 0 6px; }
#evoBar {
  margin-top: 6px; width: 220px; height: 6px; border-radius: 999px;
  background: rgba(255, 91, 120, .15); overflow: hidden;
}
#evoFill {
  height: 100%; width: 0%;
  background: linear-gradient(90deg, #a02fd0, #d31b3e);
  box-shadow: 0 0 8px rgba(211, 27, 62, .8);
  transition: width .3s;
}
#evoNext { margin-top: 3px; }

/* ability + badges — bottom center */
#abilityWrap {
  position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 6px;
}
#badges { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; max-width: min(96vw, 720px); }
.badge {
  font-size: clamp(10px, 2.9vmin, 12px); font-weight: 800; letter-spacing: .5px;
  padding: 4px 12px; border-radius: 999px;
  background: rgba(18, 8, 20, .8);
  border: 1px solid rgba(255, 91, 120, .5);
  text-shadow: 0 1px 4px #000;
  animation: pulse 1.2s ease-in-out infinite;
}
.badge.stake  { color: #ff2e57; border-color: #ff2e57; }
.badge.frenzy { color: #ff5b78; }
.badge.burn   { color: #ffb054; border-color: rgba(255, 176, 84, .6); }
.badge.gold   { color: #e8c86a; border-color: rgba(232, 200, 106, .6); }
.badge.holy   { color: #f4ecd8; border-color: rgba(191, 224, 255, .6); }
.badge.hex    { color: #c9a0ff; border-color: rgba(176, 107, 255, .6); }   /* v3.6.2: mesmerize/weaken debuffs */
@keyframes pulse { 50% { opacity: .65; } }
/* v3.10 — DRACUL's breath meter, inline in its badge. The dragon has no other
   ability, so this bar IS his readout: hold to breathe, watch it drain, let it back. */
.fireBar {
  display: inline-block; vertical-align: middle;
  width: clamp(46px, 12vmin, 74px); height: 6px; margin-left: 8px;
  border-radius: 999px; overflow: hidden;
  background: rgba(0, 0, 0, .55);
  box-shadow: inset 0 0 0 1px rgba(255, 176, 84, .35);
}
.fireBar > i { display: block; height: 100%; border-radius: 999px; transition: width .12s linear; }

#ability { display: flex; align-items: center; gap: 10px; }
#abilityIconWrap {
  position: relative; width: 46px; height: 46px; border-radius: 12px;
  background: rgba(18, 8, 20, .8);
  border: 1px solid rgba(255, 91, 120, .4);
  display: grid; place-items: center; font-size: 24px;
  overflow: hidden;
}
#abilityCd { position: absolute; inset: 0; border-radius: 12px; }
#abilityName {
  font-size: 14px; font-weight: 700; color: #ff8ea0;
  text-shadow: 0 1px 6px #000;
}
#hint { font-size: 11px; opacity: .45; letter-spacing: .4px; white-space: nowrap; }
#hint b { color: #ff8ea0; }

/* minimap — bottom right */
#mapWrap { position: absolute; right: 16px; bottom: 16px; text-align: right; }
#dayBadge {
  display: inline-block; margin-bottom: 6px;
  font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums;
  padding: 4px 12px; border-radius: 999px;
  background: rgba(18, 8, 20, .75);
  border: 1px solid rgba(255, 91, 120, .3);
}
#dayBadge.day { color: #ffd98a; border-color: rgba(232, 200, 106, .55); }
#dayBadge.night { color: #9db4ff; }
#dayBadge.moon {
  color: #ff2e57; border-color: #ff2e57; font-weight: 800;
  animation: pulse 1s ease-in-out infinite;
}
#minimap {
  display: block; border-radius: 10px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, .5);
}

#deathRecords { font-size: 13px; margin-bottom: 14px; }
#deathRecords .best { color: #e8c86a; font-weight: 800; }
#deathTip { color: #bfa8d4; font-style: italic; max-width: 360px; margin: 2px auto 6px; }

/* disconnect overlay */
#disc {
  position: absolute; inset: 0; display: grid; place-items: center;
  background: rgba(6, 3, 10, .8);
  pointer-events: auto;
  /* same stacking fix as #death, one higher: losing the connection outranks the death
     card if both are somehow up, and neither may sit under the thumb controls */
  z-index: 11;
}
#disc .card {
  text-align: center; padding: 34px 46px;
  background: rgba(20, 8, 22, .9);
  border: 1px solid rgba(255, 91, 120, .3);
  border-radius: 18px;
}
#disc h2 { font-size: 26px; margin-bottom: 6px; color: #ff8ea0; }
#disc .sub { margin-bottom: 18px; }
#disc button {
  padding: 12px 34px; font-size: 15px; font-weight: 700;
  border: 0; border-radius: 10px; cursor: pointer; color: #fff;
  background: linear-gradient(180deg, #e8264c, #a30f2e);
}

/* death screen */
#death {
  position: absolute; inset: 0; display: grid; place-items: center;
  background: radial-gradient(800px 500px at 50% 45%, rgba(60, 4, 18, .55), rgba(6, 3, 10, .82));
  pointer-events: auto;
  /* Above the in-run HUD furniture (#touch is 8, #tMenu/#fps are 9), below the
     rewarded-ad overlay (20) that this card LAUNCHES via #respawnAd. Without this the
     card only won on DOM order: the thumb controls painted on top of it and still
     hit-tested, so a tap at the card's bottom-right corner hit the fang button, and the
     kill feed rendered over the card's own text. */
  z-index: 10;
}
#death .card {
  text-align: center; padding: 34px 46px;
  /* On a phone this card is nearly full-width, so the kill feed sits directly behind it
     and ghosted through the old .85 — bright red names crossing the tip line. Opaque
     enough to read against, blurred like the other floating chrome (.tbtn, #tMenu). */
  background: rgba(20, 8, 22, .94);
  backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
  border: 1px solid rgba(255, 91, 120, .3);
  border-radius: 18px;
  box-shadow: 0 30px 80px rgba(0,0,0,.7);
}
#death h2 { font-size: 30px; margin-bottom: 8px; color: #ff5b78; }
#death p { font-size: 16px; margin-bottom: 4px; }
#death .sub { margin-bottom: 18px; }
#death button {
  padding: 12px 34px; font-size: 16px; font-weight: 700;
  border: 0; border-radius: 10px; cursor: pointer; color: #fff;
  background: linear-gradient(180deg, #e8264c, #a30f2e);
  box-shadow: 0 8px 24px rgba(180,20,50,.4);
}
#death button:hover { filter: brightness(1.1); }
/* SPECIFICITY FIX + hierarchy. `#death button` above is an ID selector (1-0-1) and
   `button.ghost` is (0-1-1), so every ghost button in this card lost its background
   and kept only `width:100%` — which INVERTED the hierarchy: Rise Again (no .ghost,
   so auto-width) rendered as the SMALLEST control while Return to Menu rendered as a
   full-width solid primary. On the one screen that decides whether a life becomes
   another life, "play again" was the least prominent thing on it. (The two
   !importants on #respawnAd below are the same fight, patched from the other side.)
   Three tiers now: rise, rise-with-a-boon, and everything else. */
#death button.ghost {
  background: none;
  box-shadow: none;
  border: 1px solid rgba(201, 182, 212, .3);
  color: #c9b6d4;
  padding: 10px;
  font-size: 13px;
  font-weight: 600;
}
#death button.ghost:hover { border-color: rgba(201, 182, 212, .7); color: #fff; background: rgba(201,182,212,.06); }
/* the primary: the biggest, loudest thing in the card */
#death #respawn {
  display: block; width: 100%; margin-top: 4px;
  padding: 16px 24px; font-size: 18px; font-weight: 800;
  letter-spacing: .6px; text-transform: uppercase;
  box-shadow: 0 12px 30px rgba(200, 20, 55, .5), inset 0 1px 0 rgba(255,255,255,.2);
}
/* the boon keeps its gold and its weight — it is a real offer, not a footnote —
   but it sits a step below Rise Again rather than towering over it. */
#death #respawnAd {
  border: 0 !important;
  color: #fff !important;
  font-size: 14px;
  font-weight: 700;
  padding: 12px;
}
/* death card is the focus: recede the frozen live readouts behind it (bottom-left
   stats + the still-lit ability/badges). main.js toggles body.dead off Net.dead,
   so this fades back the instant you Rise Again. Board + minimap stay as context. */
body.dead #stats, body.dead #abilityWrap { opacity: .18; transition: opacity .4s ease; }

/* ---------- Menu ---------- */
#menu {
  /* centre the card in the space ABOVE the pinned ad box (padding-bottom
     reserves it) so the footer never collides with the ad */
  position: fixed; top: 0; left: 0; right: 0; bottom: auto; height: 100vh; height: 100dvh; display: flex; flex-direction: column; align-items: center; justify-content: safe center; overflow-y: auto; overscroll-behavior: contain; padding: 20px;
  background:
    linear-gradient(rgba(10, 7, 16, .5), rgba(10, 7, 16, .8)),
    url('/assets/ui/menu_bg.png') center / cover no-repeat,
    radial-gradient(1200px 600px at 50% -10%, #2a0a18 0%, #0a0710 60%);
  transition: opacity .4s ease, visibility 0s;
}
/* THE MENU MUST LEAVE THE TAB ORDER, NOT JUST THE MOUSE'S REACH.
   `opacity: 0; pointer-events: none` hides it from the eye and the pointer and from
   NOTHING ELSE: an opacity-0 subtree is still focusable and still in the accessibility
   tree. Measured on the live site mid-match — the document's whole tab order was 40
   stops, of which 35 were these invisible menu controls (name box, coven code, shop,
   skins, and the two outbound links). Tab during a hunt and your focus vanished into a
   text field you cannot see, with no focus ring to find it by, and your next keystroke
   went to the name box instead of the game. A screen reader read the entire start
   screen out over live play.

   `visibility: hidden` is what actually removes it from both, but applying it at t=0
   would kill the .4s fade — so it is transitioned with a delay equal to the fade and
   lands the instant the fade finishes. Reverting has no delay, so Rise Again / Return
   to Menu brings the menu back focusable immediately. CSS-only on purpose: a JS toggle
   is one early return away from falling out of sync with the class that does the fade.

   Third time this project has shipped this class of bug (the two modals were
   "open and hope"; #death painted over a still-hit-testable #touch). The modals were
   only ever safe because `.hidden` here is `display: none !important`. */
#menu.gone {
  opacity: 0; pointer-events: none; visibility: hidden;
  transition: opacity .4s ease, visibility 0s linear .4s;
}

/* ---------- v3.9.3 menu atmosphere + entrance ---------- */
/* a decorative layer of life BEHIND the card — never intercepts a tap */
#menuFx { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.fxMoon {
  position: absolute; left: 50%; top: 9%; width: min(60vw, 480px); height: min(60vw, 480px);
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(255,64,84,.30) 0%, rgba(255,64,84,.10) 34%, transparent 64%);
  filter: blur(4px); animation: fxMoon 7s ease-in-out infinite;
}
@keyframes fxMoon { 0%,100% { opacity: .6; transform: translate(-50%,-50%) scale(1); } 50% { opacity: 1; transform: translate(-50%,-50%) scale(1.09); } }
.fxFog {
  position: absolute; left: -25%; right: -25%; top: 26%; height: 60%;
  background: radial-gradient(60% 100% at 30% 50%, rgba(120,42,72,.16), transparent 70%),
              radial-gradient(52% 100% at 76% 42%, rgba(58,22,52,.16), transparent 70%);
  animation: fxFog 36s ease-in-out infinite alternate;
}
@keyframes fxFog { from { transform: translateX(-5%); } to { transform: translateX(6%); } }
.fxEmbers { position: absolute; inset: 0; }
.ember {
  position: absolute; bottom: -14px; width: 3px; height: 3px; border-radius: 50%;
  background: #ff6a48; box-shadow: 0 0 7px 1px rgba(255,120,80,.7);
  opacity: 0; animation-name: fxEmber; animation-timing-function: linear; animation-iteration-count: infinite;
}
.ember:nth-child(even) { width: 2px; height: 2px; background: #e8c86a; box-shadow: 0 0 7px 1px rgba(232,200,106,.55); }
@keyframes fxEmber {
  0% { transform: translate(0,0); opacity: 0; }
  12% { opacity: .85; }
  88% { opacity: .45; }
  100% { transform: translate(26px, -92vh); opacity: 0; }
}

/* entrance — the card rises, its zones stagger in on first paint */
/* THE CARD AND THE HERO ENTER ON TRANSFORM ALONE — no opacity fade. This is not
 * a style preference, it is what makes the page measurable.
 *
 * Chrome does not treat an element painted at `opacity: 0` as a
 * largest-contentful-paint candidate, and it does not reconsider it when the
 * fade later reveals it. Both of these carried `both` fill-mode from
 * `opacity: 0`, so the biggest thing on the landing page — the title and tagline
 * — was invisible at first paint and never re-qualified. Measured: with the
 * fades in place the page emitted ZERO LCP entries; with animations disabled it
 * reported LCP at 176ms on the tagline. A page that reports no LCP cannot be
 * credited for the LCP it actually has.
 *
 * So the card and the hero SLIDE in (transform is off the layout path and does
 * not suppress the candidate) while the smaller zones below keep their fade —
 * none of them is ever the largest element, so none of them can hide it. */
#menu .card { animation: fxCardIn .7s cubic-bezier(.16,1,.3,1) both; }
@keyframes fxCardIn { from { transform: translateY(24px) scale(.985); } to { transform: none; } }
.menuPlay, #profilePanel, .menuExtras, .menuFoot { animation: fxZoneIn .6s cubic-bezier(.16,1,.3,1) both; }
.menuHero { animation: fxHeroIn .6s cubic-bezier(.16,1,.3,1) both; animation-delay: .10s; }
@keyframes fxHeroIn { from { transform: translateY(12px); } to { transform: none; } }
.menuPlay { animation-delay: .18s; }
#profilePanel { animation-delay: .26s; }
.menuExtras { animation-delay: .32s; }
.menuFoot { animation-delay: .38s; }
@keyframes fxZoneIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

/* living details — the title breathes, the Enter button catches a slow sheen */
#menu h1 { animation: fxTitle 6s ease-in-out infinite; }
@keyframes fxTitle { 0%,100% { filter: drop-shadow(0 2px 3px rgba(0,0,0,.4)); } 50% { filter: drop-shadow(0 2px 16px rgba(255,64,90,.5)); } }
#menu #play { position: relative; overflow: hidden; }
#menu #play::after {
  content: ""; position: absolute; top: 0; left: -70%; width: 45%; height: 100%;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.28), transparent);
  transform: skewX(-18deg); pointer-events: none; animation: fxSheen 5.5s ease-in-out infinite;
}
/* Animates TRANSFORM, not `left`, and that is the whole point.
 * `left` is a layout property: animating it re-lays out the button on every
 * frame, and the browser counts every one of those frames as a layout shift. On an
 * `infinite` 5.5s loop that meant Cumulative Layout Shift climbed forever while
 * anyone sat on the menu — measured at 0.048 and still rising after 26s, from an
 * effect that never moves anything a user can perceive. CLS is a Core Web Vital,
 * so this was a permanent, self-inflicted drag on the landing page's score.
 *
 * translateX percentages resolve against the ELEMENT's own width (45% of the
 * button), while the old `left` values were percentages of the BUTTON. The
 * sweep is -70% -> 140% of the button = 210% of the button = 210/45 ≈ 466.7% of
 * the element. Same sheen, pixel for pixel, off the layout path entirely.
 * skewX must be repeated in every keyframe or the transform would replace it. */
@keyframes fxSheen {
  0%,74%   { transform: translateX(0)      skewX(-18deg); }
  88%,100% { transform: translateX(466.7%) skewX(-18deg); }
}

@media (prefers-reduced-motion: reduce) {
  #menuFx *, #menu .card, .menuHero, .menuPlay, #profilePanel, .menuExtras, .menuFoot, #menu h1, #menu #play::after { animation: none !important; }
  .fxMoon { opacity: .7; }
  .ember { display: none; }
}

/* the card is a fixed, capped column so it never sprawls into the side panels;
   z-index keeps its interactive footer above the ad box in any overlap */
#menu .card {
  position: relative; z-index: 2;
  width: min(92vw, 452px); text-align: center; padding: 32px 38px 20px;
  background: rgba(19, 8, 21, .72);
  border: 1px solid rgba(255, 91, 120, .2);
  border-radius: 22px;
  box-shadow: 0 30px 80px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.06);
}

/* --- zone 1: identity --- */
.menuHero { margin-bottom: 24px; }
#menu h1 {
  font-size: clamp(38px, 7vw, 48px); font-weight: 800; letter-spacing: -1px; line-height: 1;
  background: linear-gradient(180deg, #ff8ea0, #d31b3e);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  margin-bottom: 10px;
}
#menu .tag { opacity: .72; font-size: 14px; line-height: 1.5; max-width: 33ch; margin: 0 auto; }
#menu .tag b { color: #ffb3c0; font-weight: 700; }

/* --- zone 2: play --- */
.menuPlay { display: flex; flex-direction: column; align-items: center; gap: 11px; }
#menu input {
  width: 100%; max-width: 320px; padding: 14px 16px; font-size: 16px; text-align: center;   /* 16px: iOS won't focus-zoom the menu */
  border-radius: 12px; border: 1px solid rgba(255,91,120,.26);
  background: #16091a; color: #fff; outline: none;
  transition: border-color .2s, box-shadow .2s;
}
#menu input::placeholder { color: rgba(255, 208, 216, .4); }
#menu input:focus { border-color: #ff5b78; box-shadow: 0 0 0 3px rgba(255,91,120,.15); }
/* the hero action — bigger, deeper, with a lift on hover */
#menu #play {
  width: 100%; max-width: 320px; margin-top: 6px;
  padding: 17px 15px; font-size: 17px; font-weight: 800; letter-spacing: 1.4px; text-transform: uppercase;
  border: 0; border-radius: 13px; cursor: pointer; color: #fff;
  background: linear-gradient(180deg, #ff3b5c 0%, #d31b3e 55%, #9c0f2b 100%);
  box-shadow: 0 12px 30px rgba(200,20,55,.5), inset 0 1px 0 rgba(255,255,255,.22);
  text-shadow: 0 1px 2px rgba(90,0,15,.55);
  transition: transform .09s cubic-bezier(.2,.9,.3,1), filter .2s, box-shadow .2s;
}
#menu #play:hover { filter: brightness(1.1); box-shadow: 0 16px 40px rgba(200,20,55,.62), inset 0 1px 0 rgba(255,255,255,.3); }
#menu #play:active { transform: translateY(2px) scale(.99); }
/* The live-arena line now sits INSIDE .menuPlay, directly above the button, so it
   takes the flex column's own gap instead of the footer margin it used to carry. */
.menuPlay .lobby { margin-top: 0; }
/* The coven-code disclosure: a quiet link, not a third button competing with Play.
   It removes itself once the field is open (main.js revealParty). */
#menu .partyToggle {
  background: none; border: 0; padding: 2px 6px; cursor: pointer;
  font: inherit; font-size: 12.5px; color: rgba(255, 208, 216, .5);
  text-decoration: underline; text-underline-offset: 3px;
  transition: color .2s;
  /* WCAG 2.2 SC 2.5.8 wants a 24px minimum target and this measured 20px on a phone.
     It is a standalone control, not a link inside a sentence, so the inline exception
     does not cover it. Grow the TARGET, not the type — the quiet look is deliberate. */
  min-height: 24px; display: inline-flex; align-items: center; justify-content: center;
}
#menu .partyToggle:hover { color: #ff8ea0; }
#partyWrap { width: 100%; display: flex; justify-content: center; }
#menu #invite {
  width: 100%; max-width: 320px; padding: 12px; font-size: 13.5px; font-weight: 700; cursor: pointer;
  border-radius: 11px; color: #ffd0d8;
  background: rgba(255,91,120,.09); border: 1px solid rgba(255,91,120,.28);
  transition: border-color .2s, color .2s, background .2s, transform .08s;
}
#menu #invite:hover { border-color: #ff5b78; color: #fff; background: rgba(255,91,120,.16); }
#menu #invite:active { transform: translateY(1px); }
.invite-msg { min-height: 14px; font-size: 12px; color: #9be59b; }

/* --- zone 4: footer (secondary, deliberately quiet) --- */
.menuFoot { margin-top: 22px; padding-top: 16px; border-top: 1px solid rgba(255, 91, 120, .12); }
#menu .controls {
  display: flex; justify-content: center; gap: 6px 14px; flex-wrap: wrap;
  font-size: 12px; opacity: .58;
}
#menu .controls b { color: #ff8ea0; }
#menu .lobby {
  margin-top: 12px; font-size: 13px; color: #ff8ea0;
  font-variant-numeric: tabular-nums; min-height: 16px;
}
#menu #lastRun { margin-top: 2px; font-size: 12px; }
.footMeta {
  margin-top: 12px; display: flex; justify-content: center; align-items: center; gap: 8px;
  font-size: 11px; opacity: .5;
}
.footMeta .link { color: #9fd0ff; }
.footMeta .ver { letter-spacing: .4px; }
.dotSep { opacity: .5; }
.footLinks { margin-top: 8px; display: flex; justify-content: center; gap: 16px; min-height: 14px; }
.footLinks a { font-size: 11px; color: #bfa8d4; text-decoration: none; opacity: .7; transition: opacity .2s, color .2s; }
.footLinks a:hover { opacity: 1; color: #e8d8f8; }

/* the Hall + Chronicle are secondary — hide them before they'd crowd the card,
   not just on phones (they float at the corners; a mid-width screen collides) */
@media (max-width: 1200px) { #hallPanel, #chroniclePanel { display: none !important; } }

/* ---------- remove-ads + the wider world (in the card) ---------- */
.menuExtras { margin-top: 24px; display: flex; flex-direction: column; gap: 20px; }

/* the AdinPlay slot lives on for a future BANNER_SLOT — 0 height until filled */
#adSlot:empty { display: none; }
#adSlot { margin: 0 auto; max-width: 100%; }

/* the paid path, made prominent: a big gold-rimmed button, not a footer pill */
.menuBuy { display: block; margin: 0; }
.buyBtn {
  width: 100%; max-width: 340px; margin: 0 auto;
  display: flex; align-items: center; justify-content: center; gap: 11px;
  font-family: inherit; font-size: 15px; letter-spacing: .3px;
  padding: 14px 18px; cursor: pointer; -webkit-appearance: none; appearance: none;
  color: #fff2c9; background: linear-gradient(180deg, rgba(232,200,106,.20), rgba(232,200,106,.05));
  border: 1.5px solid rgba(232,200,106,.55); border-radius: 14px;
  box-shadow: 0 8px 22px rgba(0,0,0,.32), inset 0 1px 0 rgba(255,246,216,.16);
  transition: background .18s, border-color .18s, transform .08s, box-shadow .18s;
}
.buyBtn:hover { background: linear-gradient(180deg, rgba(232,200,106,.30), rgba(232,200,106,.11)); border-color: #e8c86a; box-shadow: 0 12px 30px rgba(170,132,40,.4); }
.buyBtn:active { transform: translateY(1px); }
.buyBtn:focus-visible { outline: 2px solid #ffe6a3; outline-offset: 3px; }
.buyBtn .raIcon { width: 22px; height: 22px; display: block; flex: none; }
.buyBtn .buyMain { font-weight: 800; }
.buyBtn .buyPrice { display: inline-flex; align-items: baseline; gap: 6px; }
.buyBtn s { opacity: .5; font-weight: 400; font-size: 13px; }
.buyBtn b { color: #fff; font-weight: 800; font-size: 17px; }

/* sister worlds — two CUSTOM promo buttons, each dressed in its site's colours */
.worlds { display: flex; flex-direction: column; gap: 10px; }
.worldsLabel { font-size: 10px; letter-spacing: 3px; text-transform: uppercase; opacity: .42; }
.worldRow { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.worldBtn {
  display: flex; align-items: center; gap: 11px; text-align: left; text-decoration: none;
  padding: 12px 13px; border-radius: 13px; border: 1px solid transparent; position: relative; overflow: hidden;
  transition: transform .12s cubic-bezier(.2,.9,.3,1), box-shadow .22s, border-color .22s, filter .22s;
}
.worldBtn:hover { transform: translateY(-2px); filter: brightness(1.08); }
.worldBtn:active { transform: translateY(0); }
.worldBtn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.wbIcon { font-size: 22px; line-height: 1; flex: none; filter: drop-shadow(0 2px 6px rgba(0,0,0,.55)); }
.wbText { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; }
.wbText b { font-size: 14px; font-weight: 800; letter-spacing: .2px; }
.wbText small { font-size: 10.5px; opacity: .78; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wbGo { margin-left: auto; font-size: 13px; opacity: .45; flex: none; }
/* Indithar — "the bleeding world": an aged library, burgundy + tarnished gold */
.wb-indithar { background: linear-gradient(140deg, #3c1519 0%, #23100f 100%); border-color: rgba(190,132,66,.4); box-shadow: inset 0 1px 0 rgba(214,170,110,.13); }
.wb-indithar .wbText b { color: #e8c99c; }
.wb-indithar .wbText small { color: #cb9c88; }
.wb-indithar:hover { border-color: rgba(214,170,110,.72); box-shadow: 0 12px 26px rgba(60,20,24,.6); }
/* Mystvale — "a world of night": moonlit indigo + silver */
.wb-mystvale { background: linear-gradient(140deg, #1b1743 0%, #100e26 100%); border-color: rgba(150,152,224,.34); box-shadow: inset 0 1px 0 rgba(200,205,255,.13); }
.wb-mystvale .wbText b { color: #cfd3ff; }
.wb-mystvale .wbText small { color: #9ba1d6; }
.wb-mystvale:hover { border-color: rgba(184,188,255,.66); box-shadow: 0 12px 26px rgba(20,18,52,.62); }
/* phones: stack the two worlds full-width so the tagline never truncates */
@media (max-width: 540px) { .worldRow { grid-template-columns: 1fr; } }

/* purchase disclosure — follows the button out when you go ad-free */
.buyFine { font-size: 11px; opacity: .58; line-height: 1.42; max-width: 340px; margin: 9px auto 0; }
.buyFine a { color: inherit; text-decoration: underline; }
#menuBuy.hidden ~ .buyFine { display: none; }

button.ghost {
  display: block; width: 100%; margin-top: 8px;
  padding: 10px; font-size: 13px; font-weight: 600;
  border-radius: 10px; cursor: pointer; color: #c9b6d4;
  background: none; border: 1px solid rgba(201, 182, 212, .3);
}
button.ghost:hover { border-color: rgba(201, 182, 212, .7); color: #fff; }

/* bot chatter in the kill feed */
#killfeed li.chat { opacity: .72; font-style: italic; }
#killfeed li.chat b { color: #c9a2ff; font-style: normal; }

/* rewarded-ad respawn button — set apart from "Rise Again" */
#respawnAd {
  background: linear-gradient(180deg, #caa23a, #7a5a12) !important;
  box-shadow: 0 8px 24px rgba(180, 140, 30, .35) !important;
}
#shareRun { margin-top: 10px; }
#shareMsg { min-height: 16px; margin-top: 8px; color: #e8c86a; }

/* rewarded-ad overlay (fake countdown, or the AdinPlay player once configured) */
#adPlaying {
  position: absolute; inset: 0; z-index: 20; display: grid; place-items: center;
  background: rgba(4, 2, 8, .92); pointer-events: auto;
}
#adPlaying .card { max-width: 94vw; }
#preroll:not(.hidden) { display: grid; place-items: center; min-width: min(920px, 90vw); min-height: 200px; }
#adPlaying .card {
  text-align: center; padding: 40px 60px; border-radius: 16px;
  background: rgba(16, 8, 18, .9); border: 1px solid rgba(255, 91, 120, .2);
}
#adCountdown { font-size: 64px; font-weight: 800; color: #e8c86a; line-height: 1.1; }

/* v3.7 CRITICAL — let the death / disconnect / ad overlays SCROLL to their buttons
   instead of clipping the card off both edges on a short (landscape) viewport. Their
   `place-items:center` still supplies horizontal + in-track centering; this only swaps
   the vertical mechanism to the overflow-safe `align-content:safe center` so a card
   taller than the screen top-aligns (never clips) and scrolls. Without this, a touch
   player who dies in landscape literally cannot reach "Rise Again" / "Return to Menu". */
#death, #disc, #adPlaying {
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  align-content: safe center;
  justify-items: center;
  overscroll-behavior: contain;
  padding-block: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-bottom));
  padding-inline: env(safe-area-inset-left) env(safe-area-inset-right);
}

/* menu invite link feedback */
.invite-msg { min-height: 15px; margin-top: 8px; font-size: 12px; color: #9be59b; }
/* a rejected name is a refusal, not a confirmation — and it must not reserve a gap
   in the menu while it's empty (#nameMsg sits between two inputs) */
.invite-msg.bad { color: #ff9db0; }
#nameMsg:empty { display: none; }   /* #inviteMsg keeps its reserved min-height — it sits at the end of the block */

/* in-game party pill */
#partyPill {
  /* sits below the wrath meter so a reigning King never covers your coven */
  position: absolute; top: 74px; left: 50%; transform: translateX(-50%);
  padding: 6px 16px; border-radius: 999px; cursor: pointer;
  font-size: 12px; letter-spacing: .3px; color: #ffd0d8;
  background: rgba(30, 10, 24, .7); border: 1px solid rgba(255, 91, 120, .35);
  pointer-events: auto; user-select: none; z-index: 6;
  /* the roster can run long — clip gracefully, never wrap over the HUD */
  max-width: min(78vw, 560px); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
#partyPill:hover { border-color: #ff5b78; color: #fff; }

/* ---------- v3.0: profile / progression / cosmetics ---------- */
#profilePanel {
  margin: 22px 0 0; text-align: left;
  padding: 14px 15px; border-radius: 14px;
  background: rgba(28, 12, 30, .55); border: 1px solid rgba(255, 91, 120, .16);
}
.pfHead { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
#pfLevel {
  font-weight: 800; font-size: 13px; color: #ffd0d8; white-space: nowrap;
  padding: 3px 9px; border-radius: 999px; background: rgba(255, 46, 87, .18);
}
#pfBar { flex: 1; height: 8px; border-radius: 999px; overflow: hidden; background: rgba(255, 255, 255, .08); }
#pfFill { height: 100%; width: 0; border-radius: 999px; background: linear-gradient(90deg, #ff2e57, #ffd98a); transition: width .5s ease; }
#pfXp { font-size: 11px; white-space: nowrap; }
#pfSkins { display: flex; flex-wrap: wrap; gap: 6px; }
.skin {
  cursor: pointer; user-select: none; font-size: 11px; font-weight: 600;
  padding: 5px 10px; border-radius: 8px; color: #ffd0d8;
  background: rgba(30, 10, 22, .6); border: 1px solid rgba(255, 91, 120, .28);
  transition: transform .08s, border-color .12s;
}
.skin:hover { transform: translateY(-1px); border-color: #ff5b78; }
.skin.on { border-color: #ffd98a; color: #fff; background: rgba(255, 91, 120, .22); box-shadow: 0 0 0 1px rgba(255,217,138,.4); }
.skin.locked { opacity: .5; cursor: not-allowed; }
.skin.locked:hover { transform: none; border-color: rgba(255, 91, 120, .28); }
.skin .sw { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 5px; vertical-align: middle; }
.skin.streak.locked { border-color: rgba(255, 180, 90, .5); }

/* daily rite banner + streak + cloud sync */
#pfDaily {
  margin-bottom: 10px; padding: 7px 11px; border-radius: 9px; font-size: 12px; font-weight: 700;
  color: #ffe1b0; background: linear-gradient(90deg, rgba(255,120,60,.22), rgba(255,46,87,.14));
  border: 1px solid rgba(255,180,90,.4); text-align: center;
}
#pfFoot { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; gap: 8px; }
#pfStreak { color: #ffb877; font-weight: 700; }
.link { cursor: pointer; color: #9fd0ff; text-decoration: underline; font-size: 11px; user-select: none; }
.link:hover { color: #cfe6ff; }
#pfSync { margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,91,120,.14); }
.syncRow { display: flex; gap: 6px; align-items: center; margin-top: 6px; }
#pfCode { flex: 1; font: 700 12px ui-monospace, monospace; color: #ffd0d8; background: rgba(0,0,0,.3); padding: 5px 8px; border-radius: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#pfRestore { flex: 1; font-size: 12px; padding: 5px 8px; border-radius: 6px; color: #ffd0d8; background: rgba(0,0,0,.3); border: 1px solid rgba(255,91,120,.28); }
.mini { font-size: 11px; font-weight: 700; padding: 5px 12px; border-radius: 6px; cursor: pointer; color: #fff; background: rgba(255,91,120,.28); border: 1px solid rgba(255,91,120,.4); }
.mini:hover { background: rgba(255,91,120,.45); }
#pfSyncMsg { margin-top: 6px; }

/* the in-run FPS meter (toggle with F) */
#fps {
  position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
  font: 700 12px ui-monospace, monospace; color: #93e6a8;
  background: rgba(10, 6, 14, .6); padding: 3px 9px; border-radius: 6px;
  z-index: 9; pointer-events: none;
}

/* the death-screen XP + level-up line */
#deathXp.xp { color: #ffd98a; font-weight: 700; margin: 6px 0 2px; }
#deathXp .lvUp { color: #93e6a8; }

/* ---------- mobile thumb controls ---------- */
#touch {
  position: absolute; right: 20px; bottom: 24px; z-index: 8;
  display: flex; align-items: flex-end; gap: 14px;
  pointer-events: none;               /* the container ignores taps; the buttons don't */
}
.tbtn {
  pointer-events: auto; touch-action: none; -webkit-tap-highlight-color: transparent;
  width: 62px; height: 62px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 26px; line-height: 1; user-select: none;
  color: #ffd0d8; background: rgba(30, 10, 22, .6);
  border: 2px solid rgba(255, 91, 120, .45);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .5); backdrop-filter: blur(3px);
}
.tbtn.big {
  width: 88px; height: 88px; font-size: 38px;
  color: #fff; background: rgba(120, 16, 40, .62);
  border-color: rgba(255, 46, 87, .7);
}
.tbtn.press { transform: scale(.9); background: rgba(255, 46, 87, .55); border-color: #ff2e57; }

/* touch leave-to-menu button — top-left, shown only on touch in-game (base
   display:none hides it on desktop; body.touch reveals it; .hidden toggles it
   with the game state). The kill feed shifts down on touch so they never overlap. */
#tMenu {
  position: absolute; z-index: 9; display: none;
  top: calc(10px + env(safe-area-inset-top)); left: calc(12px + env(safe-area-inset-left));
  width: 42px; height: 42px; border-radius: 50%;
  align-items: center; justify-content: center;
  pointer-events: auto; touch-action: manipulation; -webkit-tap-highlight-color: transparent;
  font-size: 19px; font-weight: 700; line-height: 1; color: #ffd0d8;
  background: rgba(30, 10, 22, .62); border: 1.5px solid rgba(255, 91, 120, .45);
  box-shadow: 0 4px 14px rgba(0, 0, 0, .5); backdrop-filter: blur(3px);
}
body.touch #tMenu { display: flex; }
#tMenu.press { transform: scale(.9); background: rgba(255, 46, 87, .55); border-color: #ff2e57; }
/* the thumb-menu button owns the top-left corner on touch, so the feed starts lower —
   and its clamp has to start from THAT top, not the desktop 14px, or the extra 48px
   goes straight back into the contract card on a landscape phone. */
body.touch #killfeed {
  top: calc(62px + env(safe-area-inset-top));
  max-height: max(0px, calc(46vh - 70px - env(safe-area-inset-top)));
}

/* Hall of the Damned — weekly leaderboard panel on the menu */
#hallPanel {
  position: absolute; top: 20px; left: 20px; width: 260px; text-align: left;
  padding: 16px 18px; border-radius: 14px;
  background: rgba(18, 8, 20, .62); border: 1px solid rgba(255, 91, 120, .2);
  box-shadow: 0 20px 60px rgba(0, 0, 0, .5);
}
#hallPanel h3 { font-size: 14px; color: #ff8ea0; margin-bottom: 10px; letter-spacing: .3px; }
#hallPanel h3 .wk { font-size: 10px; opacity: .45; font-weight: 400; }
#hallPanel .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
#hallPanel h4 { font-size: 10px; text-transform: uppercase; letter-spacing: .5px; opacity: .6; margin-bottom: 6px; }
#hallPanel ol { list-style: none; font-size: 12px; font-variant-numeric: tabular-nums; }
#hallPanel li { display: flex; justify-content: space-between; gap: 6px; padding: 2px 0; }
#hallPanel li span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#hallPanel li span:last-child { color: #e8c86a; }
#hallPanel li.empty { opacity: .4; justify-content: flex-start; }

/* the Chronicle of Ages — how past ages ended, on the menu */
#chroniclePanel {
  position: absolute; top: 20px; right: 20px; width: 300px; text-align: left;
  padding: 16px 18px; border-radius: 14px;
  background: rgba(18, 8, 20, .62); border: 1px solid rgba(211, 27, 62, .25);
  box-shadow: 0 20px 60px rgba(0, 0, 0, .5);
}
#chroniclePanel h3 { font-size: 14px; color: #ff8ea0; margin-bottom: 10px; letter-spacing: .3px; }
#chroniclePanel ol { list-style: none; font-size: 11.5px; line-height: 1.35; }
#chroniclePanel li { display: flex; justify-content: space-between; gap: 8px; padding: 4px 0; border-bottom: 1px solid rgba(255, 91, 120, .08); }
#chroniclePanel li:last-child { border-bottom: none; }
#chroniclePanel li span:first-child { opacity: .85; }
#chroniclePanel li.vamp span:first-child { color: #ff8ea0; }
#chroniclePanel li.holy span:first-child { color: #e8c86a; }
#chroniclePanel li .ago { opacity: .4; white-space: nowrap; font-size: 10px; align-self: center; }

/* ---------- small screens / touch ---------- */
@media (max-width: 760px) {
  #board { display: none; }
  #hallPanel { display: none; }
  #chroniclePanel { display: none; }
  /* phones get the compact in-card buy button, not the 320px ad slot — which
     is too wide for portrait and piled onto the footer + legal text below it */
  #adBox { display: none; }
  .menuBuy { display: block; }
  #menu { padding-bottom: 20px; }
  #killfeed { max-width: 55vw; font-size: 11px; }
  #ticker { display: none; }        /* no room beside a thumb — the announce line carries it */
  #stats { font-size: 15px; }
  #evoBar { width: 150px; }
  /* the leaderboard is hidden on mobile, so the minimap moves to TOP-right,
     clear of the right-thumb action buttons that own the bottom-right corner */
  #mapWrap { top: 12px; right: 12px; bottom: auto; transform: scale(.72); transform-origin: top right; }
  #hint { display: none; }
  /* the fang/smite/split buttons ARE the ability readout on touch */
  #ability { display: none; }
  #abilityWrap { bottom: 96px; }    /* lift the status badges above the thumbs */
  #announce { white-space: normal; }   /* size handled by the responsive clamp above */
  #menu .card { padding: 26px 20px; margin: 0 12px; }
  #menu h1 { font-size: 34px; }
}

/* TOUCH DEVICES OF ANY WIDTH (phones AND tablets): the compact, thumb-clear
   layout — driven by the pointer, not the viewport. A touch tablet is wider
   than 760px but must NOT get the desktop layout with buttons on the minimap. */
body.touch #board,
body.touch #hallPanel,
body.touch #chroniclePanel { display: none; }
body.touch #mapWrap { top: 12px; right: 12px; bottom: auto; transform: scale(.72); transform-origin: top right; }
body.touch #ability { display: none; }        /* the fang/smite/split buttons ARE the readout */
body.touch #hint { display: none; }
body.touch #abilityWrap { bottom: 96px; }      /* status badges clear of the thumbs */
body.touch #badges { max-width: 94vw; gap: 5px; }
body.touch .badge { font-size: clamp(9.5px, 2.7vmin, 11px); padding: 3px 9px; letter-spacing: .3px; }

/* SAFE-AREA INSETS: keep every edge element out from under the notch, the
   rounded corners and the home indicator (env() is 0 on non-notched screens,
   so these are harmless everywhere else). */
#touch { right: calc(20px + env(safe-area-inset-right)); bottom: calc(24px + env(safe-area-inset-bottom)); }
#killfeed { top: calc(14px + env(safe-area-inset-top)); left: calc(16px + env(safe-area-inset-left)); }
#stats { left: calc(16px + env(safe-area-inset-left)); bottom: calc(16px + env(safe-area-inset-bottom)); }
body.touch #mapWrap { top: calc(12px + env(safe-area-inset-top)); right: calc(12px + env(safe-area-inset-right)); }
body.touch #abilityWrap { bottom: calc(96px + env(safe-area-inset-bottom)); }

/* The bottom-left readout and the bottom-right thumbs collide, and the threshold is
   GEOMETRY, not a guess: #stats runs to ~246px and #touch starts at (viewport - 20 - 164),
   so they meet on anything narrower than ~430px. Measured overlap: 375px -> 55px, 390px
   (iPhone 12-15) -> 40px, 393px (Pixel) -> 37px, 414px -> 16px, 430px -> clear. This rule
   used to be scoped to ≤360px, which caught almost no real phone — every common portrait
   handset lost the tail of "peak … · #rank" under the fang button. Lift it clear. */
@media (max-width: 430px) {
  body.touch #stats { bottom: calc(158px + env(safe-area-inset-bottom)); }
}
/* tiny phones (≤360px) additionally need smaller thumbs and a smaller readout — once
   lifted the readout has the full width, so shrinking is only for the genuinely cramped. */
@media (max-width: 360px) {
  .tbtn { width: 54px; height: 54px; font-size: 22px; }
  .tbtn.big { width: 74px; height: 74px; font-size: 32px; }
  body.touch #stats { font-size: 13px; }
  body.touch #stats .sub { font-size: 12px; }
}

/* short viewports (landscape phones): the ad box must never cover Play, and a
   tall menu card must be able to scroll to reach the button. */
@media (max-height: 620px) {
  #adBox { display: none; }
  .menuBuy { display: block; }
  #menu { justify-content: flex-start; padding: 10px 0 20px; }
  #menu .card { padding: 16px 22px; }
  #menu h1 { font-size: 30px; margin-bottom: 2px; }
  #menu .tag { font-size: 12px; margin-bottom: 12px; }
  #menu input { padding: 9px 14px; margin-bottom: 8px; }
  #hallPanel, #chroniclePanel { display: none; }
  #wrathWrap { width: min(300px, 90vw); }
  #kingName { font-size: 11px; }
}

/* very short viewports (landscape phones): pull the announce banner in tight so a
   long onboarding / throne line can never fill the screen. */
@media (max-height: 500px) {
  #announce { top: 11%; font-size: clamp(14px, 3.6vmin, 20px); max-width: 90vw; line-height: 1.15; }
  #announce.big { font-size: clamp(16px, 4.6vmin, 24px); }
}

/* ---------- Cosmetics Shop (The Blood Market) ---------- */
.shopOpenBtn { margin-top: 8px; width: 100%; }
#shopModal {
  position: fixed; inset: 0; z-index: 60; padding: 16px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(6, 3, 9, .8); backdrop-filter: blur(4px);
}
.shopSheet {
  position: relative; width: min(760px, 96vw); max-height: 88vh; overflow-y: auto;
  background: #120a18; border: 1px solid rgba(255, 91, 120, .35); border-radius: 16px;
  padding: 22px 22px 18px; box-shadow: 0 24px 80px rgba(0, 0, 0, .6);
}
/* .shopClose is position:absolute, so it is OUT OF FLOW and the heading ran underneath
   it — on a phone "cosmetic only ·" ended beneath the ✕ and orphaned "one-time" onto its
   own line. Reserve the button's lane (32px + its 12px offset + a gap). */
.shopSheet h3 { color: #ff5b78; font-size: 20px; margin-bottom: 14px; padding-right: 52px; }
/* the subtitle is a separate thought, not a continuation of the title — let it take its
   own line instead of wrapping mid-phrase against the close button */
.shopSub { color: #9c8ea8; font-size: 12px; font-weight: 500; letter-spacing: .5px; display: block; }
.shopClose {
  position: absolute; top: 12px; right: 12px; width: 32px; height: 32px;
  background: rgba(255, 255, 255, .06); border: 1px solid rgba(255, 255, 255, .15);
  border-radius: 50%; color: #e8dce8; cursor: pointer; font-size: 15px;
}
.shopClose:hover { background: rgba(255, 91, 120, .25); }
#shopGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
.shopCard {
  background: rgba(20, 10, 26, .7); border: 1px solid rgba(255, 255, 255, .08);
  border-radius: 12px; padding: 12px 10px 10px; text-align: center;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
}
.shopCard.t-rare      { border-color: rgba(255, 154, 106, .5); }
.shopCard.t-epic      { border-color: rgba(185, 140, 255, .55); }
.shopCard.t-legendary { border-color: rgba(255, 215, 122, .6); box-shadow: inset 0 0 18px rgba(255, 215, 122, .12); }
.shopPrev { width: 90px; height: 90px; }
.shopName { font-weight: 700; font-size: 14px; color: #f4e9ec; }
.shopTier { font-size: 10px; letter-spacing: 2px; text-transform: uppercase; opacity: .75; }
.shopCard.t-rare .shopTier      { color: #ff9a6a; }
.shopCard.t-epic .shopTier      { color: #b98cff; }
.shopCard.t-legendary .shopTier { color: #ffd77a; }
.shopAct {
  margin-top: 4px; width: 100%; padding: 8px 10px; border-radius: 999px; cursor: pointer;
  font-weight: 700; font-size: 13px; font-family: inherit; color: #fff;
  background: rgba(255, 91, 120, .22); border: 1px solid rgba(255, 91, 120, .6);
  transition: background .15s, border-color .15s;
}
.shopAct:hover:not(:disabled) { background: rgba(255, 91, 120, .4); border-color: #ff5b78; }
.shopAct.on, .shopAct:disabled { background: rgba(90, 230, 140, .16); border-color: rgba(90, 230, 140, .5); color: #b6f2cd; cursor: default; }
.shopFine { margin-top: 14px; font-size: 11px; opacity: .55; line-height: 1.4; }
.shopFine a { color: inherit; text-decoration: underline; }

/* ---------- Purchase protection: save-your-account-code ---------- */
.codeReminder {
  display: block; width: 100%; margin-top: 8px; padding: 9px 12px;
  font-family: inherit; font-size: 12.5px; font-weight: 700; cursor: pointer;
  color: #ffdca8; background: rgba(255, 176, 84, .14);
  border: 1px solid rgba(255, 176, 84, .55); border-radius: 10px;
  animation: pulse 2s ease-in-out infinite;
}
.codeReminder:hover { background: rgba(255, 176, 84, .26); }
#codeModal {
  position: fixed; inset: 0; z-index: 70; padding: 16px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(6, 3, 9, .82); backdrop-filter: blur(4px);
}
.codeSheet {
  position: relative; width: min(460px, 96vw);
  background: #140a1c; border: 1px solid rgba(255, 91, 120, .4); border-radius: 16px;
  padding: 24px 22px 20px; text-align: center; box-shadow: 0 24px 80px rgba(0, 0, 0, .6);
}
/* Same out-of-flow close button as .shopSheet: the ✕ is position:absolute, so the heading
   ran under it — measured 17px of collision at 360px. Reserve its lane. (Centred text, so
   pad both sides to keep the title optically centred in what is left.) */
.codeSheet h3 { color: #ff8ea0; font-size: 19px; margin-bottom: 12px; padding: 0 40px; }
.codeWarn { font-size: 13px; line-height: 1.5; color: #e8dce8; margin-bottom: 14px; }
.codeWarn b { color: #ffb054; }
.codeBox {
  background: #0a0710; border: 1px dashed rgba(255, 91, 120, .5); border-radius: 10px;
  padding: 12px; margin-bottom: 14px; word-break: break-all;
}
.codeBox code { font-size: 15px; letter-spacing: 1px; color: #ffd0d8; font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; }
.codeBtns { display: flex; gap: 10px; margin-bottom: 8px; }
.codeBtn {
  flex: 1; padding: 10px; border-radius: 999px; cursor: pointer; font-family: inherit;
  font-size: 13px; font-weight: 700; color: #f4e9ec;
  background: rgba(255, 255, 255, .06); border: 1px solid rgba(255, 255, 255, .18);
}
.codeBtn.primary { color: #fff; background: rgba(255, 91, 120, .25); border-color: rgba(255, 91, 120, .6); }
.codeBtn:hover { filter: brightness(1.2); }
.codeDone {
  width: 100%; margin-top: 6px; padding: 11px; border-radius: 999px; cursor: pointer;
  font-family: inherit; font-size: 14px; font-weight: 800; color: #0a0710;
  background: linear-gradient(90deg, #5ae68c, #93e6a8); border: none;
}
.codeDone:hover { filter: brightness(1.08); }

/* ============================================================================
   #seoContent — the landing page's crawlable body (see index.html).
   A search engine sees a <canvas> and a button; this is the prose that tells it
   what the page is, and it carries the only links to the guide pages. It is
   deliberately REAL and VISIBLE — a display:none block would be cloaking-adjacent
   and is discounted anyway. It sits below the card in #menu's existing scroll,
   so nothing above the fold moves.
   ========================================================================= */
#seoContent {
  position: relative; z-index: 2;
  width: min(92vw, 760px); margin: 26px auto 8px;
  animation: fxZoneIn .6s cubic-bezier(.16,1,.3,1) both; animation-delay: .44s;
}
.seoCard {
  background: rgba(19, 8, 21, .62);
  border: 1px solid rgba(255, 91, 120, .14);
  border-radius: 18px; padding: 26px 28px 24px;
  box-shadow: 0 20px 60px rgba(0,0,0,.45);
  text-align: left; font-size: 14.5px; line-height: 1.62; color: #cfc2d2;
}
.seoCard h2 {
  margin: 0 0 12px; font-size: 20px; line-height: 1.3; color: #ff8ea0; font-weight: 700;
}
.seoCard h2 .kw { color: #ff5b78; white-space: nowrap; }
.seoCard h3 { margin: 24px 0 8px; font-size: 15.5px; color: #f3d9e0; }
.seoCard p { margin: 0 0 12px; }
.seoCard b { color: #f4e9ec; }
.seoCard em { color: #e8dce8; font-style: italic; }

/* minmax(260px) rather than 220px on purpose: at the card's 704px content width
   220px fits THREE columns, which leaves the fourth feature stranded alone on a
   second row. 260px forces a clean 2x2 on desktop and stacks on phones. */
.seoFeat { list-style: none; margin: 18px 0 4px; padding: 0; display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.seoFeat li {
  background: rgba(255, 255, 255, .035); border: 1px solid rgba(255, 255, 255, .06);
  border-radius: 12px; padding: 11px 13px; display: flex; flex-direction: column; gap: 3px;
}
.seoFeat b { font-size: 14px; color: #f4e9ec; }
.seoFeat span { font-size: 13px; opacity: .8; line-height: 1.5; }

.seoLinks { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 2px; }
.seoLinks a {
  color: #d9c6e0; text-decoration: none; font-size: 13.5px;
  background: rgba(255, 91, 120, .1); border: 1px solid rgba(255, 91, 120, .22);
  border-radius: 999px; padding: 8px 14px; min-height: 38px; display: inline-flex; align-items: center;
  transition: background .15s, border-color .15s, color .15s;
}
.seoLinks a:hover { background: rgba(255, 91, 120, .22); border-color: rgba(255, 91, 120, .5); color: #fff; }

.seoFaq { margin: 4px 0 0; }
.seoFaq dt { font-weight: 700; color: #f3d9e0; margin: 14px 0 3px; font-size: 14px; }
.seoFaq dd { margin: 0; opacity: .84; }
.seoFaq a { color: #ff9fb0; }

@media (max-width: 560px) {
  .seoCard { padding: 20px 18px 18px; font-size: 14px; }
  .seoCard h2 { font-size: 18px; }
}
@media (prefers-reduced-motion: reduce) { #seoContent { animation: none !important; } }
