/* =========================================================================
   Wicked Pines — page styles
   -------------------------------------------------------------------------
   Loaded AFTER /assets/pw/stage.css, so it can do two things:

     1. Re-point the site's shared custom properties to a cold-pine palette.
        The utility strip, the footer and the motion button all read those
        variables, so they turn silver here without any of their rules
        being touched or duplicated.

     2. Own the stage and the hero, which are this page's alone.

   Everything is prefixed `wp-`. The shared `pw-` classes keep working.
   ========================================================================= */

/* ------------------------------------------------------------- palette */

.wp-page {
  /* Overrides of the shared vars. These cascade to .pw-strip, .pw-footer
     and .pw-motion, which is the whole point of doing it here. */
  --pw-void: #04070a;
  --pw-void-2: #0a1210;
  --pw-bone: #eef3f0;
  --pw-bone-dim: #9fb2b6;
  --pw-bone-faint: #62767b;
  /* The accent trio lives in stage.css under .pw-accent-frost, which this
     page's <body> carries — one definition, shared with the other themed
     pages, instead of a private copy that drifts. */

  /* Page-local. */
  --wp-pine: #0a1410;
  --wp-pine-2: #12211a;
  --wp-moon: #cfe2ea;
  --wp-frost: #a8cfd6;
  --wp-sage: #7fb59b;
  --wp-measure: 62rem;

  /* Three lines that only exist because this site has no global reset.

     margin: <body> still carried the UA's 8px, which cost 16px of vertical
     height nothing accounted for and pushed every hero element 8px right of
     the padding it was given.

     box-sizing: the padding-top set in pines-base.css (the room the fixed
     strip needs) is added OUTSIDE a content-box min-height, so `100vh` here
     meant 100vh PLUS the strip. Together with the same problem in .wp-frame
     it made a one-screen page 151px too tall and put the contact line under
     the fold. */
  margin: 0;
  box-sizing: border-box;

  background: var(--pw-void) !important;
  color: var(--pw-bone);
  min-height: 100vh;
  min-height: 100dvh;
}

/* --------------------------------------------------------------- stage */

/* Behind ordinary content, exactly like the shared stage: z-index -1 means
   no sibling needs lifting, and the fixed utility strip keeps winning. */
.wp-stage {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: var(--pw-void);
  overflow: hidden;
}

/* The forest plate. Full-bleed, and scaled slightly so the parallax
   translate has headroom and never exposes an edge. It moves LESS than
   the fog in front of it — that difference is the depth. */
.wp-plate {
  /* Wider than the frame, and pulled left by the same amount, so there is
     real off-screen picture to slide into view.

     This is not the obvious construction and the obvious one does not work.
     `object-fit: cover` crops to the ELEMENT's box, so translating the
     element moves the crop rather than revealing what cover is hiding — slide
     it far enough and you get the video's edge and then black, which is
     exactly what the first attempt at this did. The hidden picture is only
     reachable by making the element itself bigger.

     --wp-plate-bleed is how much extra sits off each side, as a percentage of
     the frame. --wp-plate-shift is how far the element may travel, as a
     percentage of ITS OWN width — which is why the two numbers do not match:
     the element is (100 + 2*bleed)% wide, so a shift of S% of the element is
     S * (1 + 2*bleed/100) percent of the frame, and that is what has to stay
     under the bleed. Both pairs below are checked against that. */
  position: absolute;
  top: 0;
  left: calc(-1 * var(--wp-plate-bleed, 8%));
  width: calc(100% + 2 * var(--wp-plate-bleed, 8%));
  /* Explicit, not top:0 + bottom:0. A <video> is a replaced element: given
     only a width it takes the height its own aspect ratio implies and ignores
     the offsets, so the plate silently became a 269px band across the top of
     an 890px frame. A div would have stretched. */
  height: 100%;
  object-fit: cover;
  object-position: center 60%;
  filter: saturate(0.82) contrast(1.08) brightness(0.74);
  transform: translate3d(calc(var(--wp-par-x, 0) * var(--wp-plate-shift, -6%)),
                         calc(var(--wp-par-y, 0) * -0.6%), 0)
             scale(1.04);
  will-change: transform;
}

