:root {
  --ink: #eae3d3;
  --ink-dim: #b8b0a0;
  --stone-900: #0d0f0e;
  --stone-800: #141613;
  --stone-700: #1c1f1b;
  --stone-600: #292d27;
  --iron: #565a52;
  --bronze: #b9873f;
  --fire: #e0782f;
  --fire-bright: #ff9a4d;
  --portal: #4fb0d9;
  --parchment: #d9c68a;
  --danger: #b0453f;
  --radius: 10px;
  --serif: Georgia, "Palatino Linotype", "Iowan Old Style", "Times New Roman", serif;
  --sans: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: radial-gradient(ellipse at top, var(--stone-800), var(--stone-900) 70%);
  color: var(--ink);
  font-family: var(--sans);
  min-height: 100%;
}

body {
  background-attachment: fixed;
}

a { color: var(--fire-bright); text-decoration: none; }
a:hover { text-decoration: underline; }

/* Landing page: full-bleed, no text/nav/chrome at all -- just three pieces
   of hero art crossfading, and the whole viewport is a single link into
   the interactive hero. Click/tap anywhere to enter. */
.landing {
  position: fixed;
  inset: 0;
  display: block;
  background: #000;
  cursor: pointer;
}
.landing-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* contain, not cover: never crop the art -- letterbox (using .landing's
     black background) on whichever side has leftover space instead. */
  object-fit: contain;
  opacity: 0;
  /* 38.1s -- the keyframes below hold each image fully visible (opacity:1)
     for 21% of the cycle, so this is solved for 21% * 38.1s = 8s held
     before each crossfade starts. Stagger below scaled to match (still
     exactly a third of the cycle each). */
  animation: landing-crossfade 38.1s ease-in-out infinite;
}
/* Three images, each offset by a third of the cycle, so exactly one is
   ever fully visible at a time and they hand off to each other. */
.landing-img:nth-of-type(1) { animation-delay: 0s; }
.landing-img:nth-of-type(2) { animation-delay: -12.7s; }
.landing-img:nth-of-type(3) { animation-delay: -25.4s; }
@keyframes landing-crossfade {
  0%    { opacity: 0; }
  6%    { opacity: 1; }
  27%   { opacity: 1; }
  33%   { opacity: 0; }
  100%  { opacity: 0; }
}

h1, h2, h3, h4 {
  font-family: var(--serif);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--parchment);
  margin: 0 0 0.4em;
}

.uncertain-tag, .verified-tag {
  display: inline-block;
  font-size: 0.7rem;
  padding: 0.1em 0.5em;
  border-radius: 999px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-left: 0.4em;
}
.verified-tag { background: rgba(79,176,217,0.15); color: var(--portal); border: 1px solid rgba(79,176,217,0.4); }
.uncertain-tag { background: rgba(224,120,47,0.12); color: var(--fire-bright); border: 1px solid rgba(224,120,47,0.4); }

/* ---------- nav ---------- */
.topnav {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.75rem 1.25rem;
  background: linear-gradient(180deg, rgba(13,15,14,0.97), rgba(13,15,14,0.9));
  border-bottom: 1px solid var(--stone-600);
  backdrop-filter: blur(6px);
  flex-wrap: wrap;
}
.topnav .brand {
  font-family: var(--serif);
  color: var(--parchment);
  font-size: 1.15rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.topnav .brand .ring { color: var(--bronze); }
.topnav nav {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  font-size: 0.92rem;
}
.topnav nav a { color: var(--ink-dim); }
.topnav nav a.active, .topnav nav a:hover { color: var(--fire-bright); }
.topnav .account {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.85rem;
}
.avatar-sm {
  width: 26px; height: 26px; border-radius: 50%;
  object-fit: cover; border: 1px solid var(--iron);
}

/* Bottom padding well clear of .music-bar (position:fixed, doesn't
   participate in document flow) -- see GitHub issue #1. */
main { max-width: 1180px; margin: 0 auto; padding: 1.5rem 1.25rem 6rem; }
.page-narrow { max-width: 820px; }
main.wide { max-width: 2200px; }

footer.site-footer {
  border-top: 1px solid var(--stone-600);
  padding: 2rem 1.25rem;
  /* The music bar is position:fixed at the bottom of the viewport (see
     .music-bar) -- it's not part of document flow, so main's own bottom
     padding doesn't clear it. Extra bottom padding here means the footer's
     own text never ends up sitting underneath it, even scrolled all the
     way to the true end of the page (see GitHub issue #1). */
  padding-bottom: 6rem;
  text-align: center;
  color: var(--ink-dim);
  font-size: 0.85rem;
}

/* ---------- buttons/forms ---------- */
.btn {
  display: inline-block;
  padding: 0.6em 1.3em;
  border-radius: var(--radius);
  border: 1px solid var(--bronze);
  background: linear-gradient(180deg, #3a2a16, #221809);
  color: var(--parchment);
  font-family: var(--serif);
  font-size: 1rem;
  cursor: pointer;
  transition: box-shadow 0.2s, transform 0.15s;
}
.btn:hover { box-shadow: 0 0 18px rgba(224,120,47,0.45); transform: translateY(-1px); text-decoration: none; }
.btn.primary { background: linear-gradient(180deg, var(--fire), #a5480f); border-color: var(--fire-bright); color: #170d04; font-weight: 600; }
.btn.ghost { background: transparent; border-color: var(--iron); color: var(--ink-dim); }
.btn.small { padding: 0.35em 0.9em; font-size: 0.85rem; }
.btn.danger { border-color: var(--danger); color: #f2b6b1; }

.form-error {
  padding: 0.7em 1em;
  margin-bottom: 1rem;
  border-radius: var(--radius);
  border: 1px solid var(--danger);
  background: rgba(176, 69, 63, 0.12);
  color: #f2b6b1;
  font-size: 0.9rem;
}

select, input[type=text], input[type=search], textarea, input[type=file] {
  background: var(--stone-700);
  border: 1px solid var(--stone-600);
  color: var(--ink);
  padding: 0.5em 0.7em;
  border-radius: 6px;
  font-family: var(--sans);
}
label { font-size: 0.85rem; color: var(--ink-dim); display: block; margin-bottom: 0.3em; }
.field { margin-bottom: 1rem; }
/* Group-membership checkboxes on /admin/music -- inline instead of the
   default block label, several per row (one per playlist group). */
.checkbox-inline {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  margin: 0 1em 0.4em 0;
  font-size: 0.85rem;
  color: var(--ink);
}
.checkbox-inline input { margin: 0; }

/* ---------- entry / hero ---------- */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

.hero-showcase {
  max-width: 2100px;
  margin: 0 auto;
  padding: 2rem 1.25rem 1rem;
  text-align: center;
}
.hero-frame {
  position: relative;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid var(--stone-600);
  box-shadow: 0 24px 70px -24px rgba(0,0,0,0.75), 0 0 40px -10px rgba(224,120,47,0.15);
  background: var(--stone-900);
}
.hero-scene { margin: 0; display: none; }
.hero-scene.active { display: block; }
.hero-scene img { display: block; width: 100%; height: auto; }

.hero-showcase .scene-caption {
  color: var(--ink-dim);
  font-size: 0.9rem;
  font-style: italic;
  margin: 1rem auto 0;
  max-width: 640px;
  min-height: 2.6em;
}

.scene-dots { display: flex; gap: 0.4rem; margin: 0.8rem 0 1.4rem; justify-content: center; }
.scene-dots button {
  width: 9px; height: 9px; border-radius: 50%; border: 1px solid var(--bronze);
  background: transparent; cursor: pointer; padding: 0;
}
.scene-dots button.active { background: var(--fire); }

.hero-actions { display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; }

/* ---------- portals / hall ---------- */
.section-title { display: flex; align-items: baseline; justify-content: space-between; margin: 2.5rem 0 1rem; flex-wrap: wrap; gap: 0.5rem; }
.section-title p { color: var(--ink-dim); margin: 0; font-size: 0.9rem; }

.current-expedition {
  border: 1px solid rgba(79,176,217,0.4);
  border-radius: 16px;
  padding: 1.5rem;
  background: linear-gradient(135deg, rgba(79,176,217,0.08), rgba(13,15,14,0.4));
  display: flex;
  gap: 1.5rem;
  flex-wrap: wrap;
  align-items: center;
}
.current-expedition .badge {
  text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.08em;
  color: var(--portal); border: 1px solid var(--portal); border-radius: 999px;
  padding: 0.2em 0.7em;
}

.portal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 1.1rem;
}
.portal-card {
  --pg: var(--fire);
  position: relative;
  border-radius: 16px;
  padding: 1.4rem 1.2rem;
  min-height: 190px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  border: 1px solid var(--stone-600);
  background:
    radial-gradient(circle at 50% 20%, color-mix(in srgb, var(--pg) 30%, transparent), transparent 65%),
    var(--stone-700);
  overflow: hidden;
  transition: transform 0.2s, box-shadow 0.2s;
}
.portal-card.has-image {
  --portal-img-size: cover;
  --portal-img-pos: center;
  background:
    linear-gradient(to top, color-mix(in srgb, var(--stone-900) 88%, transparent) 10%, color-mix(in srgb, var(--stone-900) 20%, transparent) 70%),
    var(--portal-img) var(--portal-img-pos)/var(--portal-img-size) no-repeat,
    var(--stone-700);
}
.portal-card.has-image .ring-arch { display: none; }
.portal-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 10px 30px -10px color-mix(in srgb, var(--pg) 60%, black);
  text-decoration: none;
}
.portal-card .ring-arch {
  position: absolute; top: -30px; left: 50%; transform: translateX(-50%);
  width: 130px; height: 130px; border-radius: 50%;
  border: 6px solid color-mix(in srgb, var(--pg) 70%, white 10%);
  opacity: 0.5;
  filter: blur(0.5px);
}
.portal-card h3 { color: var(--ink); font-size: 1.15rem; position: relative; }
.portal-card .tag { color: var(--ink-dim); font-size: 0.85rem; position: relative; }
.portal-card .count { position: absolute; top: 0.8rem; right: 1rem; font-size: 0.75rem; color: var(--ink-dim); }
.portal-card.other { min-height: 120px; opacity: 0.92; }

/* ---------- shrine ---------- */
.shrine-hero {
  border-radius: 18px;
  padding: 2.4rem 1.5rem;
  text-align: center;
  border: 1px solid var(--stone-600);
  background: radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--sg, var(--fire)) 25%, transparent), var(--stone-800) 70%);
  margin-bottom: 2rem;
}
.shrine-hero.has-image {
  --shrine-img-size: cover;
  --shrine-img-pos: center;
  background:
    radial-gradient(circle at 50% 0%, color-mix(in srgb, var(--sg, var(--fire)) 20%, transparent), color-mix(in srgb, var(--stone-800) 78%, transparent) 70%),
    var(--shrine-img) var(--shrine-img-pos)/var(--shrine-img-size) no-repeat,
    var(--stone-800);
}
.shrine-hero .tagline { color: var(--ink-dim); font-style: italic; }
.media-strip { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.8rem; }
.media-strip figure { margin: 0; border-radius: 10px; overflow: hidden; border: 1px solid var(--stone-600); background: var(--stone-700); }
.media-strip img, .media-strip video { width: 100%; height: 160px; object-fit: cover; display: block; }
.media-strip figcaption { padding: 0.4rem 0.6rem; font-size: 0.75rem; color: var(--ink-dim); }

