/* Guncrypt public-site — dark crypt / wild-west tone.
   Final taste pass lands in Task 12 (frontend-design skill). */

@font-face {
  font-family: 'ChakraPetch';
  src: url('../assets/fonts/ChakraPetch-Medium.ttf') format('truetype');
  font-weight: 500;
  font-display: swap;
}
@font-face {
  font-family: 'VT323';
  src: url('../assets/fonts/VT323-Regular.ttf') format('truetype');
  font-display: swap;
}
@font-face {
  font-family: 'PressStart2P';
  src: url('../assets/fonts/PressStart2P-Regular.ttf') format('truetype');
  font-display: swap;
}

:root {
  --bg: #0d0a08;
  --panel: #181311;
  --panel-2: #221a16;
  --ink: #f0e6d2;
  --ink-dim: #b09a78;
  --ink-faint: #6f5d47;
  --line: #2c211a;
  --line-strong: #4a3825;
  --accent: #d49a3b;
  --accent-strong: #f4b950;
  --danger: #c54a30;
  --good: #4fa863;
}

* { box-sizing: border-box; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'ChakraPetch', system-ui, sans-serif;
  line-height: 1.5;
}

/* ── sticky top brand bar with Wishlist CTA ── */
.topbar {
  position: sticky; top: 0; z-index: 100;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0.5rem 1rem;
  background: linear-gradient(180deg, #0d0a08 0%, #110d09 100%);
  border-bottom: 1px solid var(--line-strong);
  gap: 1rem;
}
.brand {
  display: inline-flex; flex-direction: column; gap: 2px;
  text-decoration: none; color: var(--ink);
  min-width: 0;
}
.brand-logo {
  display: block;
  height: 32px;
  width: auto;
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
}
.brand-mark {
  font-family: 'PressStart2P', monospace;
  font-size: 1.1rem;
  color: var(--accent);
  letter-spacing: 0.04em;
}
.brand-tag {
  font-size: 0.7rem;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* ── Dialogue hero — the "share your run" landing for friends ──
   Two chat bubbles riff on the trailer's closing beat: someone died,
   their friend has no idea what Guncrypt is. The build is the artifact;
   the game pitch is the answer below it. ── */
.dialogue-hero {
  position: relative;
  padding: 2.4rem 1rem 1.6rem;
  /* Round-3.7 (2026-05-13): bottom of the hero now fades into the
     hearth via an alpha-mask on the video + shade (below). The hero
     container itself gets the dirt-tile dominant color as its bg in
     the bottom region so the masked-transparent zone reveals warm
     tan (matching the hearth's dirt tile that follows). */
  background: linear-gradient(
    180deg,
    transparent 0%,
    transparent 78%,
    rgba(216, 181, 114, 0.75) 92%,
    rgba(216, 181, 114, 1)    100%
  );
  overflow: hidden;
  isolation: isolate;
  min-height: 480px;
  display: flex;
  align-items: center;
}
.dialogue-bg-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: -2;
  image-rendering: pixelated;
  /* Alpha mask: bottom 40 % of the video fades to transparent so it
     dissolves into the hero's dirt-tan bg (and thereby into the
     hearth's dirt tile below). */
  -webkit-mask-image: linear-gradient(180deg, #000 80%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 80%, transparent 100%);
}
.dialogue-bg-shade {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(180deg, rgba(13,10,8,0.1) 0%, rgba(13,10,8,0.22) 60%, rgba(13,10,8,0.28) 78%, rgba(13,10,8,0) 100%);
  /* Same mask as the video so the dark overlay also fades out at
     the bottom — otherwise it would re-darken the area we just made
     transparent. */
  -webkit-mask-image: linear-gradient(180deg, #000 80%, transparent 100%);
          mask-image: linear-gradient(180deg, #000 80%, transparent 100%);
}
.dialogue-inner {
  position: relative;
  max-width: 880px;
  margin: 0 auto;
}
/* Three-option choice layout: Button 1 vs Button 2 with OR between them
   on row 1, Button 3 centered in row 2 directly under the OR axis.
   Forcing buttons 1 & 2 to equal width (via 1fr columns) is what makes
   OR land in the true visual center — with content-width buttons, an
   unequal pair pushes OR off the page-center axis. */
.dialogue-cta-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  grid-template-areas:
    "btn1 or   btn2"
    "btn3 btn3 btn3";
  column-gap: 1rem;
  row-gap: 0.8rem;
  align-items: center;
  max-width: 880px;
  margin: 1.4rem auto 0;
}
.dialogue-cta-row > a:nth-of-type(1) { grid-area: btn1; }
.dialogue-cta-row > a:nth-of-type(2) { grid-area: btn2; }
.dialogue-cta-row > a:nth-of-type(3) { grid-area: btn3; justify-self: center; }
.dialogue-cta-row > .dialogue-or     { grid-area: or; }
/* Selector uses BOTH classes (.cta.dialogue-cta) so it beats the
   later `.cta { background: transparent }` rule on specificity —
   otherwise the default state lost the near-black bg and only the
   :hover state showed dark. */
.cta.dialogue-cta {
  /* Round-3.8h (2026-05-14): match .bubble-friend's bg — rgba(13,10,8,0.92).
     Same warm near-black at 92 % alpha so the CTAs share the bubble's
     diegetic feel. */
  background: rgba(13, 10, 8, 0.92);
  border: 2px solid var(--accent);
  color: var(--accent);
  padding: 0.75rem 1.15rem;
  text-decoration: none;
  font-family: 'ChakraPetch', sans-serif;
  font-weight: 600;
  font-size: 0.92rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
  justify-content: center;
  border-radius: 3px;
  transition: background 120ms, color 120ms;
  /* Harder shadow so the buttons read crisp on the moving video background. */
  box-shadow:
    0 0 0 1px rgba(0,0,0,0.6),
    0 6px 0 rgba(0,0,0,0.55),
    0 8px 22px rgba(0,0,0,0.75);
}
.cta.dialogue-cta:hover { background: var(--accent); color: var(--bg); }
.cta.dialogue-cta > svg { flex-shrink: 0; }
.dialogue-or {
  font-family: 'PressStart2P', monospace;
  font-size: 1.4rem;
  color: var(--ink);
  letter-spacing: 0.08em;
  text-shadow: 2px 2px 0 rgba(0,0,0,0.7);
}
/* Below 880px the desktop "1fr OR 1fr / btn3" grid doesn't have room
   for the two CTA buttons + OR side-by-side. Switch to a vertical
   stack: btn1 / btn2 / btn3 with OR hidden (the visual "three options"
   becomes implicit from the parallel stacking). Also allow the bubble
   lines to wrap below this width so they don't overflow the viewport. */
@media (max-width: 880px) {
  .dialogue-hero .bubble-line { white-space: normal; }
  .dialogue-cta-row {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "btn1"
      "btn2"
      "btn3";
    row-gap: 0.5rem;
  }
  .dialogue-cta-row > a:nth-of-type(3) { justify-self: stretch; }
  .dialogue-cta-row > .dialogue-or { display: none; }
}
.bubble {
  display: block;
  width: fit-content;
  max-width: 720px;
  padding: 0.85rem 1.1rem 0.95rem;
  border-radius: 16px;
  margin-bottom: 0.7rem;
  position: relative;
}
.bubble-friend {
  margin-right: auto;
  background: rgba(13,10,8,0.92);
  border-bottom-left-radius: 4px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.75);
}
.bubble-you {
  margin-left: auto;
  background: rgba(244,185,80,0.94);
  border-bottom-right-radius: 4px;
  text-align: right;
  box-shadow: 0 8px 32px rgba(0,0,0,0.75);
}

/* iMessage-style chat tails — single filled SVG comma path (no background-color
   cutout) so they render over the video background. Path source:
   dev.to/tobs_dl/creating-chat-bubbles-with-curls-in-react-native-svg.
   ViewBox 17x21, positioned at the bubble's bottom corner with 1-2px overlap
   into the bubble to kill the Retina anti-aliasing seam. */
.bubble-tail {
  position: absolute;
  bottom: 0;
  width: 17px;
  height: 21px;
  overflow: visible;
  pointer-events: none;
  shape-rendering: geometricPrecision;
}
.tail-left  { left: -4px; }
.tail-right { right: -4px; }
.bubble-friend .bubble-tail path { fill: rgba(13,10,8,0.92); }
.bubble-you   .bubble-tail path { fill: rgba(244,185,80,0.94); }
.bubble-you .bubble-label { color: rgba(13,10,8,0.7); }
.bubble-you .bubble-line { color: var(--bg); }
.bubble-you .bubble-line em { color: var(--bg); font-style: italic; }
.bubble-label {
  display: block;
  font-size: 0.85rem;
  color: var(--ink-faint);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-bottom: 0.3rem;
}
.bubble-line {
  margin: 0;
  font-family: 'PressStart2P', monospace;
  font-size: 1.35rem;
  color: var(--ink);
  line-height: 1.35;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.bubble-line em { font-style: italic; color: inherit; }

.dialogue-answer {
  text-align: center;
  margin: 1.2rem auto 0;
  font-size: 0.95rem;
  color: var(--ink-dim);
}
.dialogue-answer strong { color: var(--accent); display: block; margin-bottom: 0.2rem; font-size: 1rem; }

@media (max-width: 600px) {
  .dialogue-hero { padding: 1.4rem 0.8rem 1rem; }
  .bubble-line { font-size: 0.95rem; white-space: normal; }
  .bubble { padding: 0.7rem 0.9rem 0.8rem; }
}

/* ── Answer divider: the "What the hell is Guncrypt?" reveal ── */
.answer-divider {
  text-align: center;
  padding: 2.8rem 1rem 1.6rem;
}
.answer-divider-line {
  width: 60px;
  height: 3px;
  background: var(--accent);
  margin: 0 auto 1.2rem;
  border-radius: 2px;
}
.answer-divider-title {
  margin: 0 0 0.8rem;
  font-family: 'PressStart2P', monospace;
  font-size: 1.6rem;
  color: var(--accent);
  letter-spacing: 0.02em;
  /* Override main h2's accent-underline tick; this isn't a main h2 */
  padding: 0;
  border: none;
}
.answer-divider-title::before { content: none; }
.answer-divider-sub {
  margin: 0 auto;
  max-width: 820px;
  font-size: 1rem;
  color: var(--ink-dim);
  line-height: 1.7;
}
@media (max-width: 600px) {
  .answer-divider-title { font-size: 1rem; }
  .answer-divider-sub { font-size: 0.9rem; }
}

/* ── Key art hero: full-bleed cinematic with overlay hook ── */
.key-art {
  position: relative;
  width: 100%;
  min-height: 260px;
  /* Heavier gradient on the bottom half so the overlay text reads clearly
     against the busy painted art. Side-vignette as well. */
  background-image:
    linear-gradient(180deg, rgba(13,10,8,0.30) 0%, rgba(13,10,8,0.55) 50%, rgba(13,10,8,0.92) 100%),
    radial-gradient(ellipse at left, rgba(13,10,8,0.65) 0%, rgba(13,10,8,0.0) 50%),
    url('../assets/marketing/hero.png');
  background-size: cover, cover, cover;
  background-position: center;
  border-bottom: 1px solid var(--line-strong);
  display: flex;
  align-items: flex-end;
}
.key-art-overlay {
  width: 100%;
  max-width: 920px;
  margin: 0 auto;
  padding: 1.5rem 1rem 1.2rem;
  text-align: left;
}
.key-art-studio {
  display: inline-block;
  margin: 0 0 0.6rem;
  padding: 0.2rem 0.55rem;
  font-size: 0.7rem;
  color: var(--accent);
  background: rgba(13,10,8,0.7);
  border: 1px solid rgba(212,154,59,0.4);
  border-radius: 3px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.key-art-hook {
  margin: 0 0 0.5rem;
  font-family: 'PressStart2P', monospace;
  font-size: 1.8rem;
  color: var(--ink);
  letter-spacing: 0.02em;
  /* Heavy text shadow + offset blue-black for crisp legibility on busy art */
  text-shadow:
    2px 3px 0 rgba(0,0,0,0.9),
    -1px -1px 0 rgba(0,0,0,0.6),
    0 0 12px rgba(0,0,0,0.7);
}
.key-art-sub {
  margin: 0 0 1rem;
  font-size: 1rem;
  color: var(--ink);
  max-width: 620px;
  text-shadow:
    1px 2px 0 rgba(0,0,0,0.85),
    0 0 8px rgba(0,0,0,0.6);
}
.key-art-ctas {
  display: flex; gap: 0.5rem; flex-wrap: wrap;
}
@media (max-width: 600px) {
  .key-art { min-height: 200px; }
  .key-art-hook { font-size: 1.1rem; }
  .key-art-sub { font-size: 0.9rem; }
}

/* ── Autoplaying gameplay loop directly under key art ── */
.gameplay-loop {
  position: relative;
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
  background: #000;
  overflow: hidden;
  border-bottom: 1px solid var(--line-strong);
}
.gameplay-video {
  display: block;
  width: 100%;
  max-height: 420px;
  object-fit: cover;
  height: auto;
  image-rendering: pixelated;
}
.gameplay-overlay {
  position: absolute;
  right: 0.8rem;
  bottom: 0.8rem;
  background: rgba(13,10,8,0.85);
  border: 1px solid var(--accent);
  color: var(--accent);
  padding: 0.45rem 0.85rem;
  border-radius: 3px;
  text-decoration: none;
  font-family: 'ChakraPetch', sans-serif;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.gameplay-overlay:hover { background: var(--accent); color: var(--bg); }

/* ── Auto-scrolling screenshot marquee — every shipping screenshot, in a
   smooth horizontal scroll. Track is rendered 2× and CSS animates -50% so
   the loop is seamless. Pauses on hover. ── */
.screenshot-marquee {
  overflow: hidden;
  width: 100%;
  background: linear-gradient(180deg, rgba(13,10,8,0.0) 0%, rgba(13,10,8,0.5) 100%);
  padding: 1rem 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  mask-image: linear-gradient(90deg, transparent 0%, #000 6%, #000 94%, transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 6%, #000 94%, transparent 100%);
}
.marquee-track {
  display: flex;
  gap: 0.7rem;
  width: max-content;
  animation: marquee-scroll 90s linear infinite;
}
.screenshot-marquee:hover .marquee-track { animation-play-state: paused; }
.marquee-shot {
  display: block;
  flex: 0 0 auto;
  width: 320px;
  height: 180px;
  overflow: hidden;
  border-radius: 4px;
  border: 1px solid var(--line-strong);
  transition: border-color 150ms ease, transform 150ms ease;
}
.marquee-shot:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
}
.marquee-shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  image-rendering: pixelated;
}
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
@media (max-width: 600px) {
  .marquee-shot { width: 240px; height: 135px; }
  .marquee-track { animation-duration: 60s; }
}

/* ── Gameplay screenshots carousel (legacy, unused below). ── */
.gameplay-strip {
  display: flex;
  gap: 0.6rem;
  padding: 0.8rem 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
  background: linear-gradient(180deg, rgba(13,10,8,0.0) 0%, rgba(13,10,8,0.6) 100%);
}
.gameplay-strip::-webkit-scrollbar { height: 6px; }
.gameplay-strip::-webkit-scrollbar-track { background: transparent; }
.gameplay-strip::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; }
.gameplay-shot {
  position: relative;
  display: block;
  flex: 0 0 auto;
  width: 280px;
  scroll-snap-align: start;
  overflow: hidden;
  border-radius: 4px;
  border: 1px solid var(--line-strong);
  transition: transform 150ms ease, border-color 150ms ease;
}
.gameplay-shot:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
}
.gameplay-shot img {
  display: block;
  width: 100%;
  height: 158px;            /* 16:9-ish on 280w */
  object-fit: cover;
  image-rendering: pixelated;
  transition: filter 200ms ease;
}
.gameplay-shot:hover img { filter: brightness(1.1); }
.gameplay-label {
  position: absolute;
  bottom: 0; left: 0; right: 0;
  background: linear-gradient(0deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.0) 100%);
  color: var(--ink);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 1rem 0.6rem 0.4rem;
}
@media (max-width: 600px) {
  .gameplay-shot { width: 240px; }
  .gameplay-shot img { height: 135px; }
}

/* ── Town section: gameplay's "other half" so cold arrivals don't think it's
   pure bullet hell. Side-by-side video + copy. ── */
/* Town section: video on top full-width, text + townsfolk grid below.
   Cards used to compete with the side video for width — now they get the
   whole pane. */
.town-section {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  max-width: 920px;
  margin: 1rem auto 0;
  padding: 0 1rem;
}
.town-text { display: flex; flex-direction: column; }
.town-h {
  font-family: 'ChakraPetch', sans-serif;
  font-size: 1.3rem;
  font-weight: 600;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0 0 0.5rem;
}
.town-text p {
  margin: 0 0 0.8rem;
  font-size: 1rem;
  color: var(--ink);
  line-height: 1.55;
}
.townsfolk {
  list-style: none;
  padding: 0;
  margin: 0.4rem 0 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
  width: 100%;
}
.townsfolk-item {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.4rem 0.55rem;
}
.tf-icon {
  width: 32px; height: 32px;
  display: block;
  image-rendering: pixelated;
  background: var(--panel-2);
  border: 1px solid var(--line);
  padding: 1px;
  flex-shrink: 0;
}
.tf-body { min-width: 0; flex: 1; }
.tf-name {
  font-size: 0.82rem;
  color: var(--ink);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tf-role {
  font-size: 0.7rem;
  color: var(--ink-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
@media (max-width: 600px) {
  .townsfolk { grid-template-columns: 1fr; }
}
.town-video {
  display: block;
  width: 100%;
  max-height: 280px;
  object-fit: cover;
  border-radius: 4px;
  border: 1px solid var(--line-strong);
  image-rendering: pixelated;
}
@media (max-width: 600px) {
  .town-section { gap: 0.7rem; }
  .town-h { font-size: 1.1rem; }
  .town-video { max-height: 200px; }
}

#build-viewer-wrap {
  /* Pure black up top (the sim fades onto it), easing into the body's warm
     #0d0a08 over the last 160px so the seam into the "What the hell is
     Guncrypt?" section blends instead of showing a hard step (Eli, 2026-06-08). */
  background: linear-gradient(180deg, #000 0%, #000 calc(100% - 160px), var(--bg) 100%);
  padding-top: 16px;
  padding-bottom: 64px;
}

/* Round-3.8: fade strip between the dirt-tile campfire-hearth above
   and the black #build-viewer-wrap below. A 160-px tall gradient
   that goes from the dirt-tile dominant color (rgb 216 181 114)
   into pure black. Full viewport width, sits as a sibling section
   between the two. */
.scene-to-black-fade {
  /* Round-3.8f (2026-05-13): slim 80 px strip with a smooth multi-stop
     gradient — fades dirt straight to the build-viewer-wrap's black
     bg. Compact enough to feel like a soft horizon rather than a
     dedicated section. */
  height: 80px;
  background: linear-gradient(
    180deg,
    rgba(216, 181, 114, 1)   0%,
    rgba(132,  92,  52, 1)  35%,
    rgba( 42,  28,  16, 1)  72%,
    rgba(  0,   0,   0, 1) 100%
  );
  pointer-events: none;
}

/* ── Collapsed build-viewer ──
   Round-3.8f: section is full-width with a black background; the
   contained body (.build-viewer-body) is what's constrained to
   920 px and centered. This lets the black bg extend edge-to-edge
   under the scene's faded bottom. */
.build-viewer-wrap {
  margin: 0 auto;
  padding: 0;
}
.build-viewer-body {
  max-width: 920px;
  margin: 0 auto;
  padding: 0 1rem;
}
.build-viewer-summary {
  list-style: none;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 1rem;
  background: var(--panel);
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  color: var(--accent);
  font-family: 'ChakraPetch', sans-serif;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 1rem;
  transition: background 120ms;
}
.build-viewer-summary:hover { background: var(--panel-2); }
.build-viewer-summary::-webkit-details-marker { display: none; }
.build-viewer-summary::marker { content: ''; }
.build-viewer-summary::after {
  content: '▾';
  margin-left: 0.3rem;
}
.build-viewer-wrap[open] .build-viewer-summary::after { content: '▴'; }
.build-viewer-body { padding-top: 0; }

/* ── Post-video CTA bar — wishlist anchor directly after gameplay loop ── */
.post-video-cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  max-width: 920px;
  margin: 1rem auto 0;
  padding: 0.8rem 1rem;
  background: linear-gradient(90deg, rgba(212,154,59,0.10) 0%, rgba(212,154,59,0.02) 100%);
  border: 1px solid var(--line-strong);
  border-radius: 4px;
}
.pv-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.pv-eyebrow {
  font-size: 0.78rem;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 500;
}
.pv-headline {
  font-size: 1.05rem;
  color: var(--ink);
  font-weight: 500;
}
.pv-ctas { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.cta-lg { padding: 0.7rem 1.1rem; font-size: 0.95rem; }
@media (max-width: 600px) {
  .post-video-cta { flex-direction: column; align-items: flex-start; }
  .post-video-cta .cta-lg { width: 100%; justify-content: center; }
}

/* ── Plain-English chart headline ── */
.chart-headline {
  margin: 0 0 0.7rem;
  padding: 0.6rem 0.8rem;
  background: var(--panel-2);
  border-left: 3px solid var(--accent);
  border-radius: 3px;
  color: var(--ink);
  font-size: 0.95rem;
  line-height: 1.55;
}

/* ── Build-context explainer (between gameplay and build viewer). ── */
.build-context {
  max-width: 920px;
  margin: 1.2rem auto 0;
  padding: 0 1rem;
  font-size: 0.95rem;
  color: var(--ink-dim);
  text-align: center;
  line-height: 1.55;
}
.build-context strong { color: var(--ink); }

/* ── Screenshot strip ── */
.screenshot-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0.6rem;
}
.screenshot-strip a {
  display: block;
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid var(--line);
  transition: transform 150ms ease, border-color 150ms ease;
}
.screenshot-strip a:hover {
  transform: translateY(-2px);
  border-color: var(--accent);
}
.screenshot-strip img {
  display: block;
  width: 100%;
  height: auto;
  image-rendering: pixelated;
}
.screenshot-caption {
  text-align: right;
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
  color: var(--ink-dim);
}
.screenshot-caption a { color: var(--accent); text-decoration: none; }
.screenshot-caption a:hover { text-decoration: underline; }

.sim-caption {
  text-align: center;
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
  color: var(--ink-dim);
  line-height: 1.55;
}
.sim-caption strong { color: var(--ink); }
.sim-stamp {
  display: inline-block;
  margin-top: 0.4rem;
  padding: 0.2rem 0.55rem;
  background: var(--panel-2);
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  font-family: 'VT323', monospace;
  font-size: 0.8rem;
  letter-spacing: 0.02em;
  color: var(--accent);
}

/* ── Per-slot effective-damage tooltip ──
   Hover or focus a magazine slot to see the bullet's exact damage breakdown:
   base damage + each bonus contribution + effective per-projectile total.
   Source: engine.computeEffectiveBullet(i).sources array. */
.slot { position: relative; z-index: 1; }
.slot:hover, .slot:focus-within { z-index: 1000; }
.slot-tt {
  position: absolute;
  z-index: 1001;
  left: 50%;
  top: calc(100% + 6px);
  transform: translateX(-50%) translateY(-4px);
  min-width: 260px;
  background: #0d0a08;
  border: 1px solid var(--accent);
  border-radius: 4px;
  padding: 0.55rem 0.7rem;
  box-shadow: 0 6px 20px rgba(0,0,0,0.7);
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease, transform 120ms ease;
}
.slot:hover .slot-tt,
.slot:focus-within .slot-tt {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
.tt-row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.82rem;
  padding: 0.18rem 0;
  border-bottom: 1px dashed var(--line);
}
.tt-row:last-child { border-bottom: none; }
.tt-k { color: var(--ink-dim); }
.tt-v {
  font-family: 'VT323', monospace;
  font-size: 1rem;
  color: var(--accent);
}
.tt-total { padding-top: 0.3rem; border-top: 1px solid var(--line-strong); margin-top: 0.2rem; }
.tt-total .tt-k { color: var(--ink); font-weight: 500; }
.tt-total .tt-v { color: var(--accent-strong); font-size: 1.15rem; }
.tt-meta { font-size: 0.72rem; color: var(--ink-faint); margin-top: 0.3rem; text-transform: uppercase; letter-spacing: 0.06em; }
@media (max-width: 600px) {
  /* Tooltips would overflow on narrow screens; show inline beneath the slot instead. */
  .slot-tt { display: none; }
}

.magazine-hint {
  margin: 0.7rem 0 0;
  font-size: 0.78rem;
  color: var(--ink-faint);
  text-align: center;
  font-style: italic;
}
.hint-mobile { display: none; }
@media (max-width: 600px) {
  .hint-desktop { display: none; }
  .hint-mobile { display: inline; }
}

/* Fork button — distinct icon styling */
#fork {
  background: var(--accent);
  color: var(--bg);
  border-color: var(--accent);
}
#fork:hover { background: var(--accent-strong); }
.topbar-actions {
  display: inline-flex; gap: 0.5rem; align-items: center;
}
.cta {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.45rem 0.85rem;
  border-radius: 3px;
  text-decoration: none;
  background: transparent;
  border: 1px solid var(--line-strong);
  color: var(--ink);
  font-family: 'ChakraPetch', sans-serif;
  font-weight: 500;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  min-height: 36px;
}
.cta:hover { background: var(--panel-2); border-color: var(--accent); color: var(--accent); }
.cta.primary {
  background: var(--accent);
  color: var(--bg);
  border-color: var(--accent);
}
.cta.primary:hover { background: var(--accent-strong); color: var(--bg); }

.badges { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.8rem; }
.badge {
  background: var(--panel);
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  padding: 0.25rem 0.6rem;
  font-size: 0.875rem;
  color: var(--ink);
}

#damage-chart-section {
  margin-top: 1.2rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.8rem;
}
#damage-chart {
  display: block;
  width: 100%;
  height: auto;
  max-height: 340px;
}
.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem 1.4rem;
  margin-top: 0.7rem;
  font-size: 0.875rem;
  color: var(--ink-dim);
}
.legend-key {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.legend-swatch {
  display: inline-block;
  width: 14px; height: 14px;
  border-radius: 2px;
}
.legend-value {
  font-family: 'VT323', monospace;
  font-size: 1.15rem;
  color: var(--accent);
  letter-spacing: 0.02em;
}
.legend-value.base { color: var(--accent); }
.legend-value.elemental { color: #6dc97b; }
.legend-value.total { color: var(--ink); }
.legend-value.per-reload { color: var(--ink); }
.legend-label {
  text-transform: uppercase;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
}

main { padding: 1rem; max-width: 920px; margin: 0 auto; }
main section { margin-bottom: 1.8rem; }
/* The answer flow needs visual breathing room between major beats. */
#content-tail section { margin-bottom: 2.2rem; }
#content-tail section.answer-divider { margin-top: 1rem; margin-bottom: 1.2rem; }
#content-tail section.screenshot-marquee { margin-bottom: 1.5rem; }
#content-tail section.key-art { margin-bottom: 1.5rem; }
#content-tail section#about-guncrypt { margin-bottom: 1.6rem; }

/* Empty-mode flow: no shared run → hide the dialogue + show the answer up
   front. The page becomes a "what is Guncrypt + paste your link" landing. */
body.mode-empty-flow .dialogue-hero,
body.mode-empty-flow .campfire-hearth,
body.mode-empty-flow .build-viewer-wrap { display: none; }
body.mode-empty-flow #content-tail { padding-top: 1rem; }
main h2 {
  font-family: 'ChakraPetch', sans-serif;
  font-weight: 600;
  font-size: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent);
  margin: 0 0 0.8rem;
  padding: 0 0 0.5rem;
  position: relative;
  border-bottom: 1px solid var(--line-strong);
}
main h2::before {
  content: '';
  position: absolute;
  left: 0; bottom: -1px;
  width: 40px; height: 2px;
  background: var(--accent);
}

#sim-stage {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.8rem;
  position: relative;
  z-index: 1;
}
#sim-canvas {
  display: block;
  width: 100%;
  max-width: 640px;
  height: auto;
  margin: 0 auto;
  image-rendering: pixelated;
  background: #1a1410;
}