/* Tilt, not pointer. pines.js adds this on the first device-orientation event
   and drops it again if a mouse takes over.

   Nearly five times the travel. On a phone held upright the sunlit half of
   the clip is a long way off the right edge — cover renders a 16:9 clip about
   1580px wide into a 412px frame, so you are looking at roughly a quarter of
   the picture and the light is nowhere near it. The desktop shift is
   deliberately restrained because a mouse crosses its whole range in a flick;
   a phone has to be physically turned, which should be worth doing. */
.wp-tilting .wp-plate {
  --wp-plate-bleed: 30%;
  --wp-plate-shift: -18%;
}

/* Faded out by script when the clip has not arrived, so the shader's
   procedural treeline shows instead of a black box or a broken-media icon.

   Opacity rather than display:none deliberately. Hiding the element can stall
   a load that is still in flight, which turned a slow clip into a permanently
   missing one. This way it keeps downloading behind the drawn forest and
   fades in when it is ready. */
.wp-plate--waiting { opacity: 0; }

/* The fog. Sits over the plate and veils it. Sized in CSS pixels; the
   drawing buffer underneath is smaller (data-wp-quality) because fog has
   no hard edges to lose. */
.wp-fog {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* Corner fall-off, so the frame reads as a clearing rather than a photo.
   Kept in CSS rather than the shader — free, and easier to tune. */
.wp-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    /* Reading scrim. The hero sits bottom-left, so the frame is weighted
       that way — which doubles as depth: the near corner is in shadow and
       the eye travels right, into the trees. Without it, body copy over a
       bright patch of the clip is a coin toss. */
    linear-gradient(to right, rgba(2, 6, 8, 0.74) 0%, rgba(2, 6, 8, 0.40) 32%, transparent 62%),
    linear-gradient(to top, rgba(2, 6, 8, 0.62) 0%, transparent 46%),
    radial-gradient(120% 85% at 50% 42%, transparent 38%, rgba(2, 5, 7, 0.62) 100%),
    linear-gradient(to bottom, rgba(2, 5, 7, 0.55) 0%, transparent 22%, transparent 100%);
}

/* ----------------------------------------------------------- her mark */

/* The Wicked Pines wordmark and jay, floating in the bottom-right of the
   stage.

   ABOVE the whole stage, not inside it. Inside, the fog canvas would drift
   across it and the vignette would sink it into the corner shadow; the mark
   is the one thing on this page that should never be veiled. z-index 0 puts
   it over the stage (-1) and still under .wp-frame (1), so if the copy ever
   reaches it the words win and the art slides behind them.

   Fixed, not absolute, because the stage is fixed: the page scrolls past a
   backdrop that stays, and the mark belongs to the backdrop.

   THE GLOW IS NOT DECORATION. The lettering is solid black on transparent,
   and this plate is a night forest at roughly 0.74 brightness — dropped
   straight on, "Wicked Pines" disappears everywhere the trunks are dark and
   only half-reads where the light rakes through, which is a coin toss that
   changes as the plate parallaxes underneath. The radial behind it is the
   moon catching the mark, and it is what makes the lettering legible in
   every position of the clip. Recolouring her artwork would have been the
   other fix; this one leaves it untouched. */
