/* ============================================================================
   Blotch - promo site
   ----------------------------------------------------------------------------
   The palette, the radii and the chunky dark outline are lifted from the game's
   own UI theme (scripts/ui/ui.gd) so the site and the game read as one thing.
   If you retune Ui.INK / Ui.ACCENT in the game, retune the tokens below to match
   - they are hand-kept in sync on purpose, because the site has to be deployable
   without a Godot build step of any kind.

   Committed dark, deliberately: the game is a dark game and the wordmark, the
   hero art and every character portrait are drawn to sit on ink. A light theme
   would need a second set of art, so `color-scheme` pins dark rather than
   offering a toggle that would look broken.
   ========================================================================== */

/* --- tokens --------------------------------------------------------------- */
:root {
  color-scheme: dark;

  /* From Ui's palette. */
  --ink:      #11131a;
  --canvas:   #1b202b;
  --raised:   #262c3a;
  --hilite:   #2f3749;
  --line:     #3a4356;
  --text:     #eef1f7;
  --dim:      #9aa6bd;
  --gold:     #ffce47;
  --gold-dim: #c79a2e;
  --gold-lit: #ffda73;
  --teal:     #3fd9c4;

  /* The "paint family" look: every solid thing in the game wears a thick dark
     rim and sits on a hard offset shadow (see marker_art.gd's _living_body). */
  --rim: #0a0c11;
  --rim-w: 3px;
  --drop: 0 6px 0 var(--rim);
  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 26px;

  --gut: clamp(1.25rem, 4vw, 2.5rem);
  --page: 1140px;

  --ease: cubic-bezier(.2, .8, .25, 1);
}

/* --- the one webfont ------------------------------------------------------ */
@font-face {
  font-family: "Luckiest Guy";
  src: url("../assets/fonts/LuckiestGuy.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* --- reset / base -------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  /* The nav is sticky, so anchor jumps must land BELOW it rather than under it. */
  scroll-padding-top: 5.5rem;
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--text);
  font: 400 clamp(1rem, .96rem + .2vw, 1.075rem)/1.65
        system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  /* A single guard against the one layout bug that ruins a page on a phone. */
  overflow-x: hidden;
}

img, picture { max-width: 100%; display: block; }
a { color: var(--gold); text-decoration-thickness: 2px; text-underline-offset: 3px; }
a:hover { color: var(--gold-lit); }

:focus-visible {
  outline: 3px solid var(--gold);
  outline-offset: 3px;
  border-radius: 4px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: .5rem;
  z-index: 100;
  padding: .6rem 1rem;
  background: var(--gold);
  color: var(--ink);
  border-radius: var(--r-sm);
  font-weight: 700;
}
.skip:focus { left: .5rem; }

/* --- type --------------------------------------------------------------- */
.display {
  font-family: "Luckiest Guy", system-ui, sans-serif;
  font-weight: 400;
  line-height: 1.03;
  letter-spacing: .01em;
  font-size: clamp(2.1rem, 1.2rem + 4.2vw, 3.9rem);
  margin: .1em 0 .45em;
  /* The wordmark's own trick: off-white letters over a hard dark rim. */
  text-shadow: 0 3px 0 var(--rim);
}

.eyebrow {
  margin: 0;
  font-size: .8rem;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--gold);
}

.lede {
  font-size: clamp(1.06rem, 1rem + .45vw, 1.28rem);
  line-height: 1.6;
  color: color-mix(in oklab, var(--text) 82%, var(--dim));
  max-width: 62ch;
  margin: 0 0 1.5rem;
}
.lede--wide { max-width: 78ch; }
.lede em { color: var(--gold); font-style: italic; }

/* --- layout ------------------------------------------------------------- */
.wrap {
  width: min(100% - (2 * var(--gut)), var(--page));
  margin-inline: auto;
}
.wrap--narrow { --page: 820px; }

.band { padding: clamp(3.5rem, 9vw, 7rem) 0; position: relative; }

/* Section separators are a hand-drawn paint line, not a hairline rule: two
   offset gradients so the edge has the same chunky rim as everything else. */
.band + .band::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: var(--rim-w);
  background: linear-gradient(90deg,
    transparent 0 6%, var(--line) 6% 94%, transparent 94%);
  opacity: .55;
}