/* ---------- gallery ---------- */
.filter-bar { display: flex; gap: 0.8rem; flex-wrap: wrap; align-items: end; margin-bottom: 1.5rem; }
.tag-row { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
.tag-link {
  display: inline-block;
  padding: 0.3em 0.8em;
  border-radius: 999px;
  font-size: 0.82rem;
  border: 1px solid var(--iron);
  color: var(--ink-dim);
  background: var(--stone-700);
}
.tag-link:hover { text-decoration: none; border-color: var(--bronze); color: var(--ink); }
.tag-link.active { border-color: var(--fire-bright); color: var(--fire-bright); background: color-mix(in srgb, var(--fire-bright) 12%, var(--stone-700)); }
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 0.9rem; }
.tile { position: relative; border-radius: 10px; overflow: hidden; border: 1px solid var(--stone-600); background: var(--stone-700); display: block; color: var(--ink); }
.tile .thumb { width: 100%; height: 140px; object-fit: cover; display: block; background: var(--stone-600); }
.tile .meta { padding: 0.5rem 0.6rem; font-size: 0.78rem; color: var(--ink-dim); }
.tile .meta .game { color: var(--fire-bright); }
.tile:hover { text-decoration: none; box-shadow: 0 0 0 1px var(--bronze); }
.badge-count { font-size: 0.7rem; background: var(--stone-600); border-radius: 999px; padding: 0.1em 0.5em; }

/* ---------- bloopers reel ---------- */
.bloopers-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; }
.blooper-tile { border-radius: 10px; overflow: hidden; border: 1px solid var(--stone-600); background: var(--stone-700); }
.blooper-video { width: 100%; display: block; background: #000; aspect-ratio: 16 / 9; }
.blooper-label { padding: 0.5rem 0.6rem; font-size: 0.78rem; color: var(--ink-dim); }

/* ---------- upvotes ---------- */
.vote-btn {
  display: inline-flex; align-items: center; gap: 0.3em;
  background: rgba(13, 15, 14, 0.7); border: 1px solid var(--stone-600);
  color: var(--ink-dim); border-radius: 999px; font-size: 0.75rem;
  padding: 0.2em 0.6em; cursor: pointer; line-height: 1.5;
}
.vote-btn:hover { border-color: var(--bronze); color: var(--ink); }
.vote-btn.voted { border-color: var(--fire-bright); color: var(--fire-bright); }
.vote-btn .heart { font-style: normal; }
.tile .vote-btn { position: absolute; top: 0.4rem; right: 0.4rem; z-index: 2; }
.asset-view .vote-btn { font-size: 0.9rem; padding: 0.35em 0.9em; margin: 0.6rem 0; }
.music-picker-track .vote-btn { flex: none; }

/* ---------- asset / message ---------- */
.asset-view { display: grid; grid-template-columns: 1.4fr 1fr; gap: 2rem; }
.asset-view .media img, .asset-view .media video { width: 100%; border-radius: 12px; border: 1px solid var(--stone-600); }
.edit-meta-form { display: none; margin-top: 0.8rem; }
.edit-meta-form.open { display: block; }
.msg-card {
  border: 1px solid var(--stone-600);
  border-radius: 10px;
  padding: 0.9rem 1rem;
  margin-bottom: 0.8rem;
  background: var(--stone-700);
}
.msg-card .msg-meta { display: flex; justify-content: space-between; font-size: 0.78rem; color: var(--ink-dim); margin-bottom: 0.4rem; flex-wrap: wrap; gap: 0.4rem; }
.msg-card .msg-body { white-space: pre-wrap; font-size: 0.92rem; line-height: 1.5; }
.msg-card .msg-attachments { margin-top: 0.6rem; display: flex; gap: 0.5rem; flex-wrap: wrap; }
.msg-card .msg-attachments img { width: 60px; height: 60px; object-fit: cover; border-radius: 6px; border: 1px solid var(--stone-600); }
.thread-scroll {
  max-height: 70vh;
  overflow-y: auto;
  padding-right: 0.4rem;
  border: 1px solid var(--stone-600);
  border-radius: 10px;
  padding: 0.8rem;
  background: var(--stone-800);
}
.thread-scroll .msg-card:last-child { margin-bottom: 0; }

@media (max-width: 800px) {
  .asset-view { grid-template-columns: 1fr; }
}

/* ---------- members ---------- */
.member-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1rem; }
.member-card { border: 1px solid var(--stone-600); border-radius: 12px; padding: 1rem; text-align: center; background: var(--stone-700); }
.member-card img { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; border: 2px solid var(--bronze); margin-bottom: 0.6rem; }

/* ---------- login modal ---------- */
.overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.65);
  display: none; align-items: center; justify-content: center; z-index: 100; padding: 1rem;
}
.overlay.open { display: flex; }
.modal {
  background: var(--stone-800); border: 1px solid var(--stone-600); border-radius: 14px;
  padding: 1.6rem; max-width: 420px; width: 100%;
}
.entry-modal .entry-choice {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin: 0 0 0.9rem;
}
.entry-modal .entry-choice strong { flex: 0 0 auto; }
.entry-modal .small-note { margin-top: 1.1rem; }
.profile-pick { display: flex; align-items: center; gap: 0.7rem; padding: 0.6rem; border-radius: 8px; border: 1px solid var(--stone-600); margin-bottom: 0.5rem; cursor: pointer; background: var(--stone-700); }
.profile-pick:hover { border-color: var(--bronze); }
.profile-pick img { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; }
.profile-list { max-height: 320px; overflow-y: auto; }

