: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;
  animation: landing-crossfade 21s 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: -7s; }
.landing-img:nth-of-type(3) { animation-delay: -14s; }
@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);
}

main { max-width: 1180px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }
.page-narrow { max-width: 820px; }
main.wide { max-width: 2200px; }

footer.site-footer {
  border-top: 1px solid var(--stone-600);
  padding: 2rem 1.25rem;
  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; }

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; }

/* ---------- 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: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 .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; }
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 0.9rem; }
.tile { 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; }

/* ---------- 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); }
.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); }

@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%;
}
.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; }
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; }

/* 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. */
.hero-brand-wrap {
  position: fixed;
  top: 1rem;
  left: 1.1rem;
  z-index: 20;
}
.hero-brand {
  background: none;
  border: none;
  padding: 0.2em 0.3em;
  font-size: 1.6rem;
  line-height: 1;
  color: var(--parchment);
  text-shadow: 0 0 10px rgba(0,0,0,0.9), 0 0 22px rgba(0,0,0,0.7);
  opacity: 0.85;
  cursor: pointer;
  transition: opacity 0.2s ease;
}
.hero-brand:hover,
.hero-brand[aria-expanded="true"] { opacity: 1; }

.hero-brand-menu {
  display: none;
  flex-direction: column;
  position: absolute;
  top: calc(100% + 0.4rem);
  left: 0;
  min-width: 190px;
  padding: 0.5rem;
  border-radius: 10px;
  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 a {
  padding: 0.5em 0.7em;
  border-radius: 6px;
  color: var(--parchment);
  font-size: 0.95rem;
}
.hero-brand-menu a:hover { background: rgba(212, 175, 120, 0.15); text-decoration: none; }

.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);
}

.music-bar {
  position: fixed;
  left: 50%;
  bottom: 1.4rem;
  transform: translateX(-50%);
  z-index: 20;
  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);
}
.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-title {
  max-width: 16rem;
  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;
  padding-left: 0.2rem;
  border-left: 1px solid rgba(212, 175, 120, 0.35);
}
@media (max-width: 640px) {
  .music-track-title { max-width: 8rem; }
}

.music-volume {
  width: 5.5rem;
  accent-color: var(--fire-bright);
  cursor: pointer;
}
@media (max-width: 640px) {
  .music-volume { width: 3.5rem; }
}
.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;
}
@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; }
}