.band--mechanic { background: var(--canvas); }
.band--cast {
  background:
    /* The gallery floor, as a checker that costs zero bytes. */
    repeating-conic-gradient(from 0deg at 50% 50%,
      #ffffff05 0% 25%, #00000000 0% 50%) 0 0 / 64px 64px,
    var(--ink);
}
.band--more { background: var(--canvas); }
.band--closing { padding-bottom: clamp(4.5rem, 10vw, 8rem); }

/* --- top bar ------------------------------------------------------------ */
.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: clamp(.75rem, 3vw, 2rem);
  padding: .55rem var(--gut);
  background: color-mix(in oklab, var(--ink) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: var(--rim-w) solid var(--rim);
}

.topbar__mark { flex: 0 0 auto; line-height: 0; }
.topbar__mark img { width: clamp(84px, 15vw, 122px); height: auto; }

.topbar__nav {
  display: flex;
  gap: clamp(.75rem, 2.5vw, 1.6rem);
  margin-inline-start: auto;
  font-size: .95rem;
  font-weight: 600;
}
.topbar__nav a { color: var(--dim); text-decoration: none; white-space: nowrap; }
.topbar__nav a:hover { color: var(--text); }

/* The nav links are the first thing to go on a narrow screen - the wordmark and
   the wishlist chip are what the bar is actually for. */
@media (max-width: 640px) {
  .topbar__nav { display: none; }
  .topbar .cta--small { margin-inline-start: auto; }
}

/* --- buttons ------------------------------------------------------------ */
.btn, .cta {
  --btn-bg: var(--raised);
  --btn-fg: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  padding: .8em 1.35em;
  border: var(--rim-w) solid var(--rim);
  border-radius: var(--r-md);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font: 700 1rem/1.1 inherit;
  text-decoration: none;
  text-align: center;
  box-shadow: var(--drop);
  transition: transform .16s var(--ease), background-color .16s var(--ease);
}

.btn:hover, a.cta:hover {
  --btn-bg: var(--hilite);
  transform: translateY(-2px);
  box-shadow: 0 8px 0 var(--rim);
}
.btn:active, a.cta:active { transform: translateY(2px); box-shadow: 0 3px 0 var(--rim); }

.btn--ghost { --btn-bg: transparent; --btn-fg: var(--text); }

/* A LIVE wishlist link (what site.js swaps in once the store page is public). */
a.cta { --btn-bg: var(--gold); --btn-fg: #241d05; }
a.cta:hover { --btn-bg: var(--gold-lit); }

/* The PENDING state, which is what ships until the store page exists. A <span>,
   not a disabled <a>: there is no destination yet, so nothing here should look
   or behave like something you can click and have happen. */
.cta:not(a) {
  --btn-bg: color-mix(in oklab, var(--canvas) 70%, var(--gold-dim) 30%);
  --btn-fg: var(--gold-lit);
  border-style: dashed;
  border-color: var(--gold-dim);
  box-shadow: none;
  cursor: default;
}
.cta:not(a)::before { content: "★"; color: var(--gold); font-size: .9em; }

.cta--big { font-size: clamp(1rem, .95rem + .35vw, 1.15rem); padding: .95em 1.7em; }
.cta--small { font-size: .85rem; padding: .6em 1em; border-radius: var(--r-sm); }

/* --- hero --------------------------------------------------------------- */
.hero {
  position: relative;
  isolation: isolate;
  display: grid;
  align-items: center;
  min-height: min(84svh, 760px);
  padding: clamp(3rem, 8vw, 5.5rem) 0 clamp(4rem, 9vw, 6rem);
  overflow: hidden;
  border-bottom: var(--rim-w) solid var(--rim);
}

.hero__bg { position: absolute; inset: 0; z-index: -2; }
/* Framed off-centre on purpose. The library hero has a blob of its own standing on a checkered
   gallery floor near the middle of the art, and centred it lands directly behind the pitch
   paragraph - so the page appeared to have two mascots, one of them sitting in the text. This
   pushes that room off to the left and puts the crystal rooms behind the wordmark instead. */
.hero__bg img { width: 100%; height: 100%; object-fit: cover; object-position: 70% 46%; }

/* Two scrims, doing two different jobs: a vertical one so the top bar and the
   copy always have contrast, and a radial one that pushes the busy scenery away
   from the middle where the wordmark sits. */
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(120% 90% at 50% 45%,
      color-mix(in oklab, var(--ink) 42%, transparent) 0%,
      color-mix(in oklab, var(--ink) 86%, transparent) 68%),
    linear-gradient(to bottom,
      color-mix(in oklab, var(--ink) 72%, transparent) 0%,
      transparent 28% 62%,
      var(--ink) 100%);
}

.hero__inner {
  width: min(100% - (2 * var(--gut)), 880px);
  margin-inline: auto;
  text-align: center;
  position: relative;
}

.hero__mark img {
  width: min(100%, 620px);
  height: auto;
  margin-inline: auto;
  filter: drop-shadow(0 10px 26px #0009);
}

.hero__tagline {
  font-family: "Luckiest Guy", system-ui, sans-serif;
  font-size: clamp(1.25rem, .9rem + 1.9vw, 2.1rem);
  color: var(--gold);
  margin: .35em 0 .5em;
  text-shadow: 0 3px 0 var(--rim);
}

.hero__pitch {
  max-width: 54ch;
  margin: 0 auto 1.9rem;
  font-size: clamp(1.02rem, .98rem + .4vw, 1.2rem);
  color: color-mix(in oklab, var(--text) 88%, var(--dim));
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: .85rem;
  justify-content: center;
}

/* The mascot sits on a stripe of wet paint below the buttons. On a short
   viewport it is the first thing dropped - it is charm, not information. */
.hero__blob {
  position: relative;
  width: clamp(150px, 24vw, 240px);   /* ~2x the visible blob - see the SIZING NOTE below */
  margin: clamp(1.5rem, 4vw, 2.25rem) auto 0;
}
.hero__blob img {
  width: 100%;
  height: auto;
  transform-origin: 50% 88%;
  animation: breathe 3.4s var(--ease) infinite;
  filter: drop-shadow(0 8px 14px #0007);
}
/* The wet-paint stripe the mascot stands on. Positioned off the blob's VISIBLE base (~75% down
   its transparent frame), not the frame's own bottom edge, or it floats well clear of it. */
.hero__stripe {
  position: absolute;
  left: 50%;
  bottom: 21%;
  translate: -50% 0;
  width: 72%;
  height: 10px;
  border-radius: 999px;
  background: linear-gradient(90deg, transparent, var(--gold-dim) 22% 78%, transparent);
  opacity: .5;
  filter: blur(1px);
}

/* The blob's own soft-body mould: press on it and it pancakes, then reforms. */
.hero__blob:hover img, .hero__blob:active img {
  animation: none;
  scale: 1.1 .84;
  transition: scale .18s var(--ease);
}

@keyframes breathe {
  0%, 100% { scale: 1 1; }
  50%      { scale: 1.035 .965; }
}

.hero__scroll {
  position: absolute;
  bottom: 1.1rem;
  left: 50%;
  translate: -50% 0;
  margin: 0;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--dim);
  animation: nudge 2.6s var(--ease) infinite;
}
@keyframes nudge {
  0%, 100% { transform: translateY(0); opacity: .45; }
  50%      { transform: translateY(5px); opacity: .95; }
}

@media (max-height: 620px) {
  .hero__blob, .hero__scroll { display: none; }
}

/* --- the camouflage reveal ---------------------------------------------- */
/* --reveal is the authored starting split, and 50% is load-bearing: it cuts the blob down the
   middle, so the one frame a visitor with no JavaScript ever sees is already the whole argument -
   half of it vanished into the leaves, half still an obvious grey lump. site.js overwrites it on
   the element as soon as anyone touches the control. */
.camo { --reveal: 50%; margin: clamp(1.5rem, 4vw, 2.75rem) auto 0; }

.camo__frame {
  position: relative;
  border: var(--rim-w) solid var(--rim);
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: 0 14px 40px #0008;
  /* Pointer-dragging the frame is a JS enhancement over the range input below;
     this only stops a drag from selecting the image instead. */
  touch-action: pan-y;
  cursor: ew-resize;
  user-select: none;
}
.camo__img { width: 100%; height: auto; }

/* The painted frame is wiped in from the LEFT as --reveal grows, so dragging
   right paints more of the blob - the same direction the brush moves. */
.camo__painted {
  position: absolute;
  inset: 0;
  clip-path: inset(0 calc(100% - var(--reveal)) 0 0);
}

.camo__seam {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--reveal);
  width: 3px;
  translate: -1.5px 0;
  background: linear-gradient(to bottom, var(--gold), var(--gold-dim));
  box-shadow: 0 0 12px #ffce4788;
  pointer-events: none;
}

.camo__control {
  display: flex;
  align-items: center;
  gap: .9rem;
  margin: 1.15rem 0 .6rem;
  font-size: .9rem;
  font-weight: 700;
}
.camo__label { color: var(--gold); white-space: nowrap; }
.camo__out {
  min-width: 3.4ch;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}

.camo__range {
  flex: 1;
  appearance: none;
  height: 14px;
  border: var(--rim-w) solid var(--rim);
  border-radius: 999px;
  background: linear-gradient(90deg, var(--raised), var(--gold-dim));
  cursor: ew-resize;
}
.camo__range::-webkit-slider-thumb {
  appearance: none;
  width: 26px;
  height: 26px;
  border: var(--rim-w) solid var(--rim);
  border-radius: 50%;
  background: var(--gold);
  cursor: grab;
}
.camo__range::-moz-range-thumb {
  width: 26px;
  height: 26px;
  border: var(--rim-w) solid var(--rim);
  border-radius: 50%;
  background: var(--gold);
  cursor: grab;
}

.camo__cap {
  margin: 0;
  font-size: .93rem;
  color: var(--dim);
  max-width: 68ch;
}

/* --- the clips ---------------------------------------------------------- */
.clips {
  display: grid;
  gap: clamp(2rem, 5vw, 3.5rem);
  margin-top: clamp(2rem, 5vw, 3.25rem);
}

.clip {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2.25rem);
  align-items: center;
}
@media (min-width: 780px) {
  /* EQUAL tracks. With uneven ones (media 1.15fr) the `order` flip below also swaps which
     track the media lands in, so every other clip came out visibly narrower than the last. */
  .clip { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  /* Alternate the media side so four stacked rows don't read as a list. */
  .clip:nth-child(even) > .clip__media { order: 2; }
}

.clip__media {
  border: var(--rim-w) solid var(--rim);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--canvas);
  box-shadow: 0 10px 30px #0007;
}
.clip__media img { width: 100%; height: auto; }