/* ---------- misc ---------- */
.panel { border: 1px solid var(--stone-600); border-radius: 14px; padding: 1.4rem; background: var(--stone-700); margin-bottom: 1.5rem; }
.pill { display: inline-block; padding: 0.15em 0.6em; border-radius: 999px; font-size: 0.72rem; border: 1px solid var(--iron); color: var(--ink-dim); margin-right: 0.3em; }
.status-online { color: #7fd18f; }
.status-offline { color: var(--danger); }
.thread-link { display:block; margin-top: 0.6rem; font-size: 0.85rem; }
.small-note { color: var(--ink-dim); font-size: 0.8rem; }

/* ---------- chronicles ---------- */
.tag-link.zero-hits { opacity: 0.4; }
.tag-link.zero-hits:hover { opacity: 0.7; }
mark { background: color-mix(in srgb, var(--fire-bright) 55%, transparent); color: var(--stone-900); border-radius: 3px; padding: 0 0.15em; }
.chronicles-filters { display: flex; gap: 0.8rem; flex-wrap: wrap; align-items: end; margin-bottom: 0.6rem; }
.chronicles-filters .field { margin-bottom: 0; }
.chronicles-filters label { display: block; font-size: 0.75rem; color: var(--ink-dim); margin-bottom: 0.25rem; }
.chronicles-filters select, .chronicles-filters input[type="date"] {
  background: var(--stone-700);
  border: 1px solid var(--iron);
  color: var(--ink);
  border-radius: 6px;
  padding: 0.4em 0.6em;
  font-size: 0.85rem;
}
.pagination { display: flex; align-items: center; gap: 0.8rem; margin: 1.5rem 0; justify-content: center; }
.pagination .btn.disabled { opacity: 0.35; pointer-events: none; }
table.arch-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
table.arch-table th, table.arch-table td { text-align: left; padding: 0.5rem; border-bottom: 1px solid var(--stone-600); vertical-align: top; }
code, pre { background: var(--stone-900); border-radius: 6px; padding: 0.1em 0.4em; }
pre { padding: 1rem; overflow-x: auto; }

/* ---------- interactive hero (Phase 2B PoC) ---------- */
.hero-poc-frame {
  position: relative;
  width: 100%;
  max-width: 2100px;
  aspect-ratio: var(--iw) / var(--ih);
  margin: 1.5rem auto 0;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid var(--stone-600);
  box-shadow: 0 24px 70px -24px rgba(0,0,0,0.75);
  background: var(--stone-900);
  line-height: 0;
}
.hero-poc-base { display: block; width: 100%; height: auto; }

/* Full-screen click-transition overlay (see hero-frame.ejs / hero-interactive.js).
   Above literally everything else on the page -- brand menu, account
   chip, music bar included -- while it plays. */
.click-transition-overlay {
  position: fixed;
  inset: 0;
  margin: auto;
  /* Below .site-panel's z-index (90): once the flame ends, the panel opens
     on top of its frozen last frame (rather than the video hiding first
     and exposing the live hero scene underneath for a flash) -- see
     hero-interactive.js/hero-panel.js. Still above the brand menu (20) and
     music bar for the brief moment before a panel opens. */
  z-index: 85;
  /* Same letterbox math as .hero-stage .hero-poc-frame (hardcoded to the
     hero art's known native 1672x941, since this element is a sibling of
     the frame and can't inherit its --iw/--ih custom properties) -- the
     transition video ends up occupying the exact same on-screen rectangle
     as the ambient hero, so there's no jump/zoom discontinuity between
     them, just a clean crossfade at matching scale and position. */
  width: 100vw;
  height: 100vh;
  max-width: calc(100vh * 1672 / 941);
  max-height: calc(100vw * 941 / 1672);
  object-fit: contain;
  background: #000;
  opacity: 0;
  pointer-events: none;
}
.click-transition-overlay.active {
  opacity: 1;
  pointer-events: auto;
}

/* Raven scene (see playRavenScene() in hero-interactive.js) -- overlaid on
   top of .click-transition-overlay while its reveal/idle clips play. The
   click target's box is a rough guess at where the raven sits in-frame
   (it's centered per the idle clip's own prompt); nudge left/top/width/
   height once seen live. */
.raven-scene-overlay {
  position: fixed;
  inset: 0;
  z-index: 86;
  pointer-events: none;
}
.raven-click-target {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: auto;
  cursor: pointer;
  background: transparent;
  border: none;
}
.raven-exclaim {
  position: absolute;
  left: 50%;
  top: 10%;
  transform: translate(-50%, -50%);
  font-family: var(--serif);
  font-weight: 900;
  font-size: clamp(5rem, 11vw, 9rem);
  -webkit-text-stroke: 3px #ffe6ab;
  color: #ffe6ab;
  text-shadow: 0 0 14px rgba(255, 200, 120, 0.9), 0 0 32px rgba(255, 150, 50, 0.6);
  opacity: 0;
  pointer-events: none;
}
.raven-exclaim.visible {
  animation: raven-exclaim-pop 0.5s ease-out;
  opacity: 1;
}
@keyframes raven-exclaim-pop {
  0% { transform: translate(-50%, -50%) scale(0.3); opacity: 0; }
  60% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
/* Ongoing invite-to-click pulse before the first click, distinct from the
   one-shot pop above used after each click. */
.raven-exclaim.pulsing {
  opacity: 0.9;
  animation: raven-exclaim-pulse 1.6s ease-in-out infinite;
}
@keyframes raven-exclaim-pulse {
  0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.75; }
  50% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; }
}
.raven-dialogue {
  position: absolute;
  left: 50%;
  top: 10%;
  transform: translate(-50%, -50%);
  max-width: 34rem;
  padding: 0.9rem 1.4rem;
  border: 1px solid rgba(212, 175, 120, 0.5);
  border-radius: 8px;
  background: rgb(17 22 21 / 90%);
  color: var(--parchment);
  font-size: clamp(1.4rem, 2.6vw, 2.1rem);
  font-weight: 600;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.raven-dialogue.visible { opacity: 1; }

/* Production hero stage: full-viewport, no header/footer -- the frame
   itself letterboxes (black bars) rather than cropping, same treatment as
   the landing page. Standard CSS letterbox trick: the frame's width/height
   both try to fill the viewport, but max-width/max-height (computed from
   the *other* axis via the locked aspect ratio) cap whichever dimension
   would overflow, and margin:auto centers the result within the
   position:fixed;inset:0 box. */
.hero-stage {
  position: fixed;
  inset: 0;
  background: #000;
  overflow: hidden;
}
.hero-stage .hero-poc-frame {
  position: fixed;
  inset: 0;
  margin: auto;
  width: 100vw;
  height: 100vh;
  max-width: calc(100vh * var(--iw) / var(--ih));
  max-height: calc(100vw * var(--ih) / var(--iw));
  border-radius: 0;
  border: none;
  box-shadow: none;
}

/* Baked-in navigation replacing the header/footer on the production hero:
   a small home mark (top-left), account/sign-in (top-right), and the
   music transport (bottom-center) -- all floating directly over the
   frame rather than in a separate chrome bar. */
/* Opened by clicking the in-scene Warders banner hotspot itself (see
   hero-brand-menu.js / hero-interactive.js) -- no separate floating icon
   button anymore. Fixed in the same top-left spot that button used to
   occupy, since anchoring it to the banner's actual on-screen position
   would need JS to track the letterboxed frame's rendered rect. */
/* Pill-tag row, same look as the Chronicles search tags (.tag-link) --
   not a plain stacked link list. */
.hero-brand-menu {
  display: none;
  flex-direction: row;
  flex-wrap: nowrap;
  justify-content: center;
  gap: 0.5rem;
  position: fixed;
  top: 1.2rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  width: max-content;
  max-width: calc(100vw - 2rem);
  padding: 1.3rem 1.6rem;
  border-radius: 14px;
  background: rgba(13, 15, 14, 0.92);
  border: 1px solid rgba(212, 175, 120, 0.35);
  backdrop-filter: blur(6px);
  box-shadow: 0 14px 40px -14px rgba(0,0,0,0.8);
}
.hero-brand-menu.open { display: flex; }
.hero-brand-menu .tag-link {
  text-decoration: none;
  font-size: 1.05rem;
  padding: 0.55em 1.2em;
}

.hero-account {
  position: fixed;
  top: 1rem;
  right: 1.1rem;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--parchment);
  font-size: 0.9rem;
  text-shadow: 0 0 10px rgba(0,0,0,0.9);
}

.gv-credit {
  position: fixed;
  z-index: 10;
  left: 12px;
  bottom: 12px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 9px;
  border: 1px solid rgb(231 236 226 / 20%);
  border-radius: 4px;
  background: rgb(17 22 21 / 88%);
  color: #eef2ec;
  font-size: 0.92rem;
  text-decoration: none;
  opacity: 0.75;
  transition: opacity 140ms ease;
}
.gv-credit:hover { opacity: 1; }

.secrets-counter {
  position: fixed;
  z-index: 10;
  right: 12px;
  bottom: 12px;
  padding: 6px 9px;
  border: 1px solid rgba(212, 175, 120, 0.35);
  border-radius: 4px;
  background: rgb(17 22 21 / 88%);
  color: var(--parchment);
  font-size: 0.92rem;
  opacity: 0.75;
  transition: opacity 140ms ease;
}
.secrets-counter:hover { opacity: 1; }
/* Blinks the whole badge magenta a few times (see hero-interactive.js)
   to draw the eye to it -- distinct from the 13/13-bonus-hint magenta
   elsewhere, just reusing the same color family for a consistent "look
   here" cue on this site. Color alone read as too subtle in practice --
   a background fill (not just the border) plus a scale pulse make it
   register as an actual attention-grab instead of something you'd only
   notice if already looking right at that corner. */
@keyframes secrets-counter-blink {
  0%, 100% { transform: scale(1); border-color: rgba(212, 175, 120, 0.35); background: rgb(17 22 21 / 88%); box-shadow: none; opacity: 0.75; }
  50% { transform: scale(1.12); border-color: #ff5fe0; background: rgba(255, 0, 200, 0.35); box-shadow: 0 0 16px 3px rgba(255, 0, 200, 0.85); opacity: 1; }
}
.secrets-counter--blink { animation: secrets-counter-blink 0.55s ease-in-out 3; }

/* One-shot "Secrets of the Iron Ring: N of 13+1 found" title card, shown
   once on arrival at the Hero (see hero-secrets-intro.js) -- rotates in
   edge-on (a 90deg Y rotation reads as a thin vertical "|" sliver, since
   only the text plane's edge faces the camera at that angle) then
   dismisses on its own timer or an early click/tap/keypress. Sits above
   everything else on the hero (z-index 150 -- nothing else here goes
   above the click-transition overlay's own 100s range) since it's the
   very first thing a visitor should see, but only ever for a few seconds
   right after page load. */
.hero-secrets-intro {
  position: fixed;
  inset: 0;
  z-index: 150;
  display: flex;
  align-items: center;
  justify-content: center;
  padding-bottom: 22vh;
  perspective: 1200px;
  cursor: pointer;
  /* Only the text itself fades in/out (opacity on .hero-secrets-intro-text)
     -- this container is full-viewport and fixed the entire time it
     exists in the DOM, including before the initial reveal delay elapses
     and after a dismiss finishes, so without this it silently intercepts
     every click anywhere on the page for however long it sits there
     (confirmed: broke a real hotspot/panel click in testing). Only
     actually catches clicks while --revealed is applied. */
  pointer-events: none;
}
.hero-secrets-intro.hero-secrets-intro--hidden { display: none; }
.hero-secrets-intro-text {
  max-width: min(92vw, 1100px);
  padding: 0 1.5rem;
  font-family: var(--serif);
  font-size: clamp(4.4rem, 14vw, 9.2rem);
  font-weight: 700;
  text-align: center;
  /* Georgia (the primary font in --serif) uses old-style figures by
     default -- digits with varying heights/baselines (some dipping below
     the baseline) rather than uniform lining numerals, which reads badly
     at this size for something like "10 of 13". Force lining, equal-width
     figures instead. */
  font-variant-numeric: lining-nums tabular-nums;
  /* Same color/glow as .hint-mark (the rotating "?" hologram hint) --
     just this element's own larger font-size, not that one's. */
  color: #ffe6ab;
  text-shadow: 0 0 10px rgba(255, 200, 120, 0.9), 0 0 24px rgba(255, 150, 50, 0.55);
  transform: rotateY(90deg);
  opacity: 0;
  transition: transform 1.1s cubic-bezier(0.22, 0.9, 0.28, 1.15), opacity 0.4s ease;
}
/* Sized relative to the parent's own huge clamp (0.4em, not a fixed unit)
   so it scales together with the count line, but stays proportionally
   smaller -- "Secrets Found:" is roughly twice as many characters as "0
   of 13", so at one shared font-size it either wraps mid-phrase or
   overflows its container; this guarantees it always fits on its own
   line instead. */
.hero-secrets-intro-label {
  display: inline-block;
  font-size: 0.4em;
  white-space: nowrap;
}
.hero-secrets-intro--revealed { pointer-events: auto; }
.hero-secrets-intro--revealed .hero-secrets-intro-text {
  transform: rotateY(0deg);
  opacity: 1;
}
/* Dismiss reads as a burst of light rather than a plain fade -- a quick
   brightness flash, then scaling rapidly outward past the viewer while
   blurring and fading, all in one animation (a plain opacity transition
   can't do the flash-then-burst shape; this needs actual keyframes). */
.hero-secrets-intro--dismissing .hero-secrets-intro-text {
  animation: hero-secrets-intro-explode 0.6s cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
}
@keyframes hero-secrets-intro-explode {
  0% { transform: rotateY(0deg) scale(1); opacity: 1; filter: brightness(1) blur(0); }
  20% { transform: rotateY(0deg) scale(1.15); opacity: 1; filter: brightness(1.9) blur(0); }
  100% { transform: rotateY(0deg) scale(2.8); opacity: 0; filter: brightness(2.4) blur(18px); }
}
/* Bonus-hint sequence only (see showSecretsBonusIntro in
   hero-interactive.js) -- a "wait, there's more" beat between the normal
   "13 of 13" reveal and swapping to "Bonus Secret Unlocked", distinct
   from the dismiss explode above. */
.hero-secrets-intro--blinking .hero-secrets-intro-text {
  animation: hero-secrets-intro-blink 0.25s steps(1, end) 6;
}
@keyframes hero-secrets-intro-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.15; }
}
/* Distinct from the normal gold count text -- magenta ties it to the
   hologram chroma-key backdrops (and whatever eventually leads into the
   song-picker highlight this is meant to hint at). */