#synergy-list {
  padding-left: 1.2rem;
  margin: 0;
}
#synergy-list li {
  margin: 0.4rem 0;
  color: var(--ink);
}

#slot-list {
  list-style: none; padding: 0; margin: 0;
  display: grid;
  gap: 0.5rem;
  grid-template-columns: 1fr;
}
.slot {
  display: flex;
  gap: 0.8rem;
  align-items: center;
  background: linear-gradient(90deg, var(--panel) 0%, var(--panel-2) 100%);
  border: 1px solid var(--line);
  border-left: 3px solid var(--line-strong);
  border-radius: 4px;
  padding: 0.7rem 0.9rem;
  transition: border-color 150ms ease, transform 150ms ease;
}
.slot:hover {
  border-left-color: var(--accent);
  transform: translateX(2px);
}
.slot img {
  width: 44px; height: 44px;
  image-rendering: pixelated;
  background: var(--panel-2);
  border: 1px solid var(--line-strong);
  padding: 2px;
  flex-shrink: 0;
}
.slot .role {
  color: var(--accent);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 500;
  margin-top: 2px;
}
.slot .slot-name { font-weight: 600; color: var(--ink); }
.slot .slot-desc {
  font-size: 0.85rem;
  color: var(--ink-dim);
  line-height: 1.4;
  margin-top: 0.25rem;
}
.slot .slot-idx {
  color: var(--accent);
  font-family: 'VT323', monospace;
  font-size: 1.6rem;
  min-width: 1.8rem;
  text-align: center;
  font-weight: 700;
}