.wp-mark {
  position: fixed;
  z-index: 0;
  /* The numbers are measured, not chosen. The <h1>'s ink stops at a hard
     990px once its clamp tops out and never moves again, and the sound
     button owns the last ~80x25px of the corner. So the free rectangle is
     "right of 990, above the button", and the three terms below are exactly
     that: 24rem is the size the art wants, 24vw keeps it proportional, and
     100vw - 66rem is the hard one — 66rem is 990px of heading plus a 10px
     gap plus the 3.5rem inset, so that term is literally "whatever is left
     after the title". It is what lets this placement survive down to 80rem
     instead of needing a breakpoint at 90. */
  right: 3.5rem;
  bottom: 4.25rem;
  width: min(24vw, 24rem, calc(100vw - 66rem));
  aspect-ratio: 1200 / 872;
  pointer-events: none;
  /* Drifts AGAINST the plate, not with it. The plate's shift is negative
     (see --wp-plate-shift) and travels around 100px across the full pointer
     range; matching its direction at a tenth the distance just makes the
     mark look like a piece of the scene that is lagging. Reversing the sign
     for a much smaller distance reads the other way — the forest slides and
     the mark holds, which is what "in front of it" looks like.

     Travel is a variable because the narrow layout sits closer to the edge
     and the art is trimmed to its own ink, so an over-generous drift there
     would push the bird's tail off the screen. */
  --wp-mark-travel-x: 22px;
  --wp-mark-travel-y: 14px;
  transform: translate3d(calc(var(--wp-par-x, 0) * var(--wp-mark-travel-x)),
                         calc(var(--wp-par-y, 0) * var(--wp-mark-travel-y)), 0);
  will-change: transform;
}

.wp-mark::before {
  content: "";
  position: absolute;
  /* Bigger than the mark on every side: the falloff has to finish outside
     the ink or it reads as a disc behind a sticker. */
  inset: -22% -16% -20% -18%;
  background: radial-gradient(closest-side ellipse at 47% 56%,
                rgba(207, 226, 234, 0.34) 0%,
                rgba(168, 207, 214, 0.17) 42%,
                rgba(168, 207, 214, 0.06) 66%,
                transparent 100%);
}

.wp-mark__img {
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  /* Lifts it off the trees. Two shadows: a tight one for the edge, a long
     soft one for the distance to the forest floor. */
  filter: drop-shadow(0 0 4px rgba(216, 234, 239, 0.85))
          drop-shadow(0 0 15px rgba(168, 207, 214, 0.5))
          drop-shadow(0 18px 30px rgba(2, 6, 8, 0.6));
  /* The float lives on the IMAGE and the parallax on the wrapper, because a
     keyframe animating transform replaces the whole property — put both on
     one element and whichever runs last wins and the other silently stops. */
  animation: wp-mark-float 11s ease-in-out infinite alternate;
}

@keyframes wp-mark-float {
  from { transform: translate3d(0, 0.9%, 0) rotate(-0.35deg); }
  to   { transform: translate3d(0, -1.6%, 0) rotate(0.4deg); }
}

/* ---------------------------------------------------------------- hero */

.wp-frame {
  position: relative;
  z-index: 1;
  /* Stated, because this site has no global box-sizing reset and the default
     is content-box - under which min-height sizes the CONTENT and the 135px
     of vertical padding below is added on top. The frame came out 993px tall
     inside an 858px budget, which is what put the contact line under the fold
     and gave a one-screen page a scrollbar. */
  box-sizing: border-box;
  /* --pw-chrome-h, not --pw-strip-h: the chrome is two fixed bands now, the
     utility strip and the breadcrumb row under it. Offsetting by the strip
     alone puts the hero a breadcrumb-height too low. */
  min-height: calc(100vh - var(--pw-chrome-h));
  min-height: calc(100dvh - var(--pw-chrome-h));
  display: flex;
  align-items: flex-end;
  padding: clamp(2rem, 7vh, 6rem) clamp(1.1rem, 5vw, 4rem) clamp(2rem, 8vh, 5rem);
}

.wp-hero {
  width: 100%;
  max-width: var(--wp-measure);
}

.wp-eyebrow {
  font-family: var(--pw-mono);
  font-size: 0.66rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--pw-bone-faint);
  margin-bottom: clamp(0.9rem, 2.5vh, 1.6rem);
}

.wp-title {
  margin: 0;
  font-family: var(--pw-display);
  font-weight: 900;
  font-size: clamp(2.6rem, 11vw, 7.5rem);
  line-height: 0.92;
  letter-spacing: 0.02em;
  color: var(--pw-bone);
  /* A cold halo, as if the letters were catching the same moon the fog is.
     Two shadows: a tight one for legibility over a moving background, a
     wide one for the glow. */
  text-shadow:
    0 0 1px rgba(4, 7, 10, 0.9),
    0 0.06em 0.5em rgba(4, 10, 12, 0.75),
    0 0 1.4em rgba(168, 207, 214, 0.14);
}