#hero-secrets-intro-bonus {
  color: #ff5fe0;
  text-shadow: 0 0 10px rgba(255, 95, 224, 0.95), 0 0 24px rgba(255, 0, 200, 0.6);
}
/* EXPERIMENTAL -- bonus-sequence-only dismiss variant, in place of the
   normal explode. CSS can't morph typography into a literal ball, so this
   cross-fades the text into a *separate* round element instead
   (.hero-secrets-bonus-ball, a radial-gradient "ember" with its own
   flicker) -- timed so it reads as the words wadding up into a
   sparkling, burning ball, which then drifts through a loop around the
   screen before fading out. Both elements are descendants of
   #hero-secrets-intro, so the same --collapsing/--swirling classes
   toggled there by dismissBonusIntroWithSwirl (hero-interactive.js)
   drive both at once -- no separate JS hooks needed for the ball.
   Isolated from hero-secrets-intro-explode entirely, so the normal
   dismiss is unaffected either way this turns out. Revert: change
   dismissBonusIntroWithSwirl's one call site back to
   dismissSecretsIntro() and remove this block plus the ball element in
   hall-interactive.ejs. */
.hero-secrets-intro--collapsing .hero-secrets-intro-text {
  /* Shrinks down to well under the ball's own on-screen size -- not a
     fade, ends fully opaque, just blinks a couple of times right at the
     very end (see 80-95% below) as the ball itself fades in on top of
     it, reinforcing the handoff. Growing blur (not overall opacity) is
     what reads as "melting into a blob" rather than just "getting
     smaller" -- font-weight is included too, but Georgia isn't a
     variable font, so that jumps to bold rather than smoothly
     thickening; the blur is doing the real visual work there. */
  animation: hero-secrets-bonus-text-shrink 0.9s ease-in forwards;
}
@keyframes hero-secrets-bonus-text-shrink {
  /* Squeezing width down harder than height (scaleX well below scaleY,
     not a uniform scale()) reads as the sides actually getting crushed
     inward, not just "the same shape getting smaller." */
  0% { transform: scaleX(1) scaleY(1); filter: blur(0); opacity: 1; font-weight: 700; }
  60% { transform: scaleX(0.45) scaleY(0.55); filter: blur(2px); opacity: 1; font-weight: 800; }
  80% { transform: scaleX(0.06) scaleY(0.18); filter: blur(5px); opacity: 1; font-weight: 900; }
  85% { opacity: 0.15; }
  90% { opacity: 1; }
  95% { opacity: 0.15; }
  100% { transform: scaleX(0.06) scaleY(0.18); filter: blur(5px); opacity: 1; font-weight: 900; }
}
.hero-secrets-bonus-ball {
  position: absolute;
  /* Matches .hero-secrets-intro's own padding-bottom: 22vh (the "raise it
     up higher" offset) exactly, rather than inset:0 -- an absolutely
     positioned descendant centers against its containing block's full
     padding box, which (padding only shrinks the *content* box available
     to in-flow flex children like .hero-secrets-intro-text) is NOT the
     same reduced region the text is actually centered within. Without
     this the ball centered at the true viewport middle while the text
     sat ~11vh above it -- confirmed visually, not just in theory. */
  top: 0;
  right: 0;
  left: 0;
  bottom: 22vh;
  margin: auto;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  transform: scale(0);
  background: radial-gradient(circle at 35% 35%, #fff6f9 0%, #ff9ae8 12%, #ff2fd6 32%, #d600b0 60%, rgba(214, 0, 176, 0) 100%);
  box-shadow: 0 0 18px 6px rgba(255, 0, 220, 0.95), 0 0 36px 14px rgba(214, 0, 200, 0.7);
}
.hero-secrets-intro--collapsing .hero-secrets-bonus-ball {
  /* Delayed to start partway through the text-shrink above, so the ball
     visibly fades in "catching up" as the text shrinks past its size,
     the two finishing together rather than the ball just appearing
     instantly alongside the shrink from the very start. */
  animation-name: hero-secrets-bonus-ball-in;
  animation-duration: 0.4s;
  animation-timing-function: ease-out;
  animation-delay: 0.5s;
  animation-fill-mode: both;
}
.hero-secrets-intro--settling .hero-secrets-bonus-ball {
  /* Brief in-place bob right after the ball fades in -- unlike the old
     hover-and-fade, this does NOT fade out; it's still fully opaque when
     the class is removed, right before the ball actually starts
     traveling (see dismissBonusIntroWithSwirl in hero-interactive.js). */
  animation:
    hero-secrets-bonus-ball-settle 1.1s ease-in-out forwards,
    hero-secrets-bonus-ball-flicker 0.35s ease-in-out infinite;
}
@keyframes hero-secrets-bonus-ball-in {
  0% { transform: scale(0); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes hero-secrets-bonus-ball-flicker {
  0%, 100% { filter: brightness(1) saturate(1); }
  50% { filter: brightness(1.7) saturate(1.35); }
}
@keyframes hero-secrets-bonus-ball-settle {
  0% { transform: translate(0, 0) scale(1); }
  25% { transform: translate(0, -4px) scale(1.05); }
  50% { transform: translate(0, 3px) scale(0.97); }
  75% { transform: translate(0, -3px) scale(1.04); }
  100% { transform: translate(0, 0) scale(1); }
}
/* Picker toggle icon: a few rapid magenta flashes right as the ball
   arrives and fades out (--blink, temporary), then a permanent magenta
   recolor (--marked, persists across visits -- only cleared by
   secretsClearBtn in hero-interactive.js). The button's SVG uses
   fill="currentColor" (see .music-btn svg), so recoloring the button
   itself recolors the icon. */
#music-picker-toggle.music-picker-toggle--blink {
  animation: music-picker-toggle-blink 0.2s steps(1, end) 6;
}
@keyframes music-picker-toggle-blink {
  /* color only, deliberately -- no filter/drop-shadow here. That would
     force a blur repaint on every step even for a brief 1.2s blink;
     recoloring an SVG that already uses fill="currentColor" is just a
     cheap repaint, no blur/filter recomputation at all. */
  0%, 100% { color: var(--parchment); }
  50% { color: #ff2fd6; }
}
#music-picker-toggle.music-picker-toggle--marked {
  color: #ff2fd6;
  filter: drop-shadow(0 0 4px rgba(255, 0, 200, 0.85));
  opacity: 1;
}
/* The specific track this whole bonus hint points at (see
   BONUS_TRACK_TITLE in hero-music.js) -- same persistence/clearing rules
   as the picker icon above. */
.music-picker-track--bonus .music-picker-track-title {
  color: #ff2fd6;
  text-shadow: 0 0 6px rgba(255, 0, 200, 0.6);
}
@media (prefers-reduced-motion: reduce) {
  .hero-secrets-intro--collapsing .hero-secrets-intro-text,
  .hero-secrets-intro--collapsing .hero-secrets-bonus-ball,
  .hero-secrets-intro--settling .hero-secrets-bonus-ball {
    animation: none;
    opacity: 0;
    transition: opacity 0.3s ease;
  }
}
@media (prefers-reduced-motion: reduce) {
  .hero-secrets-intro-text { transition: opacity 0.3s ease; transform: none; }
  .hero-secrets-intro--dismissing .hero-secrets-intro-text { animation: none; opacity: 0; transition: opacity 0.3s ease; }
  .hero-secrets-intro--blinking .hero-secrets-intro-text { animation: none; }
}

.secrets-list {
  display: none;
  position: absolute;
  right: 0;
  bottom: calc(100% + 8px);
  width: 400px;
  max-width: min(400px, calc(100vw - 32px));
  max-height: 75vh;
  overflow-y: auto;
  padding: 12px;
  border: 1px solid rgba(212, 175, 120, 0.35);
  border-radius: 6px;
  background: rgb(17 22 21 / 94%);
}
.secrets-counter:hover .secrets-list,
.secrets-counter:focus .secrets-list,
.secrets-counter:focus-within .secrets-list,
.secrets-counter--open .secrets-list {
  display: block;
}
.secrets-arrow {
  display: inline-block;
  font-size: 1.4rem;
  line-height: 0.7;
  vertical-align: middle;
  transition: transform 150ms ease;
}
.secrets-counter:focus .secrets-arrow,
.secrets-counter:focus-within .secrets-arrow {
  transform: rotate(180deg);
}
.secrets-list-row {
  display: flex;
  gap: 10px;
  padding: 7px 3px;
  font-size: 0.86rem;
  line-height: 1.35;
  color: rgba(240, 230, 210, 0.65);
  border-bottom: 1px solid rgba(212, 175, 120, 0.12);
}
.secrets-list-row:last-child { border-bottom: none; }
.secrets-list-row.found { color: var(--parchment); }
.secrets-list-num {
  flex: 0 0 auto;
  min-width: 1.2em;
  color: rgba(212, 175, 120, 0.7);
  font-variant-numeric: tabular-nums;
}
.secrets-list-row.found .secrets-list-num { color: #d4af78; }
.secrets-list-text { flex: 1 1 auto; }
.secrets-list-row.found .secrets-list-text { font-weight: 600; }

.secrets-clear-btn {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 6px 10px;
  border: 1px solid rgba(212, 175, 120, 0.3);
  border-radius: 4px;
  background: transparent;
  color: rgba(240, 230, 210, 0.6);
  font-size: 0.75rem;
  cursor: pointer;
  transition: color 140ms ease, border-color 140ms ease;
}
.secrets-clear-btn:hover {
  color: var(--parchment);
  border-color: rgba(212, 175, 120, 0.6);
}

.music-bar {
  position: fixed;
  left: 50%;
  bottom: 1.4rem;
  transform: translateX(-50%);
  /* Above .site-panel (90) and its .site-panel-bar (92) -- panels are a
     full-screen overlay, but music keeps playing underneath them the whole
     time (see hero-panel.js), so the transport controls need to stay
     visible/clickable while any panel is open, not just on the bare hero.
     Still below .lightbox (200), which is fine to cover it. */
  z-index: 95;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.55em 1.1em;
  border-radius: 999px;
  background: rgba(13, 15, 14, 0.6);
  border: 1px solid rgba(212, 175, 120, 0.35);
  backdrop-filter: blur(6px);
  box-shadow: 0 10px 34px -12px rgba(0,0,0,0.7);
}
/* On the interactive hero specifically, bottom-center overlaps the
   Lineage II "roach hole" hotspot on non-ultrawide screens (the hotspot
   is positioned against the scaled hero image, this bar against the raw
   viewport -- they only line up on wide-enough aspect ratios). Move it
   under the sign-in control instead, only here -- other pages keep the
   bottom-center placement, which isn't fighting any hotspot (see GitHub
   issue #5). */
.hero-stage .music-bar {
  top: 3.3rem;
  right: 1.1rem;
  left: auto;
  bottom: auto;
  transform: none;
}
/* Landscape phones: the hero frame's aspect ratio is close enough to the
   viewport's that it fills nearly the whole screen (unlike portrait, where
   a wide letterboxed margin keeps these badges clear of it entirely) --
   there's no free space to move them into, so shrink their footprint
   instead. Less area covered means less chance any given badge actually
   sits on top of a hotspot underneath it. Height-based, not width-based,
   since that's what actually distinguishes "phone rotated sideways" from
   a normal desktop window. */
@media (max-height: 500px) and (orientation: landscape) {
  .hero-stage .music-bar {
    top: 0.5rem;
    right: 0.5rem;
    gap: 0.5rem;
    padding: 0.3em 0.6em;
  }
  .hero-stage .music-bar .music-track-title { display: none; }
  .hero-stage .music-bar .music-volume-wrap { display: none; }
  .gv-credit, .secrets-counter {
    padding: 3px 6px;
    font-size: 0.6rem;
    bottom: 6px;
  }
  .gv-credit { left: 6px; }
  .secrets-counter { right: 6px; }
}
.music-btn {
  background: none;
  border: none;
  padding: 0;
  color: var(--parchment);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0.75;
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.music-btn:hover { opacity: 1; transform: scale(1.08); }
.music-btn svg { width: 1.15rem; height: 1.15rem; fill: currentColor; }
.music-btn--play {
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 50%;
  background: color-mix(in srgb, var(--fire-bright) 85%, transparent);
  opacity: 1;
}
.music-btn--play svg { width: 1.3rem; height: 1.3rem; }
.music-btn--play .icon-pause { display: none; }
.music-btn--play.playing .icon-play { display: none; }
.music-btn--play.playing .icon-pause { display: block; }

.music-track-wrap {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  max-width: 16rem;
  padding-left: 0.2rem;
  border-left: 1px solid rgba(212, 175, 120, 0.35);
}
.music-track-title {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--parchment);
  font-family: var(--serif);
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  opacity: 0.9;
}
/* Elapsed/duration for the currently playing track -- a plain readout,
   not a scrubber (not clickable) -- see hero-music.js's timeupdate
   listener for what actually moves the fill. Distinct in shape from the
   volume ramp (a diagonal wedge, see .music-volume below) specifically
   so the two can't be confused for each other, which people kept doing
   before either existed: one thin flat line under the title reads as
   "how far into this song," nothing else on this bar looks like that. */
.music-progress {
  display: block;
  height: 3px;
  border-radius: 999px;
  background: rgba(212, 175, 120, 0.2);
  overflow: hidden;
}
.music-progress-fill {
  display: block;
  height: 100%;
  width: 0%;
  background: var(--fire-bright);
  border-radius: inherit;
}
@media (max-width: 640px) {
  .music-track-wrap { max-width: 8rem; }
}

.music-volume-wrap {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
/* .music-btn svg (the generic rule every other icon button uses) is more
   specific than a bare .music-volume-icon class alone would be, so this
   needs the .music-btn ancestor qualifier too, or that generic 1.15rem
   sizing wins instead of this rule's own. */
.music-btn .music-volume-icon {
  width: 1.4rem;
  height: 1.4rem;
  flex: none;
  cursor: pointer;
  fill: var(--parchment);
  opacity: 0.7;
}
/* Native <input type=range> pseudo-thumbs (::-webkit-slider-thumb /
   ::-moz-range-thumb) turned out not to be a viable place to put a
   knob that visibly protrudes past the track -- at least one engine
   still visually clips the thumb to the *local* height of the
   ::-webkit-slider-runnable-track's own clip-path at whatever x
   position it's sitting at, regardless of the thumb's own declared
   height. No combination of thumb height/color fixed that, because
   the thumb was never actually a free-standing element -- it's a
   pseudo-element bound to (and, in practice, visually gated by) the
   track it belongs to.
   So the input itself is now purely an invisible hit-target/keyboard
   handler (opacity: 0, native track+thumb suppressed below), and the
   actual visuals -- the wedge-shaped ramp and the knob -- are two real
   sibling <span>s, absolutely positioned inside .music-volume-control,
   drawn independently of each other and of the input. That's the only
   way to guarantee the knob is a genuinely separate, unclipped element
   rather than something the ramp's own shape can gate. --vol is set
   by hero-music.js on #music-volume-control (the shared ancestor of
   track/knob/input), not on the input itself, so both siblings can
   read it via inheritance. */
.music-volume-control {
  position: relative;
  width: 4rem;
  height: 1.9rem;
}
/* A right triangle -- flat along the bottom and right edges, a single
   smooth diagonal from the bottom-left corner up to the top-right --
   not stepped. Shorter than, and vertically centered within,
   .music-volume-control -- the knob (below) is taller than this and
   deliberately protrudes above/below it. */
.music-volume-track {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1.3rem;
  transform: translateY(-50%);
  clip-path: polygon(0% 100%, 100% 0%, 100% 100%);
  background: linear-gradient(
    to right,
    #7a3d10 0%,
    var(--fire-bright) var(--vol, 50%),
    var(--stone-600) var(--vol, 50%),
    var(--stone-600) 100%
  );
  pointer-events: none;
}
/* The actual handle. `left` is set inline by hero-music.js (matching
   --vol) rather than in CSS, since centering it on an arbitrary
   percentage needs the translateX(-50%) below applied on top of it.
   Deliberately NOT fire-bright -- that's also the ramp's own "filled"
   color, so a same-colored knob would visually fuse with the fill the
   moment it sits past the boundary. Parchment reads as a genuinely
   separate control against both the orange fill and the grey unfilled
   track. */
.music-volume-knob {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0.7rem;
  height: 1.9rem;
  transform: translate(-50%, -50%);
  border-radius: 4px;
  background: var(--parchment);
  border: 2px solid var(--stone-900);
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.7);
  pointer-events: none;
}
.music-volume-control:focus-within .music-volume-knob {
  outline: 2px solid var(--fire-bright);
  outline-offset: 1px;
}
.music-volume {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  opacity: 0;
}
.music-volume::-webkit-slider-runnable-track,
.music-volume::-webkit-slider-thumb,
.music-volume::-moz-range-track,
.music-volume::-moz-range-thumb,
.music-volume::-moz-range-progress {
  background: transparent;
  border: none;
  box-shadow: none;
}
@media (max-width: 640px) {
  .music-volume-control { width: 3.2rem; }
}

/* Site-wide music picker: browse every playlist and pick any track --
   replaces the dedicated /music page. Anchored above the music-bar. */
.music-picker {
  display: none;
  position: fixed;
  left: 50%;
  bottom: 4.6rem;
  transform: translateX(-50%);
  z-index: 96; /* one above .music-bar, same reasoning -- see that rule */
  width: min(26rem, calc(100vw - 2rem));
  max-height: min(60vh, 30rem);
  overflow-y: auto;
  padding: 0.6rem;
  border-radius: 14px;
  background: rgba(13, 15, 14, 0.92);
  border: 1px solid rgba(212, 175, 120, 0.35);
  backdrop-filter: blur(6px);
  box-shadow: 0 14px 40px -14px rgba(0,0,0,0.8);
}
.music-picker.open { display: block; }
.music-picker-group + .music-picker-group { margin-top: 0.6rem; }
.music-picker-group-label {
  font-family: var(--serif);
  font-size: 0.85rem;
  color: var(--bronze);
  padding: 0.3em 0.5em;
  opacity: 0.9;
}
.music-picker-track {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  border-radius: 6px;
}
.music-picker-track:hover { background: rgba(212, 175, 120, 0.15); }
.music-picker-track-title {
  flex: 1;
  text-align: left;
  background: none;
  border: none;
  color: var(--parchment);
  font-family: inherit;
  font-size: 0.88rem;
  padding: 0.45em 0.6em;
  cursor: pointer;
}
.music-picker-track-title.playing { color: var(--fire-bright); }
.music-picker-shuffle {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  border-radius: 6px;
  padding: 0.3em 0.6em 0.5em;
  font-family: inherit;
  font-size: 0.78rem;
  font-style: italic;
  color: var(--ink-dim);
  letter-spacing: 0.03em;
  cursor: pointer;
}
.music-picker-shuffle:hover { background: rgba(212, 175, 120, 0.15); color: var(--fire-bright); }
.music-picker-divider { height: 1px; margin: 0.5rem 0.3rem; background: var(--stone-600); }

.hero-poc-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Never use display:none on a <video> we need to preload/decode in the
   background (the intro→loop swap, the hologram source) — Chrome skips
   loading media entirely for display:none elements. opacity:0 keeps the
   element "live" for decode/buffering while staying invisible. */
.hero-video-pending { opacity: 0; pointer-events: none; }
.hologram-source {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
  overflow: hidden;
}

.ring-hologram {
  /* left/top/width/height set per-hologram via inline style (see
     src/data/holograms.js `box`) -- different objects need different
     proportions to avoid looking stretched/squashed. */
  position: absolute;
  z-index: 5;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.6s ease;
  filter: drop-shadow(0 0 22px rgba(120, 255, 150, 0.55)) drop-shadow(0 0 46px rgba(120, 255, 150, 0.25));
}
.ring-hologram canvas {
  width: 100%;
  height: 100%;
  display: block;
}
.ring-hologram.visible { opacity: 1; }

/* CSS-only hologram variant: a rotating "?" mark, popped above the ring
   when hovering any of the 13 non-portal POI hotspots -- a wordless hint
   that something's hidden there, no tooltip text of its own. */
.ring-hologram--hint {
  display: flex;
  align-items: center;
  justify-content: center;
  filter: drop-shadow(0 0 16px rgba(255, 205, 120, 0.65)) drop-shadow(0 0 34px rgba(255, 160, 60, 0.35));
}
.hint-mark {
  font-family: var(--serif);
  font-weight: 700;
  font-size: clamp(4.6rem, 10vw, 8.5rem);
  color: #ffe6ab;
  text-shadow: 0 0 10px rgba(255, 200, 120, 0.9), 0 0 24px rgba(255, 150, 50, 0.55);
  animation: hint-spin 3.4s ease-in-out infinite;
}
/* Once a POI's been found, its hover shows a short abbreviation (e.g.
   "SWTOR") instead of the single "?" glyph -- same spin, much smaller font
   so multi-character text doesn't overflow the hologram's box. */
.hint-mark--abbr {
  font-size: clamp(1.6rem, 3.2vw, 2.6rem);
  white-space: nowrap;
}
@keyframes hint-spin {
  0% { transform: perspective(600px) rotateY(0deg) scale(1); }
  50% { transform: perspective(600px) rotateY(180deg) scale(1.1); }
  100% { transform: perspective(600px) rotateY(360deg) scale(1); }
}

/* ---------- dev-only coordinate picker ---------- */
.devtools-readout {
  position: absolute;
  left: 0.6rem;
  top: 0.6rem;
  z-index: 50;
  background: rgba(13, 15, 14, 0.88);
  border: 1px solid var(--bronze);
  color: var(--parchment);
  font-family: var(--mono, monospace);
  font-size: 0.8rem;
  padding: 0.35em 0.7em;
  border-radius: 6px;
  pointer-events: none;
  white-space: nowrap;
}
.devtools-marker {
  position: absolute;
  width: 10px;
  height: 10px;
  margin: -5px 0 0 -5px;
  border-radius: 50%;
  background: var(--fire-bright);
  border: 2px solid #fff;
  z-index: 51;
  pointer-events: none;
}

.devtools-toggle {
  position: absolute;
  right: 0.6rem;
  top: 0.6rem;
  z-index: 50;
  background: rgba(13, 15, 14, 0.88);
  border: 1px solid var(--bronze);
  color: var(--parchment);
  font-family: var(--sans);
  font-size: 0.75rem;
  padding: 0.3em 0.7em;
  border-radius: 6px;
  cursor: pointer;
}

.devtools-log {
  max-width: 2100px;
  margin: 0.8rem auto 0;
  padding: 0.8rem 1rem;
  border: 1px solid var(--stone-600);
  border-radius: 10px;
  background: var(--stone-800);
}
.devtools-log-title {
  font-size: 0.8rem;
  color: var(--ink-dim);
  margin-bottom: 0.5rem;
}
.devtools-log-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.3rem 0;
  border-top: 1px solid var(--stone-600);
}
.devtools-log-row:first-of-type { border-top: none; }
.devtools-log-text {
  flex: 1;
  font-size: 0.82rem;
  color: var(--parchment);
  user-select: all;
  cursor: text;
}
.devtools-log-copy {
  flex-shrink: 0;
  background: var(--stone-700);
  border: 1px solid var(--bronze);
  color: var(--parchment);
  font-size: 0.75rem;
  padding: 0.25em 0.7em;
  border-radius: 5px;
  cursor: pointer;
}

/* Permanent hitbox outlines, toggled by .devtools-toggle. Every .hotspot
   gets a visible boundary regardless of hover/technique, so all boxes can
   be compared against the art at once instead of one at a time. */
.hero-poc-frame.debug-hitboxes .hotspot {
  outline: 2px dashed #ff3b3b;
  outline-offset: -1px;
  background: rgba(255, 59, 59, 0.08);
}
.devtools-hitbox-label {
  position: absolute;
  left: 2px;
  top: 2px;
  z-index: 52;
  background: rgba(0, 0, 0, 0.75);
  color: #ff9b9b;
  font-family: var(--sans);
  font-size: 0.65rem;
  padding: 0.1em 0.35em;
  border-radius: 3px;
  pointer-events: none;
  display: none;
}
.hero-poc-frame.debug-hitboxes .devtools-hitbox-label { display: block; }

/* Dashed oval showing where a wave-rise portal's exclusion zone actually
   is (see setupWaveExcludeOverlay in hero-devtools.js), positioned
   directly in the frame's own coordinate space -- same toggle as the
   hitbox outlines. */
.devtools-wave-exclude {
  position: absolute;
  z-index: 53;
  border-radius: 50%;
  border: 2px dashed #3bf3ff;
  background: rgba(59, 243, 255, 0.12);
  pointer-events: none;
  display: none;
}
.hero-poc-frame.debug-hitboxes .devtools-wave-exclude { display: block; }

/* Ambient cockroach easter egg (poi-lineage2-crack): always animating, not
   tied to hover -- a scene detail rather than an interaction reward. Lives
   in its own oversized "stage" box (hotspots.js `critterBox`) centered on
   the actual crack, much bigger than the tiny hitbox itself so the roach
   has room to dart around near it, and deliberately NOT clipped by
   overflow:hidden the way .hotspot children are (it needs to range freely
   within its stage). Sits above the base art, below the hotspot layer. */
.critter-roach {
  position: absolute;
  z-index: 2;
  pointer-events: none;
}
.critter-roach-body {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 22%;
  height: auto;
  transform-origin: 50% 50%;
  transform: translate(-50%, -50%) scale(0);
  animation: roach-dart 16s steps(6, end) infinite;
}
/* Emerge from the crack, dart to three nearby points with a hold at each.
   steps(6, end) applied across the whole animation means the holds (start
   and end value identical) show no motion regardless, but the actual dart
   segments below are now given a real span of time (not a near-instant
   0.3%) so that stepped motion is visible as a multi-frame skitter instead
   of a single invisible jump, then retreat back into the crack and pause
   before the next cycle. */
@keyframes roach-dart {
  0%     { transform: translate(-50%, -50%) scale(0); }
  4%     { transform: translate(-50%, -50%) scale(1); }
  16%    { transform: translate(-50%, -50%) scale(1); }
  21%    { transform: translate(calc(-50% + 160%), calc(-50% - 60%)) scale(1); }
  33%    { transform: translate(calc(-50% + 160%), calc(-50% - 60%)) scale(1); }
  38%    { transform: translate(calc(-50% - 120%), calc(-50% - 100%)) scale(1); }
  50%    { transform: translate(calc(-50% - 120%), calc(-50% - 100%)) scale(1); }
  55%    { transform: translate(calc(-50% + 70%), calc(-50% - 130%)) scale(1); }
  67%    { transform: translate(calc(-50% + 70%), calc(-50% - 130%)) scale(1); }
  72%    { transform: translate(-50%, -50%) scale(1); }
  82%    { transform: translate(-50%, -50%) scale(1); }
  86%    { transform: translate(-50%, -50%) scale(0); }
  100%   { transform: translate(-50%, -50%) scale(0); }
}

.hotspot {
  position: absolute;
  overflow: hidden;
  border-radius: 6px;
  cursor: pointer;
  outline: none;
}
.hotspot:focus-visible {
  box-shadow: 0 0 0 3px var(--portal), 0 0 24px rgba(79,176,217,0.6);
}

/* technique: css-only glow */
.hotspot-glow {
  position: absolute; inset: -6%;
  /* Individual hotspots can override the glow tint via an inline
     --hotspot-color custom property (see hotspots.js `glowColor`); falls
     back to the default fire-orange. */
  background: radial-gradient(ellipse at 50% 45%, color-mix(in srgb, var(--hotspot-color, var(--fire-bright)) 55%, transparent) 0%, transparent 68%);
  opacity: 0;
  transition: opacity 0.35s ease;
  filter: blur(2px);
}
.hotspot--css:hover .hotspot-glow,
.hotspot--css:focus-visible .hotspot-glow,
.hotspot--css.tapped .hotspot-glow {
  opacity: 0.85;
  animation: glow-pulse 1.6s ease-in-out infinite;
}

/* Oval variant: an actual soft-edged ellipse (not a rectangle with a
   gradient inside it) with a wider, more gradual multi-stop fadeout so the
   edge dithers out instead of ending in a visible ring. Opt-in per hotspot
   via glowShape: "oval" in hotspots.js. */
.hotspot-glow--oval {
  border-radius: 50%;
  /* Larger radii + later fade stops than a first pass, so the oval fills
     most of the hitbox instead of reading as a small blob centered in it
     (the parent's overflow:hidden clips it to the box regardless, so a
     small ellipse just leaves visible dead space at the edges). */
  background: radial-gradient(
    ellipse 65% 62% at 50% 48%,
    color-mix(in srgb, var(--hotspot-color, var(--fire-bright)) 95%, transparent) 0%,
    color-mix(in srgb, var(--hotspot-color, var(--fire-bright)) 75%, transparent) 45%,
    color-mix(in srgb, var(--hotspot-color, var(--fire-bright)) 40%, transparent) 75%,
    transparent 94%
  );
  filter: blur(6px) brightness(1.35);
}
@keyframes glow-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 0.75; }
}