button {
  background: var(--panel);
  color: var(--ink);
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  padding: 0.65rem 1.1rem;
  font-family: 'ChakraPetch', sans-serif;
  font-weight: 500;
  font-size: 0.9rem;
  cursor: pointer;
  min-height: 44px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  transition: border-color 120ms ease, background 120ms ease, color 120ms ease;
}
button:hover { border-color: var(--accent); color: var(--accent); }
button[disabled] { opacity: 0.4; cursor: not-allowed; }
button.primary {
  background: var(--accent);
  color: var(--bg);
  border-color: var(--accent);
}
button.primary:hover { background: var(--accent-strong); color: var(--bg); border-color: var(--accent-strong); }

#share {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 0.6rem;
  margin: 1rem 0;
  align-items: stretch;
}
#share button { width: 100%; padding: 0.7rem 0.6rem; font-size: 0.82rem; }
#share-msg {
  grid-column: 1 / -1;
  color: var(--good);
  text-align: center;
  min-height: 1.2em;
}
button.primary {
  background: var(--accent);
  color: var(--bg);
  border: none;
}
button.primary:hover { background: var(--accent-strong); }
@media (max-width: 900px) {
  #share { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 480px) {
  #share { grid-template-columns: 1fr; }
}

details {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.8rem;
}
details summary { cursor: pointer; color: var(--accent); font-weight: 500; }
.detail-block {
  padding: 0.5rem 0;
  border-bottom: 1px dashed var(--line);
}
.detail-block:last-child { border-bottom: none; }
.detail-block strong { color: var(--accent); }
.detail-block ul {
  margin: 0.3rem 0 0 1.2rem;
  padding: 0;
}
.detail-block li {
  color: var(--ink-dim);
  font-size: 0.875rem;
}