.clip__text h3 {
  font-family: "Luckiest Guy", system-ui, sans-serif;
  font-weight: 400;
  font-size: clamp(1.35rem, 1.1rem + 1vw, 1.85rem);
  line-height: 1.12;
  margin: 0 0 .5em;
  text-shadow: 0 2px 0 var(--rim);
}
.clip__text p { margin: 0; color: color-mix(in oklab, var(--text) 78%, var(--dim)); }
.clip__text em { color: var(--gold); font-style: italic; }

/* --- the cast ----------------------------------------------------------- */
/* SIX cards, so the column count is pinned rather than left to auto-fit: at 1140px auto-fit
   picks four and leaves a stranded row of two. 3x2 and 2x3 both divide evenly. */
.cast {
  list-style: none;
  margin: clamp(2rem, 5vw, 3.25rem) auto 0;
  padding: 0;
  display: grid;
  gap: clamp(1rem, 2.5vw, 1.5rem);
  grid-template-columns: 1fr;
}
@media (min-width: 560px) { .cast { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 900px) { .cast { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.cast__card {
  padding: 1.4rem 1.3rem 1.5rem;
  background: var(--canvas);
  border: var(--rim-w) solid var(--rim);
  border-radius: var(--r-lg);
  box-shadow: var(--drop);
  transition: transform .2s var(--ease), border-color .2s var(--ease);
}
.cast__card:hover {
  transform: translateY(-4px);
  border-color: var(--gold-dim);
}
.cast__card--hero { background: color-mix(in oklab, var(--canvas) 78%, var(--gold-dim) 22%); }

/* SIZING NOTE for every portrait box on this page (.cast__art, .hero__blob, .closing__peek):
   the captures are NOT trimmed to each character's bounding box - they share one world scale so
   the roster's real size differences survive (see CAST_WORLD_R in web/tools/cast_capture.gd). The
   consequence is transparent margin: the smallest member fills ~40% of its frame and the largest
   ~85%. So a display box reads at roughly HALF its own size, and these numbers are deliberately
   about 2x what you would pick from the visible art. Shrink the box and the sentinel disappears. */
.cast__art {
  display: grid;
  place-items: center;
  height: 180px;
  margin-bottom: .4rem;
}
.cast__art img {
  width: 176px;
  height: 176px;
  object-fit: contain;   /* one shared capture scale, so relative sizes are real */
  filter: drop-shadow(0 6px 10px #0008);
  transition: rotate .25s var(--ease), scale .25s var(--ease);
}
/* Each hunter leans in when you look at it. */
.cast__card:hover .cast__art img { rotate: -5deg; scale: 1.08; }

.cast__card h3 {
  font-family: "Luckiest Guy", system-ui, sans-serif;
  font-weight: 400;
  font-size: 1.35rem;
  margin: 0;
  text-shadow: 0 2px 0 var(--rim);
}
.cast__role {
  margin: .1rem 0 .7rem;
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--teal);
}
.cast__card p:last-child {
  margin: 0;
  font-size: .95rem;
  color: color-mix(in oklab, var(--text) 74%, var(--dim));
}

/* --- everything else ---------------------------------------------------- */
.feats {
  list-style: none;
  margin: clamp(1.5rem, 4vw, 2.5rem) 0 0;
  padding: 0;
  display: grid;
  gap: 1.1rem 2rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
}
.feats li { display: flex; gap: .85rem; align-items: start; }
.feats img { width: 26px; height: 26px; flex: 0 0 auto; margin-top: .18rem; }
.feats span { color: color-mix(in oklab, var(--text) 76%, var(--dim)); }
.feats strong { color: var(--text); }

/* FOUR shots, pinned to 2x2 for the same reason the cast is pinned to 3x2: auto-fit picks three
   at desktop width and strands the fourth on a row of its own. */
.shots {
  list-style: none;
  margin: clamp(2rem, 5vw, 3rem) 0 0;
  padding: 0;
  display: grid;
  gap: clamp(.75rem, 2vw, 1.15rem);
  grid-template-columns: 1fr;
}
@media (min-width: 700px) { .shots { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.shots li {
  border: var(--rim-w) solid var(--rim);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--ink);
}
.shots img { width: 100%; height: auto; }

/* --- closing ----------------------------------------------------------- */
.closing {
  position: relative;
  margin-top: 2.5rem;
  padding: clamp(2.5rem, 6vw, 3.75rem) clamp(1.5rem, 5vw, 3.25rem);
  text-align: center;
  background: color-mix(in oklab, var(--canvas) 84%, var(--gold-dim) 16%);
  border: var(--rim-w) solid var(--rim);
  border-radius: var(--r-lg);
  box-shadow: 0 10px 0 var(--rim), 0 24px 50px #0008;
}
.closing .lede { margin-inline: auto; }
.closing__actions { display: flex; justify-content: center; }

/* A sentinel peering over the top edge, hiding behind the card. It only pokes
   its eye out once the card scrolls into view. */
/* z-index -1 means only the part of this box ABOVE the card's top edge is ever visible, so the
   translate has to clear the portrait's own transparent margin (~30% of the frame) BEFORE the
   sentinel starts to show. -54% puts a bit over half its body over the lip; anything shallower
   just parks it behind the card. */
.closing__peek {
  position: absolute;
  top: 0;
  right: clamp(1.5rem, 8vw, 5rem);
  width: 170px;
  translate: 0 -54%;
  z-index: -1;
  transition: translate .5s var(--ease);
}
.closing__peek img { width: 100%; height: auto; filter: drop-shadow(0 5px 8px #0009); }
/* Gated on `.js`: with no script the peek never gets `is-in`, so it must already
   be at its final position rather than stuck hidden behind the card forever. */
.js .closing:not(.is-in) .closing__peek { translate: 0 -18%; }

/* --- footer ------------------------------------------------------------ */
.foot {
  padding: 2.5rem 0 3rem;
  border-top: var(--rim-w) solid var(--rim);
  background: var(--ink);
}
.foot__inner { display: grid; gap: .5rem; }
.foot__tag {
  margin: 0;
  font-family: "Luckiest Guy", system-ui, sans-serif;
  font-size: 1.15rem;
  color: var(--gold);
}
.foot__credits { margin: 0; font-size: .88rem; color: var(--dim); max-width: 70ch; }

/* --- scroll reveal ----------------------------------------------------- */
/* Opt-IN: the class is only added by site.js, so with JS off or broken every
   section is simply visible. Never hide content behind a script. */
.js [data-reveal] {
  opacity: 0;
  translate: 0 18px;
  transition: opacity .55s var(--ease), translate .55s var(--ease);
}
.js [data-reveal].is-in { opacity: 1; translate: 0 0; }

/* --- 404 --------------------------------------------------------------- */
.lost {
  min-height: 74svh;
  display: grid;
  place-content: center;
  text-align: center;
  gap: .5rem;
}
.lost .lede { margin-inline: auto; }   /* centred here, not via a style attribute - see .camo */
.lost__blob { width: clamp(110px, 20vw, 170px); margin: 0 auto 1rem; }
.lost__blob img { width: 100%; height: auto; animation: breathe 3.4s var(--ease) infinite; }

/* --- motion --------------------------------------------------------------
   Everything above that moves on its own is turned off here. The camo reveal
   stays interactive (it is a control, not decoration) but stops auto-demoing
   itself - site.js checks the same query. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .js [data-reveal] { opacity: 1; translate: 0 0; }
  .js .closing:not(.is-in) .closing__peek { translate: 0 -54%; }
  .hero__scroll { display: none; }
}