/* Effect 1: hue-drift -- the color itself subtly cycles during the pulse
   instead of staying a flat static hue. Opt-in via glowEffect containing
   "hue". Replaces the plain opacity-only glow-pulse animation with one that
   also animates filter's hue-rotate in step. */
.hotspot-glow--hue {
  animation-name: glow-pulse-hue !important;
}
@keyframes glow-pulse-hue {
  0%, 100% { opacity: 0.4; filter: blur(6px) brightness(1.2) hue-rotate(0deg); }
  50% { opacity: 0.75; filter: blur(6px) brightness(1.3) hue-rotate(20deg); }
}

/* Effect 2: layered core+halo -- an inner, tighter, brighter "core" pulses
   on its own faster cycle with a phase offset from the outer halo, instead
   of one flat blob scaling uniformly. Opt-in via glowEffect containing
   "layered". */
.hotspot-glow--layered {
  position: absolute; /* re-assert: establishes containing block for ::after */
}
.hotspot-glow--layered::after {
  content: "";
  position: absolute;
  inset: 22%;
  border-radius: 50%;
  background: radial-gradient(
    ellipse 60% 58% at 50% 50%,
    color-mix(in srgb, var(--hotspot-color, var(--fire-bright)) 100%, transparent) 0%,
    color-mix(in srgb, var(--hotspot-color, var(--fire-bright)) 55%, transparent) 55%,
    transparent 85%
  );
  filter: blur(3px) brightness(1.6);
  opacity: 0;
  animation: glow-pulse-core 1.1s ease-in-out infinite;
  animation-delay: 0.35s; /* offset from the halo's own phase */
}
.hotspot--css:hover .hotspot-glow--layered::after,
.hotspot--css:focus-visible .hotspot-glow--layered::after,
.hotspot--css.tapped .hotspot-glow--layered::after {
  opacity: 0.7;
}
@keyframes glow-pulse-core {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 0.7; }
}