/* ── relics section ── */
#relic-list {
  list-style: none; padding: 0; margin: 0;
  display: grid;
  gap: 0.5rem;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.relic {
  display: flex;
  gap: 0.7rem;
  align-items: flex-start;
  background: linear-gradient(135deg, var(--panel) 0%, var(--panel-2) 100%);
  border: 1px solid var(--line);
  border-top: 2px solid var(--line-strong);
  border-radius: 4px;
  padding: 0.7rem 0.85rem;
  transition: border-top-color 150ms ease, transform 150ms ease;
}
.relic:hover {
  border-top-color: var(--accent);
  transform: translateY(-2px);
}
.relic img {
  width: 40px; height: 40px;
  image-rendering: pixelated;
  background: var(--panel-2);
  border: 1px solid var(--line-strong);
  padding: 2px;
  flex-shrink: 0;
}
.relic-body { flex: 1; min-width: 0; }
.relic-name {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--ink);
}
.relic-desc {
  font-size: 0.85rem;
  color: var(--ink-dim);
  line-height: 1.4;
  margin-top: 0.2rem;
}

/* ── about / pitch / CTAs ── */
#about-guncrypt {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 1rem 1.2rem;
}
.about-blurb {
  margin: 0 0 0.9rem;
  font-size: 1rem;
  color: var(--ink);
  line-height: 1.55;
}
.about-blurb strong { color: var(--accent); display: block; margin-bottom: 0.4rem; font-size: 1.1rem; }
.about-blurb em { color: var(--accent-strong); font-style: normal; }
.about-ctas { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.9rem; }
.trailer-embed {
  position: relative;
  margin-top: 0.8rem;
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid var(--line-strong);
  background: #000;
  aspect-ratio: 16 / 9;
}
.trailer-embed iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* ── Floating Wishlist badge (mobile-first, appears on scroll) ── */
.floating-wishlist {
  position: fixed;
  right: 0.8rem;
  bottom: 0.8rem;
  z-index: 200;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.7rem 1rem;
  background: var(--accent);
  color: var(--bg);
  border-radius: 28px;
  font-family: 'ChakraPetch', sans-serif;
  font-weight: 600;
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  text-decoration: none;
  box-shadow: 0 6px 22px rgba(0,0,0,0.6), 0 0 0 1px rgba(244,185,80,0.4);
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 250ms ease, transform 250ms ease, background 150ms ease;
  pointer-events: none;
}
.floating-wishlist.visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}
.floating-wishlist:hover { background: var(--accent-strong); }
@media (max-width: 600px) {
  .floating-wishlist span { display: none; }
  .floating-wishlist { padding: 0.85rem; border-radius: 28px; }
  .floating-wishlist::after { content: 'Wishlist'; font-size: 0.8rem; }
}

/* ── footer ── */
.site-footer {
  border-top: 1px solid var(--line);
  padding: 1.2rem 1rem 1.5rem;
  margin-top: 1.5rem;
  text-align: center;
  color: var(--ink-faint);
  font-size: 0.85rem;
}
.footer-row {
  margin-bottom: 0.4rem;
  display: flex; flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: center;
  align-items: center;
}
.footer-brand {
  font-family: 'PressStart2P', monospace;
  font-size: 0.85rem;
  color: var(--accent);
  letter-spacing: 0.04em;
}
.footer-sub { color: var(--ink-faint); }
.site-footer a {
  color: var(--ink-dim);
  text-decoration: none;
}
.site-footer a:hover { color: var(--accent); text-decoration: underline; }
.footer-dot { color: var(--line-strong); }

@media (max-width: 600px) {
  .hero h1 { font-size: 1.05rem; }
  main h2 { font-size: 0.8rem; }
  /* Legend on mobile reflows to 2 columns so the chart breathes. */
  .legend {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.4rem 0.7rem;
    font-size: 0.85rem;
  }
  .legend-value { font-size: 1.05rem; }
  /* Chart needs more vertical room on mobile to remain legible. */
  #damage-chart { min-height: 240px; }
  .chart-headline { font-size: 0.9rem; line-height: 1.5; }
}

/* ── Dialogue hero intro animation ──────────────────────────────────
   The .js-anim class is added to <html> by an inline <script> in <head>
   so the hidden initial state applies before the page paints. CSS
   defaults are the END state, so JS-disabled / prefers-reduced-motion
   users never see the empty stage. JS in js/dialogue_intro.js drives
   the reveal sequence via the Web Animations API + async/await.
*/
.js-anim .dialogue-hero .bubble {
  opacity: 0;
  transform: translateY(80px);
}
.js-anim .dialogue-hero .bubble-line .ch {
  opacity: 0;
  white-space: pre;
}
/* Wave emphasis needs to transform individual chars, which requires
   inline-block. Other .ch stay as inline so natural text wrapping (at
   spaces only, not between any two atomic inlines) keeps punctuation
   like ." from orphaning onto its own line at narrow viewports. */
.js-anim .dialogue-hero .emph-ripple > .ch {
  display: inline-block;
}
.js-anim .dialogue-hero .bubble-line .ch.is-revealed {
  opacity: 1;
  transition: opacity 30ms linear;
}
.js-anim .dialogue-hero .dialogue-cta-row {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .js-anim .dialogue-hero .bubble,
  .js-anim .dialogue-hero .dialogue-cta-row {
    opacity: 1;
    transform: none;
  }
  .js-anim .dialogue-hero .bubble-line .ch { opacity: 1; }
}

/* RPG-style emphasis on specific words inside the typed bubble lines.
   Jitter: a quick 420ms shake on "died" (a la JRPG damage-flash).
   Wave:   each char of "Guncrypt" bobs up and back down with a staggered
           delay, driven by the --i variable set on each .ch in JS.        */
.emph-jitter { display: inline-block; }
@keyframes dialogue-jitter {
  0%   { transform: translate(0, 0) rotate(0deg); }
  10%  { transform: translate(-2px, -1px) rotate(-2deg); }
  25%  { transform: translate(2px, 1px) rotate(2deg); }
  40%  { transform: translate(-2px, 1px) rotate(-2deg); }
  55%  { transform: translate(2px, -1px) rotate(2deg); }
  70%  { transform: translate(-1px, 0) rotate(-1deg); }
  85%  { transform: translate(1px, 0) rotate(0deg); }
  100% { transform: translate(0, 0) rotate(0deg); }
}
.emph-jitter.is-active { animation: dialogue-jitter 420ms ease-in-out; }

.emph-ripple { display: inline-block; }
@keyframes dialogue-wave {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-5px); }
}
.emph-ripple.is-active > .ch {
  animation: dialogue-wave 360ms ease-in-out;
  animation-delay: calc(var(--i, 0) * 50ms);
}

/* ────────────────────────────────────────────────────────────────────
   CAMPFIRE HEARTH
   The death-recap page's focal area: a top-down reproduction of the
   in-game CampfireRoom restaged as a Western wake. The hearth wraps
   #build-viewer-wrap; inside, .campfire-scene is the 816×816 focal
   composition. Lower content sections continue on a dirt-floor
   continuation underneath.

   Coord system inside .campfire-scene:
     1 Unity tile = 68 px (true 4× scaling from 17 px source sprites)
     12×12 tile grid = 816×816 px container
     web_x = 408 + unity_x × 68
     web_y = 757 - (unity_y + 1.66) × 68
   ──────────────────────────────────────────────────────────────────── */