.wp-lede {
  margin: clamp(1rem, 3vh, 1.8rem) 0 0;
  max-width: 38ch;
  font-family: var(--pw-body);
  font-size: clamp(0.98rem, 1.6vw, 1.16rem);
  line-height: 1.55;
  color: var(--pw-bone-dim);
  text-shadow: 0 1px 6px rgba(2, 6, 8, 0.85);
}

/* -------------------------------------------------------------- roster */

.wp-roster {
  list-style: none;
  margin: clamp(1.8rem, 5vh, 3rem) 0 0;
  padding: 0;
  border-top: 1px solid var(--pw-rule);
  max-width: 46rem;
}

.wp-roster__row { border-bottom: 1px solid var(--pw-rule); }

.wp-roster__link {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: 0.4rem 1rem;
  padding: 0.85rem 0.2rem;
  text-decoration: none;
  color: var(--pw-bone);
  transition: background-color 160ms ease, padding-left 160ms ease;
}

.wp-roster__link:hover,
.wp-roster__link:focus-visible {
  background: rgba(168, 207, 214, 0.06);
  padding-left: 0.7rem;
}

.wp-roster__index {
  font-family: var(--pw-mono);
  font-size: 0.7rem;
  color: var(--pw-bone-faint);
  letter-spacing: 0.12em;
}

.wp-roster__title {
  font-family: var(--pw-body);
  font-weight: 500;
  font-size: clamp(1rem, 2vw, 1.2rem);
  letter-spacing: 0.01em;
}

/* The blurb rides under the title, in the same column. */
.wp-roster__blurb {
  grid-column: 2;
  font-family: var(--pw-body);
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--pw-bone-faint);
}

.wp-roster__status {
  font-family: var(--pw-mono);
  font-size: 0.6rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--pw-brass-dim);
  color: var(--pw-bone-dim);
  white-space: nowrap;
}

.wp-roster__status--live    { color: var(--wp-sage);  border-color: rgba(127, 181, 155, 0.5); }
.wp-roster__status--soon    { color: var(--wp-frost); border-color: rgba(168, 207, 214, 0.45); }
.wp-roster__status--planned { color: var(--pw-bone-faint); }

/* ------------------------------------------------------------- contact */

.wp-contact {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  align-items: baseline;
  margin-top: clamp(1.6rem, 5vh, 2.6rem);
  font-family: var(--pw-mono);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
}

.wp-contact__label { color: var(--pw-bone-faint); text-transform: uppercase; }
.wp-contact__value { color: var(--pw-bone-dim); text-decoration: none; }
.wp-contact__value:hover,
.wp-contact__value:focus-visible { color: var(--wp-frost); }

/* ---------------------------------------------------------- the tuner */

/* Only ever rendered when the URL carries ?tune. It is a build tool that
   happens to live in the page: drag sliders until the fog is right, hit
   Copy, paste the attributes onto <div class="wp-stage"> and delete the
   query string. Nothing about it ships to a normal visitor. */
.wp-tuner {
  position: fixed;
  right: clamp(0.6rem, 2vw, 1.2rem);
  /* Clear of the jay switch, which sits in the same corner. */
  bottom: 3.6rem;
  z-index: 80;
  width: min(17rem, calc(100vw - 1.6rem));
  padding: 0.85rem 0.9rem 0.7rem;
  background: rgba(6, 12, 14, 0.86);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border: 1px solid var(--pw-rule);
  font-family: var(--pw-mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  color: var(--pw-bone-dim);
}

.wp-tuner h2 {
  margin: 0 0 0.6rem;
  font-size: 0.62rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--wp-frost);
}

.wp-tuner label { display: block; margin-bottom: 0.55rem; }
.wp-tuner label span { display: flex; justify-content: space-between; margin-bottom: 0.2rem; }
.wp-tuner input[type="range"] { width: 100%; accent-color: var(--wp-frost); }