/* technique: clipped duplicate raster layer */
.hotspot-clip {
  position: absolute; inset: 0;
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.3s ease;
}
.hotspot--clipped-raster:hover .hotspot-clip,
.hotspot--clipped-raster:focus-visible .hotspot-clip,
.hotspot--clipped-raster.tapped .hotspot-clip {
  opacity: 1;
}
.hotspot-clip img {
  position: absolute;
  max-width: none;
  filter: brightness(1) saturate(1);
  animation: portal-shimmer 1.8s ease-in-out infinite;
}
@keyframes portal-shimmer {
  0%, 100% { filter: brightness(1.15) saturate(1.25) hue-rotate(0deg); transform: scale(1); }
  50% { filter: brightness(1.55) saturate(1.6) hue-rotate(8deg); transform: scale(1.03); }
}

/* clipped-raster variant: "subtle" -- much gentler filter pulse, no scale
   or hue swing, and a lower peak opacity, for small/irregular POI objects
   where the original portal-shimmer read as too aggressive. Opt-in via
   hotspots.js `clipVariant: "subtle"`. */
.hotspot-clip--subtle.hotspot-clip {
  opacity: 0;
}
.hotspot--clipped-raster:hover .hotspot-clip--subtle,
.hotspot--clipped-raster:focus-visible .hotspot-clip--subtle,
.hotspot--clipped-raster.tapped .hotspot-clip--subtle {
  opacity: 0.65;
}
.hotspot-clip--subtle img {
  animation-name: subtle-shimmer;
  animation-duration: 2.6s;
}
@keyframes subtle-shimmer {
  0%, 100% { filter: brightness(1.01) saturate(1.02); }
  50% { filter: brightness(1.06) saturate(1.08); }
}