.campfire-hearth {
  position: relative;
  isolation: isolate;
  /* Clip horizontal overflow so the tumbleweed/dust clouds drifting off the
     edges (and the scaled scene) never add page-level horizontal scroll. */
  overflow-x: clip;
  /* Round-2 H2: 6× scale of 17px source dirt strip (1224 wide, 408 tall).
     Round-3.2 (2026-05-13): padding-top dropped from 32 → 0 so the
     campfire-scene's top edge sits flush against the dialogue-hero's
     bottom edge. Lets tall back-row props (cart, joshua-tree) "meet"
     the hero seam per Eli's request. */
  background-image: url('../assets/campfire/ground-strip-1224x408.png');
  background-repeat: repeat;
  background-size: 1224px 408px;
  image-rendering: pixelated;
  padding-top: 0;
  /* Round-3.8 (2026-05-13): padding-bottom dropped from 64 → 0 so the
     scene-to-black-fade strip starts immediately at the scene's
     bottom edge (right where the front-row cacti sit) instead of
     leaving an empty dirt band beneath. */
  padding-bottom: 0;
}

/* Scene-wide vignette + candle-flicker. Round-3 (2026-05-13):
   - Moved from .campfire-scene to .campfire-hearth so the gradient
     extends to the FULL VIEWPORT WIDTH, not clipped to the 1224 scene
     box. This means the dialogue-hero video's bottom-darkening blends
     seamlessly with the top of the hearth (both ~75% dark).
   - Made the gradient WIDER (55% × 50% ellipse instead of 38% × 32%)
     so more of the scene is in the brighter zone — Eli's note:
     "make the gradient wider so things are more visible but still
     have that vignette effect."
   - Animated steps(4, end) over 0.6 s — same cadence as the candle
     flame's 4-frame cycle — so the ambient light "responds" to the
     candle's flicker rather than reading as a static frame.

   The element is fixed-height (1280 px = scene + paddings) so the
   vignette stops at the bottom of the scene area, NOT covering the
   content sections below (which already have their own dark
   rgba(20,16,14,0.85) panel backgrounds). */
/* Round-3.7 (2026-05-13): vignette + candle pool both DROPPED per Eli —
   they were reading dull and busy. The seam-blend job is now handled
   by mask-image on the dialogue-hero video (see .dialogue-bg-video):
   the video fades to alpha-transparent at its bottom, dissolving
   directly into the hearth's dirt tile rather than meeting it at a
   hard line. No more flicker, no more night-darkening overlay. */

.campfire-scene {
  position: relative;
  /* Round-3 (2026-05-13): scene compressed vertically from 1224 to 816
     per Eli — "the town scene needs to be compressed quite a bit
     vertically." Width stays at 1224 (= 12 dirt tiles × 102 px = 6×
     of 17 px source). New height = 8 tiles × 102 px = 816 px. All
     element positions scaled × 0.666 from the H2 1224-tall layout. */
  width: 1224px;
  /* Round-3.8d (2026-05-13): scene height further trimmed 714 → 639
     (top 75 px trimmed per Eli). bg-image kept at 714 tall but
     positioned at y=-75 so the BOTTOM 639 of the composite shows
     (no rebake needed). All interior elements have their top values
     shifted −75 to preserve their world position.
     Round-3.8f (2026-05-13): mask-image fades the bottom of the
     scene to alpha-transparent so the black build-viewer-wrap
     beneath shows through — graceful dirt→black blend without a
     dedicated fade strip taking vertical space. */
  height: 639px;
  margin: 0 auto;
  background-image: url('../assets/campfire/ground-1224x714.png');
  background-size: 1224px 714px;
  background-position-y: -75px;
  background-position: 0 0;
  background-repeat: no-repeat;
  image-rendering: pixelated;
  overflow: hidden;
  /* Round-3 (2026-05-13): scene border + outer halo DROPPED per Eli —
     the hearth's vignette gradient now handles "this is the lit
     focal area" without any hard frame. Scene flows seamlessly into
     the dirt-strip background of the hearth and into the rest of
     the page. */
}

/* Content sections inside the build-viewer-wrap (formerly nested in
   .campfire-hearth; now a sibling on black bg per Round-3.8). Keep
   the dark-translucent panel look so tables/prose remain legible. */
#build-viewer-wrap #content > section,
#build-viewer-wrap #content > details {
  background: rgba(20, 16, 14, 0.85);
  border-radius: 6px;
  padding: 16px 18px;
  margin-bottom: 18px;
}

/* Round-3 (2026-05-13): campfire prop DROPPED. The single light source
   is now a candle on top of the memorial tombstone (.memorial-candle
   + .memorial-candle-flame rules near the memorial). The campfire-
   flame animation rule below is kept for reference but the prop is
   gone from the DOM. */

/* Candle bases — a few ivory pixels on top of the memorial tombstone.
   Memorial stone occupies scene-local y=609 to y=801 (centered at
   y=705, 192 tall). Its visible top (the curved silhouette of the
   gravestone) is at roughly scene-y=618. Three candles cluster on
   that top: a tall center candle flanked by two shorter side
   candles. Per-candle position/height set by the side modifiers. */