.wp-tuner button {
  width: 100%;
  margin-top: 0.3rem;
  padding: 0.4rem;
  font: inherit;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--wp-frost);
  background: transparent;
  border: 1px solid var(--pw-brass-dim);
  cursor: pointer;
}

.wp-tuner button:hover { background: var(--wp-frost); color: var(--pw-void); }
.wp-tuner code { display: block; margin-top: 0.5rem; color: var(--pw-bone-faint); word-break: break-all; line-height: 1.5; }

/* ------------------------------------------------------------ fallback */

/* If WebGL is missing entirely, pines.js adds this class and the fog
   becomes two slowly-sliding CSS gradients. Not the same thing, but the
   page still reads as a foggy forest instead of a bare video. */
.wp-nogl .wp-fog { display: none; }

.wp-nogl .wp-stage::after,
.wp-nogl .wp-stage::before {
  content: "";
  position: absolute;
  left: -30%;
  right: -30%;
  height: 70%;
  pointer-events: none;
  background: radial-gradient(60% 100% at 30% 60%, rgba(190, 215, 222, 0.16), transparent 70%),
              radial-gradient(50% 100% at 75% 40%, rgba(150, 180, 190, 0.13), transparent 70%);
  filter: blur(22px);
}

.wp-nogl .wp-stage::before { bottom: -6%; animation: wp-drift 68s linear infinite; }
.wp-nogl .wp-stage::after  { bottom: 22%; opacity: 0.6; animation: wp-drift 104s linear infinite reverse; }

@keyframes wp-drift {
  from { transform: translate3d(-8%, 0, 0); }
  to   { transform: translate3d(8%, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .wp-plate { transform: scale(1.04); }
  .wp-nogl .wp-stage::before,
  .wp-nogl .wp-stage::after { animation: none; }
  /* Still lit, still there — just not moving. */
  .wp-mark { transform: none; }
  .wp-mark__img { animation: none; }
}

/* ------------------------------------------------------------- narrow */

/* On a phone the text spans the full width, so a left-weighted scrim
   protects nothing. Swap it for a bottom-up one. */
@media (max-width: 48rem) {
  .wp-vignette {
    background:
      linear-gradient(to top, rgba(2, 6, 8, 0.86) 0%, rgba(2, 6, 8, 0.52) 34%, transparent 68%),
      radial-gradient(130% 80% at 50% 30%, transparent 40%, rgba(2, 5, 7, 0.6) 100%),
      linear-gradient(to bottom, rgba(2, 5, 7, 0.55) 0%, transparent 24%);
  }
}

/* Under 80rem "whatever is left after the title" is under 220px and still
   shrinking, so the mark moves rather than dwindling into a smudge.

   It goes to the band between the strip and the eyebrow, which is dead space
   at every width below this — still the top RIGHT, so it is the same mark in
   the same corner of the eye.

   THAT BAND IS SET BY VIEWPORT HEIGHT, NOT WIDTH. The hero is bottom-aligned
   in a full-height frame, so a short window squeezes the gap from above and
   below at once: measured across ten sizes it runs from 42% of the viewport
   height on a tall tablet down to 20% on a 640px-high laptop. Hence the third
   term. 23vh is that 20% floor expressed as a WIDTH through the art's 1.376
   aspect, with room left over for the gap — which is why a height limit is
   doing its work in a width property. Drop it and the mark lands on the
   eyebrow on any short screen. */
@media (max-width: 79.99rem) {
  .wp-mark {
    top: calc(var(--pw-chrome-h) + clamp(0.75rem, 2.5vh, 2rem));
    right: clamp(1.25rem, 3.4vw, 3.5rem);
    bottom: auto;
    width: min(42vw, 18rem, 23vh);
    --wp-mark-travel-x: 9px;
    --wp-mark-travel-y: 6px;
  }
}

@media (max-width: 34rem) {
  .wp-roster__link { grid-template-columns: auto 1fr; }
  .wp-roster__status { grid-column: 2; justify-self: start; margin-top: 0.3rem; }
}