/* POI demo variant: sheen sweep -- a soft diagonal light band travels once
   across the hitbox per pulse cycle, instead of a pulsing blob. Opt-in via
   glowEffect: "sheen". */
.hotspot-glow--sheen {
  border-radius: 50%;
  /* A single narrow band relative to a much larger background-size, so
     only one sweep is ever visible at once regardless of the hitbox's
     aspect ratio (a narrower/taller box was showing two bands at once,
     reading as a checkerboard flicker instead of one clean sweep). */
  background: linear-gradient(
    115deg,
    transparent 46%,
    color-mix(in srgb, var(--hotspot-color, var(--fire-bright)) 70%, white 25%) 50%,
    transparent 54%
  );
  background-size: 500% 500%;
  filter: blur(3px);
  animation-name: sheen-sweep !important;
  animation-duration: 1.8s !important;
}
@keyframes sheen-sweep {
  0% { background-position: -120% -120%; opacity: 0; }
  15% { opacity: 0.85; }
  55% { background-position: 90% 90%; opacity: 0.85; }
  75% { opacity: 0; }
  100% { background-position: 90% 90%; opacity: 0; }
}

/* POI demo variant: spotlight -- a soft white highlight blended onto the
   existing pixels with mix-blend-mode, reading as "a light found this
   object" rather than "a colored light source appeared on top of it".
   Opt-in via glowEffect: "spotlight". */
.hotspot-glow--spotlight {
  border-radius: 50%;
  background: radial-gradient(
    ellipse 60% 58% at 50% 48%,
    rgba(255, 255, 255, 1) 0%,
    rgba(255, 255, 255, 0.55) 45%,
    transparent 80%
  );
  mix-blend-mode: overlay;
  filter: blur(4px) brightness(1.6);
  animation-name: spotlight-pulse !important;
  animation-duration: 2s !important;
}
@keyframes spotlight-pulse {
  0%, 100% { opacity: 0.6; }
  50% { opacity: 1; }
}

/* Portal variant: wave-rise -- a single brightness band, tinted with the
   portal's own glow color (same var(--hotspot-color) used everywhere
   else), that travels bottom-to-top through the arch in a loop, clipped
   to an oval. Opt-in via glowEffect: "wave-rise".
   Tried on a POI first with a plain white band -- overlay blend at high
   alpha crushed dark stone to a flat white bar, and toning it down with
   soft-light+low-alpha just looked washed out. Screen blend with the
   portal's actual hue (rather than white) reads as colored light adding
   on top of an already-bright, already-colorful portal arch instead of
   bleaching the pixels underneath, so it's tried here instead. */