.campfire-scene .memorial-candle {
  position: absolute;
  width: 8px;
  transform: translate(-50%, -100%);
  background: linear-gradient(180deg, #f7ecc9 0%, #e0d4b3 60%, #b09870 100%);
  border-left: 1px solid rgba(40, 28, 18, 0.4);
  border-right: 1px solid rgba(40, 28, 18, 0.4);
  z-index: 5;
}
/* Round-3.3: candle tops shifted +29 to follow the now-halved tombstone.
   New visible-curve top of the stone is at scene-y ≈ 441 (was 412). */
/* Round-3.8d (2026-05-13): candle bottoms shifted −75 (339 → 264,
   341 → 266) to follow the scene-trim. */
.campfire-scene .memorial-candle--center { left: 612px; top: 264px; height: 22px; }
.campfire-scene .memorial-candle--left   { left: 590px; top: 266px; height: 16px; }
.campfire-scene .memorial-candle--right  { left: 634px; top: 266px; height: 18px; }

/* Candle flames — 4-frame sprites cycled from distinct flames.gif cells.
   Each source is 16×16, displayed at 4× = 64×64. Anchored so the flame
   center sits at the candle's top edge (sprite renders the visible
   flame above that line). Three different sprites + three different
   flicker timings so the candles don't read as a single shared frame. */
.campfire-scene .memorial-candle-flame {
  position: absolute;
  width: 64px;
  height: 64px;
  transform: translate(-50%, -50%);
  background-size: 256px 64px;   /* 16 × 4 frames, 4× scale */
  background-repeat: no-repeat;
  background-position-x: 0;
  image-rendering: pixelated;
  z-index: 5;  /* "rocks/trees/props" layer per Eli's z-order */
}

/* Center: the existing tall classic flame — brightest halo, scene's primary light. */
.campfire-scene .memorial-candle-flame--center {
  left: 612px;
  top: 242px;  /* 264 - 22 — Round-3.8d shift */
  background-image: url('../assets/campfire/effects/candle-flame.png');
  animation: candle-flame-flicker 0.6s steps(4, end) infinite;
  filter:
    drop-shadow(0 0 14px rgba(255, 180, 80, 0.95))
    drop-shadow(0 0 34px rgba(255, 140, 50, 0.7))
    drop-shadow(0 0 70px rgba(255, 110, 40, 0.4));
}

/* Left side: small steady flame — softer halo so the center stays dominant.
   Slightly faster cycle (0.5s) so it visibly leads its own rhythm. */
.campfire-scene .memorial-candle-flame--left {
  left: 590px;
  top: 250px;  /* 266 - 16 — Round-3.8d shift */
  background-image: url('../assets/campfire/effects/candle-flame-small.png');
  animation: candle-flame-flicker 0.5s steps(4, end) infinite;
  filter:
    drop-shadow(0 0 10px rgba(255, 180, 80, 0.75))
    drop-shadow(0 0 22px rgba(255, 140, 50, 0.5))
    drop-shadow(0 0 48px rgba(255, 110, 40, 0.28));
}

/* Right side: fuller rounded flame — slower cycle (0.72s) so the trio
   reads as three independent flickers instead of a unison flash. */
.campfire-scene .memorial-candle-flame--right {
  left: 634px;
  top: 248px;  /* 266 - 18 — Round-3.8d shift */
  background-image: url('../assets/campfire/effects/candle-flame-wide.png');
  animation: candle-flame-flicker 0.72s steps(4, end) infinite;
  filter:
    drop-shadow(0 0 12px rgba(255, 180, 80, 0.82))
    drop-shadow(0 0 28px rgba(255, 140, 50, 0.58))
    drop-shadow(0 0 58px rgba(255, 110, 40, 0.34));
}

@keyframes candle-flame-flicker {
  /* 4 frames cycled by background-position. Duration set per-candle so
     the three flames don't share a frame index. */
  from { background-position-x: 0; }
  to   { background-position-x: -256px; }
}

/* Scene-wide vignette — moved from .campfire-scene::after up to
   .campfire-hearth::after (2026-05-13 per Eli) so the gradient
   blends seamlessly from the dialogue-hero's dark video at the
   page top down through the hearth area. Now spans the FULL
   viewport width (was clipped to the 1224 scene box), giving
   atmospherics + props page-wide presence.
   See the .campfire-hearth::after rule below for the actual
   implementation. */

/* NPC seats — Round-2 H2: 32×32 source displayed at 6× = 192×192 (was 4×).
   Larger NPCs match the scaled-up 1224×1224 scene. */
.campfire-scene .npc-seat {
  position: absolute;
  width: 192px;
  height: 192px;
  transform: translate(-50%, -50%);
  z-index: 6;
}
.campfire-scene .npc-seat .npc-sprite {
  width: 192px;
  height: 192px;
  display: block;
  background-image: var(--npc-sheet);
  background-repeat: no-repeat;
  background-size: var(--npc-sheet-width) 192px;
  background-position-x: 0;
  image-rendering: pixelated;
  /* All NPCs idle at the in-game cadence: 167 ms per sprite frame
     (matches SpriteAnimatorClip m_framesPerSprite=10 × 1/60 s game
     tick = 0.167 s). animation-name, animation-duration, AND
     animation-timing-function are all set inline by campfire_scene.js
     because Safari refuses var()/calc() inside steps() and inside
     animated background-position-x. iteration-count stays here. */
  animation-iteration-count: infinite;
  animation-play-state: running;
}

.campfire-scene .npc-seat .npc-sprite.is-speaker {
  /* Round-3.8: speaker animates at the SAME 167 ms/frame cadence as
     non-speakers ("animate normally" per Eli). The warm glow filter
     is the only thing that visually distinguishes the speaker. */
  filter: drop-shadow(0 0 8px rgba(255, 180, 80, 0.6))
          drop-shadow(0 0 18px rgba(255, 140, 40, 0.35));
}

/* Round-3.8b (2026-05-13): one keyframe per distinct frame-count.
   Safari can't animate background-position-x when the endpoint
   resolves through calc(var(...)), so we hard-code the pixel
   endpoint for each NPC sheet width (frames × 192 px-per-frame).
   campfire_scene.js picks the right animation-name per NPC. */
@keyframes campfire-npc-idle-6  { from { background-position-x: 0; } to { background-position-x: -1152px; } }
@keyframes campfire-npc-idle-10 { from { background-position-x: 0; } to { background-position-x: -1920px; } }
@keyframes campfire-npc-idle-11 { from { background-position-x: 0; } to { background-position-x: -2112px; } }
@keyframes campfire-npc-idle-13 { from { background-position-x: 0; } to { background-position-x: -2496px; } }
@keyframes campfire-npc-idle-17 { from { background-position-x: 0; } to { background-position-x: -3264px; } }

/* Round-2 H2: positions scaled × 1.5 from the round-1 coords. The Unity
   spawn-point relationship is preserved (web_x = 612 + unity_x × 102 at
   6× scale; web_y = 1136 - (unity_y + 1.66) × 102). */
/* Round-3: Y values scaled × 0.666 from the H2 1224-tall layout per
   Eli's "compress quite a bit vertically" feedback. */
/* Round-3.8 (2026-05-13): seats 0/1/2 (back-row NPCs) dropped to reclaim
   vertical space. Remaining seats shifted up −102 so the memorial
   cluster sits higher in the now-shorter scene. seat-6 added as the
   right-side mirror of seat-5 ("one opposite of the master"). */
/* Round-3.8d (2026-05-13): all top values shifted −75 to match the
   scene's new 639 px height (top 75 trimmed). */
.campfire-scene .seat-3 { left: 119px;  top: 158px; }
.campfire-scene .seat-4 { left: 1118px; top: 167px; }
.campfire-scene .seat-4 .npc-sprite { transform: scaleX(-1); }
.campfire-scene .seat-5 { left: 263px;  top: 310px; }
.campfire-scene .seat-6 { left: 961px;  top: 310px; }
.campfire-scene .seat-6 .npc-sprite { transform: scaleX(-1); }
/* seat-7 (back-of-room outlier at unity y=-1.66) dropped 2026-05-13 — read as
   a stranded NPC in the lower-right corner away from the horseshoe group. */

/* Speech indicator rules dropped 2026-05-13 — the main #fable-stage is
   now itself a proper speech bubble with a tail to the speaker. The
   small indicator was redundant (Eli: "the text isn't in the bubble at
   all"). The .is-speaker warm-glow drop-shadow on the speaker sprite
   plus the bubble's tail handles "who is talking" identification. */

/* Central memorial — Round-2 H2: scaled × 1.5 to read as the focal point.
   Stone 192×192 (6× of 32 source), hat 96×72 (6× of 16/12), revolver
   48×48 (1.5× of 32 source — slightly larger than the previous 32×32
   to remain readable at the new scene scale, while still much smaller
   than the hat physically). */
.campfire-scene .memorial-stone {
  position: absolute;
  left: 612px;
  top: 293px;  /* Round-3.8d: shift −75 */
  /* Round-3.3 (2026-05-13): halved from 192×192 → 96×96 per Eli — the
     tombstone was reading as 2× bigger than the rest of the pixel-art
     scale (characters/hat/wagon all at ×6 source; stone was effectively
     ×12). Now consistent with the scene's pixel grid. */
  width: 96px;
  height: 96px;
  transform: translate(-50%, -50%);
  transform-origin: 50% 100%;
  image-rendering: pixelated;
  z-index: 5;  /* "rocks/trees/props" layer */
  filter: drop-shadow(0 3px 0 rgba(0, 0, 0, 0.55));
}
/* Round-3.5 (2026-05-13): grave mound. Top-down rectangular raised
   earth, palette sampled from the dirt-strip composite so it reads
   as the SAME ground just piled up — central ridge highlight,
   mid-tone slopes, darker rim shadow. Width matches the tombstone
   (96 px); length = 264 px (coffin proportions ≈ 2.75× wide).
   Positioned with the top edge slightly overlapping the tombstone's
   bottom (no visible gap). */
.campfire-scene .memorial-mound {
  position: absolute;
  left: 612px;
  top: 328px;  /* Round-3.8d: shift −75 */
  width: 96px;
  height: 264px;
  transform: translate(-50%, 0);  /* horizontally centered, top-anchored */
  image-rendering: pixelated;
  z-index: 4;  /* below hat/revolver/flowers so they rest on top */
  /* Soft ground drop-shadow anchors the mound to the dirt and sells
     the raised height without darkening the body. */
  filter: drop-shadow(0 4px 3px rgba(40, 24, 10, 0.45));
}

/* Hat — Round-3.5: moved up 25 px (580 → 555) so it sits at the HEAD
   of the grave mound (near the tombstone) instead of floating below
   the memorial cluster. */
.campfire-scene .memorial-hat {
  position: absolute;
  left: 612px;
  top: 378px;  /* Round-3.8d: shift −75 */
  width: 96px;   /* 16 × 6 */
  height: 72px;  /* 12 × 6 */
  transform: translate(-50%, -50%);
  image-rendering: pixelated;
  z-index: 5;
  filter: drop-shadow(0 2px 0 rgba(0, 0, 0, 0.5));
}

/* Revolver — Round-3.5: centered on the grave's long axis, rotated
   135° so it lies diagonally with the grip at the upper-right and
   the barrel pointing down-left at 45°. Reads as deliberately
   placed on the grave with the grip oriented for easy pickup. */
.campfire-scene .memorial-revolver {
  position: absolute;
  left: 612px;
  top: 448px;  /* Round-3.8d: shift −75 */
  width: 48px;
  height: 48px;
  transform: translate(-50%, -50%) rotate(45deg);
  image-rendering: pixelated;
  z-index: 5;
  filter: drop-shadow(0 2px 0 rgba(0, 0, 0, 0.5));
}


/* Round-3 prop layout (Eli-curated 2026-05-13):
   - BACK row (top of scene): shadow-free sprites — joshua-tree-noshadow,
     shrubs, sign, cart-canvas. They live at the top of the scene
     (scene-y ~30-150) without the shadow-direction problems the
     bottom-anchored shadow sprites had.
   - FRONT row (below the candle): scattered rocks + cacti + Joshua
     trees at varying scene-y 940-1218 around the memorial.
   - Both rows at z-index 5 ("rocks/trees/props" layer per Eli). */
.campfire-scene .back-prop {
  position: absolute;
  image-rendering: pixelated;
  z-index: 5;
  transform: translate(-50%, -100%);
  transform-origin: 50% 100%;
}
.campfire-scene .front-prop {
  position: absolute;
  image-rendering: pixelated;
  z-index: 5;
  transform: translate(-50%, -100%);
  transform-origin: 50% 100%;
}

/* BACK row — 7 shadow-free props spaced across the top of the scene.
   `top` is each prop's BOTTOM-y on the scene. Scaled at 3× source for
   reasonable back-row weight; tree at 2.5× so it doesn't dominate. */
/* Y values scaled × 0.666 for the new 816-tall scene. bp-1 height
   reduced from 320 → 192 (64×128 source × 1.5× instead of 2.5×) so
   the tall tree doesn't clip dramatically into the back row. */
/* Round-3.8e (2026-05-13): bushes moved BACK DOWN so they're fully
   visible in the trimmed 639 scene (Eli: "move the bushes back down
   on the top so they're not pushed out of frame"). Each top is now
   ≥ the prop's height so the top edge sits at scene-y ≥ 0. bp-5
   (generic-sign) DROPPED entirely per Eli ("get rid of the sign
   post sprite"). */
.campfire-scene .bp-1 { left: 130px;   top: 200px; width:  96px; height: 192px; } /* joshua-tree-noshadow 64×128 ×1.5 */
.campfire-scene .bp-2 { left: 320px;   top: 100px; width:  96px; height:  96px; } /* shrub 32×32 ×3 */
.campfire-scene .bp-3 { left: 440px;   top:  74px; width:  60px; height:  60px; } /* shrub-small-0 20×20 ×3 */
.campfire-scene .bp-4  { left: 530px;   top: 145px; width:  60px; height:  60px; } /* rock-small-0  20×20 ×3 */
.campfire-scene .bp-6  { left: 1080px;  top:  68px; width:  60px; height:  60px; } /* shrub-small-1 20×20 ×3 */
.campfire-scene .bp-7  { left: 1170px;  top:  72px; width:  60px; height:  60px; } /* shrub-small-2 20×20 ×3 */
/* Round-3.7: three distinct small rocks + one medium bush scattered
   across the gap where the wagon was. Y-positions intentionally vary
   from 55 → 175 so the back row reads as a natural scatter rather
   than a straight line. bp-10 (shrub) is mirrored via scaleX so it
   doesn't visually echo bp-2's shrub on the far left. */
/* Round-3.8e: bp-8/9/10 also brought back into frame. */
.campfire-scene .bp-8  { left: 640px;   top:  68px; width:  60px; height:  60px; } /* rock-small-1  20×20 ×3 */
.campfire-scene .bp-9  { left: 760px;   top: 130px; width:  60px; height:  60px; } /* rock-small-2  20×20 ×3 — slightly lower */
.campfire-scene .bp-10 { left: 855px;   top: 102px; width:  96px; height:  96px; transform: translate(-50%, -100%) scaleX(-1); } /* shrub 32×32 ×3 — mirrored */

/* FRONT row — Round-3.4 (2026-05-13): pared down to 4 props in a
   half-moon arc framing the bottom of the scene per Eli ("the focus
   should be the grave, hat, and gun, not all this rocks and junk").
   Two tall cacti anchor the bottom-left and bottom-right corners;
   two rock-large boulders sit closer to the memorial as the arc's
   mid-points. No props directly below the gravestone — that space
   stays clear so the hat + gun read as the focal cluster. */
/* Round-3.8: front-row prop bottoms shifted up −102 (815 → 713, 775 → 673)
   to match the new scene bottom at y=713. */
/* Round-3.8d: front-row prop bottoms shifted −75 to match the new
   639 scene bottom (was 714). */
.campfire-scene .fp-1 { left:  80px;   top: 638px; width: 256px; height: 256px; } /* cactus-tall    64×64  ×4 — far-left anchor */
.campfire-scene .fp-2 { left: 400px;   top: 598px; width: 192px; height: 192px; } /* rock-large     32×32  ×6 — left mid-arc */
.campfire-scene .fp-3 { left: 820px;   top: 598px; width: 192px; height: 192px; transform: translate(-50%, -100%) scaleX(-1); } /* rock-large 32×32 ×6 — right mid-arc, mirrored */
.campfire-scene .fp-4 { left: 1140px;  top: 638px; width: 256px; height: 256px; } /* cactus-saguaro 64×64  ×4 — far-right anchor */

/* Funeral flowers — Round-3.4 (2026-05-13): six small flowers
   scattered around the hat/gun at the base of the memorial per Eli.
   Each is a distinct variety from flowers.png so they read as
   "different flowers thrown by different mourners" rather than a
   uniform bouquet. Source sprites are alpha-trimmed; widths/heights
   below match each trimmed sprite × 5 (small enough to feel
   delicate, big enough to register against the dirt). Bottom-anchor
   transform inherited from `.funeral-flower`. */
.campfire-scene .funeral-flower {
  position: absolute;
  image-rendering: pixelated;
  z-index: 5;  /* "rocks/trees/props" layer */
  transform: translate(-50%, -100%);
  transform-origin: 50% 100%;
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.45));
}
/* Round-3.5: fanned-out scatter around the memorial — three flowers
   each side of the grave at varying distances + heights. Restored
   from the cluster-on-mound layout (which read too rigid) per Eli.
   All y-positions shifted ~25 px up from the original to match the
   new hat position. */
/* Round-3.8: each flower's top shifted up −102 to track the memorial cluster. */
/* Round-3.8d: flower tops shifted −75 with the rest. */
.campfire-scene .ff-1 { left: 470px; top: 408px; width: 35px; height: 60px; } /* flower-blue       7×12 ×5 — far-left, lower */
.campfire-scene .ff-2 { left: 760px; top: 403px; width: 25px; height: 60px; } /* flower-yellow     5×12 ×5 — far-right, lower */
.campfire-scene .ff-3 { left: 520px; top: 353px; width: 50px; height: 65px; } /* flower-violet    10×13 ×5 — left, mid */
.campfire-scene .ff-4 { left: 720px; top: 353px; width: 25px; height: 60px; } /* flower-pink       5×12 ×5 — right, mid */
.campfire-scene .ff-5 { left: 560px; top: 326px; width: 25px; height: 50px; } /* flower-white      5×10 ×5 — upper-left near grave */
.campfire-scene .ff-6 { left: 680px; top: 326px; width: 40px; height: 65px; } /* flower-sunflower  8×13 ×5 — upper-right near grave */

/* Fable-stage column layout: at desktop ≥1080px the 816×816 scene is a
   left column and the fable prose flows on the right. Below 1080px the
   fable stacks under the scene. The campfire wake DOM is the only fable
   surface — the pre-campfire storyteller-portrait layout was removed in
   Task 24 along with cemetery_parallax.js and the sky PNGs. */
.campfire-stage {
  display: block;
  max-width: 1600px;
  margin: 0 auto;
  position: relative;
}

@media (min-width: 1080px) {
  /* Round-2 H2 desktop layout: the 1224px scene IS the stage. Speech bubble
     overlays the upper-right area inside the scene; no separate right
     column needed because the scene is now wide enough to contain both.
     Tail points down-left at the speaker NPC (seat-3 at scene-local
     119, 503). */
  .campfire-stage {
    width: 1224px;
    margin: 0 auto;
  }
  .campfire-stage .campfire-scene {
    margin: 0;
  }

  /* SPEECH BUBBLE — #fable-stage is itself a proper speech bubble. Eli
     pointed to RPG-Maker-XP custom message systems
     (gdu.one/scripts/rpg-maker-xp/custom-message-system/) as the
     reference pattern: a compact bubble that pops up near the
     character, contains the dialogue text itself, and has a short
     tail clearly pointing at the speaker.

     This bubble is anchored to a FIXED scene-local BOTTOM edge so the
     bubble grows UP as text lengthens — the bottom-left corner stays
     at a predictable position, which means the SVG tail target is
     fixed regardless of fable length.

     Position: scene-local x=260, bottom-edge at scene-local y=360.
     With auto-height the bubble grows UP from y=360. Speaker NPC head
     at scene-local (~119, ~440). Tail spans from bubble's bottom-left
     (260, 360) down-LEFT to roughly (100, 460) — right where the
     speaker's head is. */
  /* Speech bubble — Round-3.8c (2026-05-13): switched from Temani
     Afif's clip-path + border-image technique to a simpler two-piece
     setup (rounded-rect body + ::before tail). The clip-path approach
     was leaving visible squared-off corner artifacts (Eli's flag).
     Now the body uses plain border-radius for clean rounded corners
     and the tail is a separate triangle. `filter: drop-shadow` on
     the parent composites both pieces, so the shadow follows the
     bubble + tail silhouette as a single shape. */
  .campfire-stage #fable-stage {
    display: block;
    border-bottom: none;
    margin-bottom: 0;
    position: absolute;
    /* Round-3: scene compressed to 816 tall; speech bubble bottom-y
       target: 560 × 0.666 ≈ 373. CSS bottom = 816 − 373 = 443. */
    bottom: 443px;
    left: 240px;
    width: 720px;
    padding: 20px 26px;
    color: #2b1d10;
    background: rgba(245, 232, 200, 0.96);
    border-radius: 22px;
    /* Unified drop-shadow that follows the bubble + ::before tail
       silhouette (filter composites them before applying). */
    filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.45))
            drop-shadow(0 2px 0   rgba(0, 0, 0, 0.30));
    z-index: 10;
  }

  /* Tail — left-pointing triangle attached to the bubble's bottom-left
     area (~70 % down the left edge), matching where the speaker NPC
     sits below. Same bg color as the bubble; the parent's
     filter: drop-shadow renders one continuous shadow for the
     bubble + tail. */
  .campfire-stage #fable-stage::before {
    content: '';
    position: absolute;
    left: -28px;
    top: calc(70% - 11px);  /* tail-base centered at 70 % down */
    width: 30px;
    height: 22px;
    background: rgba(245, 232, 200, 0.96);
    clip-path: polygon(100% 0, 100% 100%, 0 50%);
  }

  .campfire-stage #fable-stage .fable-line {
    font-family: 'Crimson Pro', Georgia, serif;
    font-style: italic;
    /* 2026-05-13: bumped from 1.05 → 1.2rem so the tale reads at a
       glance inside the bubble. The closer paragraph was moved out
       (now lives in #sim-stage), freeing the bubble for just the
       speaker's quoted words. */
    font-size: 1.2rem;
    line-height: 1.55;
    margin: 0;
    color: #2b1d10;
  }
  .campfire-stage #fable-stage .fable-line::before { content: '\201C'; }
  .campfire-stage #fable-stage .fable-line::after  { content: '\201D'; }
  /* Build identifiers (bullet names, relic names) bolded by renderFable's
     highlights pass — sepia-brown so they pop against the parchment. */
  .campfire-stage #fable-stage .fable-line strong {
    color: #7a4318;
    font-weight: 700;
  }
}

@media (max-width: 1079px) {
  /* Narrow (Eli, 2026-06-08): the storyteller's tale sits ABOVE the scene as a
     readable speech bubble whose tail points down at the speaking NPC (seat-3,
     upper-left of the scene). Flex column + order pulls the fable above the
     diorama even though it follows it in the DOM. */
  .campfire-stage {
    display: flex;
    flex-direction: column;
  }
  .campfire-stage .campfire-scene { order: 2; }
  .campfire-stage #fable-stage {
    order: 1;
    display: block;
    border-bottom: none;
    max-width: 640px;
    margin: 8px auto 18px auto;
    padding: 14px 18px;
    background: rgba(9, 6, 4, 0.86);
    border: 1px solid rgba(212, 154, 59, 0.3);
    border-radius: 12px;
    color: #ffffff;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
    position: relative;
  }
  /* Bubble tail — points down toward the speaker (seat-3, ~10% from the left
     of the scene). */
  .campfire-stage #fable-stage::after {
    content: '';
    position: absolute;
    left: 11%;
    bottom: -12px;
    width: 0;
    height: 0;
    border-left: 10px solid transparent;
    border-right: 10px solid transparent;
    border-top: 12px solid rgba(9, 6, 4, 0.86);
  }
  .campfire-stage #fable-stage .speech-bubble-tail { display: none; }
  .campfire-stage #fable-stage .fable-line {
    font-family: 'Crimson Pro', Georgia, serif;
    font-style: italic;
    font-size: 1.05rem;
    line-height: 1.55;
  }
  .campfire-stage #fable-stage .fable-line::before { content: '\201C'; }
  .campfire-stage #fable-stage .fable-line::after  { content: '\201D'; }
  .campfire-stage #fable-stage .fable-line strong { color: var(--accent); }
}