.hotspot-glow--wave-rise {
  border-radius: 50%;
  overflow: hidden;
  background: none !important;
  /* The parent's own opacity pulse (glow-pulse, from the base .hotspot-glow
     hover rule) would fade in and out *underneath* the moving band,
     reading as a second, stuttering motion -- disabled here so the only
     motion is the band itself. */
  animation-name: none !important;
  /* Optional exclusion ovals (up to two): some portals have a character
     standing in front of the arch, and the band washing straight over
     them looks bad. Set per-hotspot via --wave-exclude-* / --wave-exclude2-*
     custom properties (hotspots.js `waveExclude`, an array of one or two
     boxes); zero-sized by default, so hotspots without one (or with only
     one) are unaffected by the unused slot. Applied here on the static
     container (not the moving ::after band) so the holes stay fixed in
     place while the band travels through them, instead of moving with
     the band. Mask layers XORed together punch the holes out of the
     full-coverage base layer -- inscribed ellipses (not rectangles) so
     the cutouts don't read as hard-edged boxes over rounded characters. */
  -webkit-mask-image:
    linear-gradient(#000, #000),
    radial-gradient(ellipse 50% 50% at 50% 50%, #000 99%, transparent 100%),
    radial-gradient(ellipse 50% 50% at 50% 50%, #000 99%, transparent 100%);
  -webkit-mask-size:
    100% 100%,
    var(--wave-exclude-width, 0%) var(--wave-exclude-height, 0%),
    var(--wave-exclude2-width, 0%) var(--wave-exclude2-height, 0%);
  -webkit-mask-position:
    0 0,
    var(--wave-exclude-left, 0%) var(--wave-exclude-top, 0%),
    var(--wave-exclude2-left, 0%) var(--wave-exclude2-top, 0%);
  -webkit-mask-repeat: no-repeat, no-repeat, no-repeat;
  -webkit-mask-composite: xor;
  mask-image:
    linear-gradient(#000, #000),
    radial-gradient(ellipse 50% 50% at 50% 50%, #000 99%, transparent 100%),
    radial-gradient(ellipse 50% 50% at 50% 50%, #000 99%, transparent 100%);
  mask-size:
    100% 100%,
    var(--wave-exclude-width, 0%) var(--wave-exclude-height, 0%),
    var(--wave-exclude2-width, 0%) var(--wave-exclude2-height, 0%);
  mask-position:
    0 0,
    var(--wave-exclude-left, 0%) var(--wave-exclude-top, 0%),
    var(--wave-exclude2-left, 0%) var(--wave-exclude2-top, 0%);
  mask-repeat: no-repeat, no-repeat, no-repeat;
  mask-composite: exclude;
}
.hotspot-glow--wave-rise::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 38%;
  /* Plain white + soft-light (not screen): screen always pushes every
     channel toward white proportional to alpha, which reads as
     "desaturating toward white" no matter how low the alpha, not as a
     brightness lift. soft-light is a gentler, more luminance-preserving
     blend -- it lifts brightness while leaving more of the underlying
     hue/saturation intact. */
  background: linear-gradient(0deg, transparent 0%, rgba(255, 255, 255, 0.35) 50%, transparent 100%);
  mix-blend-mode: soft-light;
  filter: blur(4px);
  animation: wave-rise 3s linear infinite;
}
@keyframes wave-rise {
  0% { top: 100%; }
  100% { top: -38%; }
}

/* POI demo variant: outline-trace -- a thin bright arc rotates around the
   oval's border instead of filling its interior. A conic-gradient bright
   wedge, masked down to a thin ring the same aspect ratio as the hitbox.
   Opt-in via glowEffect: "outline". */
.hotspot-glow--outline {
  border-radius: 50%;
  background: conic-gradient(
    from 0deg,
    transparent 0%,
    color-mix(in srgb, var(--hotspot-color, var(--fire-bright)) 95%, white 15%) 6%,
    transparent 22%,
    transparent 100%
  );
  -webkit-mask: radial-gradient(ellipse, transparent 60%, black 66%, black 80%, transparent 86%);
  mask: radial-gradient(ellipse, transparent 60%, black 66%, black 80%, transparent 86%);
  animation-name: outline-spin !important;
  animation-duration: 1.8s !important;
  animation-timing-function: linear !important;
}
@keyframes outline-spin {
  0% { transform: rotate(0deg); opacity: 0.9; }
  100% { transform: rotate(360deg); opacity: 0.9; }
}

/* technique: generated alternate-state crossfade */
.hotspot-alt {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.5s ease;
}
.hotspot--generated-alt:hover .hotspot-alt,
.hotspot--generated-alt:focus-visible .hotspot-alt,
.hotspot--generated-alt.tapped .hotspot-alt {
  opacity: 1;
  animation: surge-flicker 1.4s ease-in-out infinite;
}
@keyframes surge-flicker {
  0%, 100% { opacity: 0.9; }
  45% { opacity: 1; }
  55% { opacity: 0.82; }
}

@media (min-width: 1800px) {
  main.wide { padding-left: 2.5rem; padding-right: 2.5rem; }
}

/* In-page content panel (Gallery/Chronicles/Expedition/Contribute etc.) --
   see hero-panel.js. A full-viewport scrollable overlay above the hero
   scene; its content is the same head-less <main> markup those pages
   render standalone, just fetched and injected instead of navigated to. */
.site-panel {
  position: fixed;
  inset: 0;
  z-index: 90;
  overflow-y: auto;
  background: rgba(8, 7, 5, 0.94);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.site-panel.open {
  opacity: 1;
  pointer-events: auto;
}
.site-panel-inner {
  min-height: 100%;
}
/* Sticky and width-matched to the content column (not the raw viewport
   edge) -- on an ultra-wide monitor, a corner-pinned close button ends up
   far from centered content and is easy to miss entirely. This keeps it
   near what you're actually reading, and visible again any time you
   scroll, instead of only at the very top. */
.site-panel-bar {
  position: sticky;
  top: 0;
  z-index: 92;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 0.7rem;
  max-width: 1180px;
  margin: 0 auto;
  padding: 1rem 1.25rem 0;
  pointer-events: none;
}
.site-panel-bar > * {
  pointer-events: auto;
}
.site-panel-close {
  display: flex;
  align-items: center;
  gap: 0.4em;
  background: rgba(13, 15, 14, 0.92);
  border: 1px solid rgba(212, 175, 120, 0.7);
  color: var(--parchment);
  font-family: inherit;
  font-size: 1rem;
  padding: 0.55em 1.1em;
  border-radius: 999px;
  cursor: pointer;
  opacity: 1;
  box-shadow: 0 4px 16px -2px rgba(0,0,0,0.9);
}
.site-panel-close:hover { background: rgba(13, 15, 14, 1); border-color: rgba(212, 175, 120, 1); }

/* Gallery lightbox: first click on a tile enlarges the image in-page,
   second click on the enlarged image requests real browser fullscreen --
   see app.js. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: rgba(8, 7, 5, 0.92);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.lightbox.open {
  opacity: 1;
  pointer-events: auto;
}
.lightbox-img {
  max-width: min(92vw, 1400px);
  max-height: 84vh;
  object-fit: contain;
  border-radius: 6px;
  box-shadow: 0 20px 60px -10px rgba(0, 0, 0, 0.8);
  cursor: zoom-in;
}
.lightbox-img:fullscreen {
  max-width: 100vw;
  max-height: 100vh;
  object-fit: contain;
  background: #000;
}
.lightbox-detail-link {
  margin-top: 1.2rem;
  color: var(--parchment);
  opacity: 0.85;
  font-size: 0.95rem;
  text-decoration: none;
  border-bottom: 1px solid rgba(212, 175, 120, 0.4);
}
.lightbox-detail-link:hover { opacity: 1; }
.lightbox-close {
  position: fixed;
  top: 1.2rem;
  right: 1.3rem;
  z-index: 201;
  background: rgba(13, 15, 14, 0.55);
  border: 1px solid rgba(212, 175, 120, 0.4);
  color: var(--parchment);
  width: 2.4rem;
  height: 2.4rem;
  border-radius: 50%;
  font-size: 1.1rem;
  cursor: pointer;
  opacity: 0.85;
}
.lightbox-close:hover { opacity: 1; }
.lightbox-nav {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  z-index: 201;
  background: rgba(13, 15, 14, 0.55);
  border: 1px solid rgba(212, 175, 120, 0.4);
  color: var(--parchment);
  width: 2.8rem;
  height: 2.8rem;
  border-radius: 50%;
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
  opacity: 0.85;
}
.lightbox-nav:hover { opacity: 1; }
.lightbox-nav[hidden] { display: none; }
.lightbox-prev { left: 1.3rem; }
.lightbox-next { right: 1.3rem; }
@media (max-width: 640px) {
  .lightbox-nav { width: 2.2rem; height: 2.2rem; font-size: 1.3rem; }
  .lightbox-prev { left: 0.5rem; }
  .lightbox-next { right: 0.5rem; }
}

/* --- Admin: music playlist drag-and-drop UI (admin-music.ejs / admin-music.js) --- */
.music-admin .panel { max-width: 1600px; margin-left: auto; margin-right: auto; }
.music-admin-header { max-width: 1600px; margin: 0 auto; text-align: center; }
.group-picker-row { display: flex; align-items: center; gap: 0.5rem; }
.group-picker-row select { flex: 1 1 auto; }
.music-admin-default-note { margin: 0; font-weight: 700; font-size: 1.05rem; text-align: center; }
.group-picker-row .track-row-menu-btn {
  flex: 0 0 auto;
  border: 1px solid var(--stone-600);
  border-radius: 8px;
  padding: 0.45em 0.6em;
}
.music-admin-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1.5rem; margin-top: 1rem; }
@media (max-width: 900px) { .music-admin-grid { grid-template-columns: minmax(0, 1fr); } }
.track-list { list-style: none; margin: 0; padding: 0.4rem; min-height: 3rem; max-height: 65vh; overflow-y: auto; border: 1px dashed var(--stone-600); border-radius: 10px; }
.track-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 0.45em 0.6em;
  margin-bottom: 0.3em;
  background: var(--stone-700);
  border: 1px solid var(--stone-600);
  border-radius: 8px;
  cursor: grab;
  font-size: 0.88rem;
}
.track-row.dragging { opacity: 0.35; }
.track-row-handle { color: var(--ink-dim); flex: 0 0 auto; }
.track-row-title { flex: 1 1 auto; min-width: 6em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.track-row-menu-btn,
.track-row-remove {
  flex: 0 0 auto;
  background: transparent;
  border: none;
  color: var(--ink-dim);
  cursor: pointer;
  font-size: 0.9rem;
  line-height: 1;
  padding: 0.2em 0.45em;
}
.track-row-menu-btn:hover { color: var(--fire-bright); }
.track-row-remove:hover { color: var(--danger); }
.track-badge {
  flex: 0 0 auto;
  font-size: 0.7rem;
  color: var(--ink-dim);
  border: 1px solid var(--stone-600);
  border-radius: 999px;
  padding: 0.1em 0.6em;
  white-space: nowrap;
}
.track-badge--unassigned { color: var(--fire-bright); border-color: rgba(224,120,47,0.4); }
.track-row--shuffle {
  cursor: default;
  background: transparent;
  border-style: dashed;
  color: var(--ink-dim);
  font-style: italic;
  letter-spacing: 0.03em;
}
.track-row--shuffle .track-row-handle { color: var(--fire-bright); }
.context-menu {
  position: absolute;
  z-index: 50;
  background: var(--stone-700);
  border: 1px solid var(--stone-600);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.4);
  display: flex;
  flex-direction: column;
  min-width: 140px;
  overflow: hidden;
}
/* `.context-menu`'s own `display: flex` is an author rule, so it beats the
   browser's default `[hidden] { display: none }` UA rule regardless of
   source order (author origin always wins over UA origin) -- setting
   `menu.hidden = true` in JS was toggling the attribute correctly but
   never actually hiding the element. This attribute-selector override
   has higher specificity than the bare class rule above, so it wins. */
.context-menu[hidden] { display: none; }
.context-menu button {
  background: transparent;
  border: none;
  color: var(--ink);
  text-align: left;
  padding: 0.6em 0.9em;
  font-size: 0.85rem;
  cursor: pointer;
}
.context-menu button:hover { background: rgba(224,120,47,0.15); color: var(--fire-bright); }

.music-shuffle-badge {
  font-size: 0.68rem;
  color: var(--fire-bright);
  border: 1px solid rgba(224,120,47,0.4);
  border-radius: 999px;
  padding: 0.1em 0.6em;
  margin-left: 0.4em;
  white-space: nowrap;
}