/* Narrator transition: "Folks who saw them say it looked a little like
   this…" Now lives in #sim-stage as a standalone caption between the
   speech bubble (above) and the thought bubble (below). Italic cream
   prose, centered, max-width matches the thought bubble below it. */
/* Round-4 (2026-06-08): the fable-closer is overlaid as a cinematic title card
   across the top of the simulator (a storyteller's caption burned into the
   "vision"), with a dark scrim for legibility and a flickering gold ember
   flourish above it — instead of plain text sitting above the canvas. */
#sim-stage .fable-closer {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 4;
  margin: 0;
  padding: 16px clamp(8px, 3vw, 20px) 42px;
  background: linear-gradient(180deg,
    rgba(9, 6, 4, 0.94) 0%,
    rgba(9, 6, 4, 0.6) 45%,
    rgba(9, 6, 4, 0) 100%);
  text-align: center;
  font-family: 'Crimson Pro', Georgia, serif;
  font-style: italic;
  /* Fluid size + nowrap so the caption stays on a single line at any width
     (scales down on phones instead of wrapping) — Eli, 2026-06-08. */
  white-space: nowrap;
  font-size: clamp(0.52rem, 2.7vw, 1.15rem);
  line-height: 1.4;
  color: #e7d4ab;
  letter-spacing: 0.015em;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9), 0 0 16px rgba(212, 154, 59, 0.18);
  pointer-events: none;
}
#sim-stage .fable-closer::before {
  content: '\25C6';            /* ◆ — a small ember/spark above the line */
  display: block;
  margin: 0 auto 5px;
  font-style: normal;
  font-size: 0.62rem;
  color: var(--accent);
  text-shadow: 0 0 10px rgba(212, 154, 59, 0.75);
  animation: fableEmber 2.6s ease-in-out infinite;
}
@keyframes fableEmber {
  0%, 100% { opacity: 0.5; transform: translateY(0); }
  50%      { opacity: 1;   transform: translateY(-1px); }
}
@media (prefers-reduced-motion: reduce) {
  #sim-stage .fable-closer::before { animation: none; opacity: 0.85; }
}

/* ─────── SIMULATOR STAGE — Round-3.8 (2026-05-13) ───────
   Thought-bubble framing + trail dots DROPPED per Eli ("kill the
   thought bubble idea now and just fade to the black background to
   the simulator"). The simulator now lives on a black backdrop that
   fades in from the dirt above, with just the fable-closer caption
   above it ("Folks who saw them in action…"). */
#sim-stage #sim-canvas {
  display: block;
  width: 100%;
  max-width: 100%;
  margin: 0;
  position: relative;
  z-index: 1;
  /* Subtle warm color grading — the "vision in the memory" treatment. */
  filter: sepia(0.18) saturate(1.08) brightness(1.04);
}

/* Round-3.8: sim-stage now lives on the black .build-viewer-wrap bg
   (campfire-hearth terminated above this section, with a fade strip
   transitioning the page from dirt to black between them). */
/* Round-4 (2026-06-08): the simulator is now a panel matching the other
   build-viewer sections (same width + slightly rounded 6px corners) rather than
   a bare canvas on the black backdrop. The canvas fills it edge-to-edge and the
   fable-closer is overlaid as a title card on top. */
#build-viewer-wrap #content > section#sim-stage {
  padding: 0;
  overflow: hidden;
  position: relative;
}

.sim-caption {
  background: rgba(20, 16, 14, 0.85);
  padding: 8px 12px;
  border-radius: 4px;
  margin-top: 18px;
  max-width: 720px;
  margin-left: auto;
  margin-right: auto;
}

/* ─────── PAGE-WIDE AMBIENT ATMOSPHERICS ───────
   Tumbleweed, dust clouds, and dust specks all live at the .campfire-hearth
   level (not clipped inside the 816×816 focal scene). z-index 0 keeps them
   BEHIND the content panels (which have opaque rgba(20,16,14,0.85)
   backgrounds), so they only show through the focal scene + the dirt-strip
   gaps. Eli's intent (2026-05-13): "all of the similar effects should
   spread across the entire page". */

/* Tumbleweed — Round-3.8 (2026-05-13): now lives inside .campfire-scene
   (Eli: "lock the dust clouds and tumblweeds to campfire-hearth").
   Size doubled to 192×192. Cadence changed to "very random, not
   constant": the visible roll happens in only ~10 % of the cycle,
   with a long off-screen pause between rolls so the tumbleweed
   reads as a rare "huh, look at that" beat. */
/* Atmospherics clip layer — campfire_scene.js pins this to exactly the dirt
   region of the (scaled) scene, full width, overflow hidden, so the tumbleweed
   and dust clouds can never drift into the fade-to-black gradient below the
   scene (Eli, 2026-06-08). */
.campfire-atmos {
  position: absolute;
  left: 0;
  width: 100%;
  top: 0;            /* top + height set per layout in JS */
  height: 100%;
  overflow: hidden;
  pointer-events: none;
  z-index: 5;
}

.campfire-hearth .tumbleweed {
  position: absolute;
  top: 62%;  /* rolls along the lower dirt of the clip layer */
  width: 192px;
  height: 192px;
  background-image: url('../assets/campfire/effects/tumbleweed.png');
  background-size: 2496px 192px;  /* 13 frames × 192 (2× of original 1248×96) */
  background-repeat: no-repeat;
  background-position-x: 0;
  image-rendering: pixelated;
  /* Round-3.8c (2026-05-13): z=5 so the tumbleweed rolls IN FRONT of
     the mound (z=4) — Eli: "the grave is on the ground, the tumbleweed
     and clouds should not blow under the dirt mound." Still behind
     NPCs (z=6) and behind the tombstone/hat/gun/flowers (z=5, but
     rendered after this element in DOM order). */
  z-index: 5;
  pointer-events: none;
  animation:
    campfire-tumble-rotate 1.517s steps(13, end) infinite,
    campfire-tumble-roll-scene 75s linear infinite;
}

@keyframes campfire-tumble-rotate {
  from { background-position-x: 0; }
  to   { background-position-x: -2496px; }
}

@keyframes campfire-tumble-roll-scene {
  /* 75 s cycle: 0-90 % off-screen left (= 67.5 s pause), then 90-100 %
     rolls across the FULL hearth width (= 7.5 s of visible motion). The end
     distance (--drift-end) is bound to the real hearth width in JS. Reads as
     a rare event rather than a constant roll. */
  0%, 90% { transform: translateX(-220px); }
  100%    { transform: translateX(var(--drift-end, 1424px)); }
}

/* Dust clouds — wind-blown ambient dust drifting horizontally across the
   page. Cite: Textures/Effects/DustClouds.png (192×64 = 3 frames of
   64×64) used in TownRoom.unity as a ParticleSystem with low-alpha warm
   gold color (31-38%), 7-12 unit cloud size, 3-10 s lifetime. NOT smoke
   from the fire — that was a misread in round-1. The in-game effect is
   ambient haze that the WIND moves across the town. */
/* Dust clouds — Round-4 (2026-06-08): relocated to .campfire-hearth so the
   haze drifts across the FULL hearth width (like the specks), not just the
   centered scene. Each cloud's drift end is set per-element in JS
   (--drift-end = hearth width + buffer). */
.campfire-hearth .dust-cloud {
  position: absolute;
  width: 320px;
  height: 320px;
  background-image: url('../assets/campfire/effects/dust-clouds.png');
  background-size: 960px 320px;
  background-repeat: no-repeat;
  background-position-x: 0;
  image-rendering: pixelated;
  opacity: 0;
  filter: brightness(1.1) saturate(0.7);
  /* Round-3.8c (2026-05-13): z=5 so clouds drift IN FRONT of the
     mound (z=4) — they shouldn't blow UNDER it. Still behind NPCs
     (z=6) and behind tombstone/hat/gun/flowers via DOM order at
     the same z=5 layer. */
  z-index: 5;
  pointer-events: none;
  animation-name: campfire-dust-drift;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

@keyframes campfire-dust-drift {
  /* Drift from off-left to off-right across the full hearth width
     (--drift-end set per cloud in JS). Peak opacity 0.07 — "barely
     noticeable" per Eli. */
  0%   { transform: translateX(-380px);                       opacity: 0;    }
  15%  {                                                       opacity: 0.07; }
  85%  {                                                       opacity: 0.07; }
  100% { transform: translateX(var(--drift-end, 1324px));     opacity: 0;    }
}

/* Dust specks — small drifting motes. Cite: Prefabs/Effects/DustPFX.prefab
   (lifetime 3-5 s, speed 0-2 u/s, size 0.1 u, warm white→tan color lerp).
   JS-generated, scattered across the entire .campfire-hearth (not just
   the focal scene) — at the v=4 controller, ~40 motes across the upper
   ~1800 px of the hearth area covering scene + simulator. */
.campfire-hearth .dust-speck {
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 1px;
  pointer-events: none;
  z-index: 7;  /* "particle specks" layer per Eli's z-order — in front
                  of characters but behind dust clouds */
  animation-name: campfire-speck-drift;
  animation-timing-function: ease-out;
  animation-iteration-count: infinite;
}

@keyframes campfire-speck-drift {
  0%   { transform: translate(0, 0);                opacity: 0;   }
  30%  {                                            opacity: 0.8; }
  100% { transform: translate(var(--dx), var(--dy)); opacity: 0;   }
}

@media (prefers-reduced-motion: reduce) {
  .campfire-hearth .dust-speck { display: none; }
  .campfire-hearth .tumbleweed { display: none; }
  .campfire-hearth .dust-cloud { display: none; }
  /* Speaker animation also pauses under reduced motion */
  .campfire-scene .npc-seat .npc-sprite.is-speaker {
    animation-play-state: paused;
  }
  #sim-stage #sim-canvas { animation: none; opacity: 1; }
}
