/* ==========================================================================
   stevekelly.io — enhancement layer (PREVIEW)
   Progressive enhancement over the base site. Everything here is additive and
   guarded: if JS is off, GSAP fails to load, or the visitor asked for reduced
   motion, the base site renders exactly as before. Nothing below changes
   layout or content — only motion, texture, and the album "record" moment.
   Eases stay in the site's vocabulary: long, opacity-led, no bounce.
   ========================================================================== */

/* Lenis smooth-scroll baseline (only active once .js-enhanced is set). */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }

/* Guard against any protruding disc ever creating page-level side-scroll.
   Discs swing within their section's wrap, so clip there (not on the disc). */
.js-enhanced .listen .wrap,
.js-enhanced .merch .wrap { overflow-x: clip; }

/* --------------------------------------------------------------------------
   1. HERO NAME — two-line mask draw-in
   The words rise out of a clip. We take over ONLY when enhanced, so the
   meticulous album-cover kerning inside .hn-l2 is never touched by a splitter.
   -------------------------------------------------------------------------- */
.js-enhanced .hero h1 .hn-l1,
.js-enhanced .hero h1 .hn-l2 {
  /* Each word gets its own clip window; GSAP animates an inner transform.
     overflow:clip keeps descenders honest without adding a scroll box. */
  overflow: clip;
  will-change: transform;
}
.js-enhanced .hero h1 .hn-l1 > .draw-in { display: block; }
/* KELLY's wrapper has to carry the flex context through, or the letters lose
   their explicit per-pair margins the moment JS wraps the line for the mask
   reveal (no space-between/width here anymore — see site.css for why). */
.js-enhanced .hero h1 .hn-l2 > .draw-in {
  display: flex;
  width: 100%; /* inherit the fixed 2.479em from .hn-l2 so Y still lands flush */
}
/* Hold the name hidden until GSAP takes over (JS sets the yPercent start and
   flips this back). visibility is used, not a transform, so GSAP never absorbs
   it into y. .js-enhanced only exists once JS is live, so no-JS/reduced-motion
   visitors keep the name fully visible. */
.js-enhanced .hero h1 { visibility: hidden; }

/* Section titles: line-mask reveal wrapper (added by JS via SplitText). */
.js-enhanced .split-line-mask { overflow: clip; display: block; }

/* --------------------------------------------------------------------------
   2. FILM GRAIN — cinematic texture over the dark moments only
   A single fixed, pointer-transparent turbulence layer. ~0 network cost
   (inline SVG data URI), GPU-cheap, and it never sits over the linen body.
   -------------------------------------------------------------------------- */
.grain {
  position: absolute;
  inset: 0;
  z-index: 3; /* above scrim (1) + content sits at 2/higher where needed */
  pointer-events: none;
  opacity: 0;                 /* faded in by JS so no-JS visitors see nothing */
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 140px 140px;
}
.hero .grain { z-index: 3; }
.live-band .grain { z-index: 3; opacity: 0; }
/* Hero content + live content must stay above the grain. */
.hero .hero-content { z-index: 4; }
.live-band .live-content { z-index: 4; position: relative; }

@keyframes grain-drift {
  0%   { transform: translate(0, 0); }
  20%  { transform: translate(-3%, 2%); }
  40%  { transform: translate(2%, -3%); }
  60%  { transform: translate(-2%, -2%); }
  80%  { transform: translate(3%, 2%); }
  100% { transform: translate(0, 0); }
}
.js-enhanced .grain.is-live { animation: grain-drift 1.1s steps(4) infinite; }

/* --------------------------------------------------------------------------
   3. THE RECORD — vinyl slides out of the sleeve on hover (pointer + wide only)
   Built as a pure-CSS disc (grooves via repeating-radial-gradient) with the
   real album art as the centre label. GSAP drives the slide + spin.
   -------------------------------------------------------------------------- */
@media (hover: hover) and (min-width: 900px) {
  /* Shared "disc reveal": a real record / CD image sits behind its cover, spins
     out on hover, returns to present in front, and slips back on leave. The
     owning card lifts its stacking (set in JS) so the swung-out disc floats over
     its neighbours; opening the box clip would square the cover's corners, so we
     move the rounding onto the cover image itself. */
  .js-enhanced .has-disc { position: relative; overflow: visible; }

  /* Album: open the sleeve, keep the cover rounded + the badge on top.
     :not(.disc) so this targets only the cover, never the injected disc img. */
  .js-enhanced .album-art.has-disc > img:not(.disc) {
    position: relative; z-index: 2; border-radius: var(--radius);
  }
  .js-enhanced .album-art.has-disc .album-art-badge { z-index: 6; }

  /* CD merch card: open the case, round the image box's top corners so the
     square cover doesn't poke past the card's rounded top. */
  .js-enhanced .merch-card.has-disc .merch-img {
    overflow: visible;
    border-radius: var(--radius) var(--radius) 0 0;
  }
  .js-enhanced .merch-card.has-disc .merch-img > img:not(.disc) {
    position: relative; z-index: 2;
    border-radius: var(--radius) var(--radius) 0 0;
  }

  /* The disc — the real transparent PNG. GSAP owns transform + z-index; the
     drop-shadow is UNCONDITIONAL (no .is-out toggle) — that's what fixes the
     glitch. At rest the disc sits exactly behind the opaque cover (same
     bounding box, lower z-index), so the shadow is entirely hidden with it;
     the instant any part of the disc swings clear of the cover's edge, that
     part — and only that part — becomes visible, shadow included, purely as
     a natural consequence of the z-stacking. No JS-driven pop, no delay: the
     shadow reveals itself exactly as fast as the disc physically does. */
  .js-enhanced .disc {
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    object-fit: contain;
    pointer-events: none;
    filter: drop-shadow(0 16px 34px rgba(0, 0, 0, 0.30));
    z-index: 1;               /* behind the cover until it presents */
    will-change: transform;
  }
  /* A garment is not a disc: it keeps the same inset as the card's own shirt so
     the two read as the same object photographed twice, not two different sizes. */
  .js-enhanced .disc--garment { padding: 6% 8%; }
}

/* --------------------------------------------------------------------------
   Apparel merch card — crossfade to a second image on hover. Pure CSS, works
   without JS; both images are clipped to the square frame by the base rules.
   -------------------------------------------------------------------------- */
.merch-img--swap { position: relative; }
/* The garment shots are portrait inside a square frame, so `cover` (the base
   rule) cropped the shoulders off. `contain` + a little inset shows the whole
   shirt, letterboxed against .merch-img's own background (var(--surface),
   set in site.css) rather than a second fill on the base cover image itself
   — it used to carry its own var(--bg) here, a slightly different off-white
   than the container's var(--surface): two overlapping near-white fills
   created a faint seam at the padding edge that read as an unintended
   shadow around a rectangle, not the shirt. Transparent removes it — the
   container's own single background shows through consistently instead.
   :not(.disc) — the injected hover garment is sized/positioned entirely by
   GSAP (see discReveal), so it must NOT pick up this padding, only the
   static base cover gets the inset. */
.merch-img--swap img:not(.disc) {
  object-fit: contain;
  padding: 6% 8%;
  background: transparent;
}
.merch-img--swap .disc { background: transparent; }
/* The hover reveal itself is driven by enhance.js (see discReveal) so the
   apparel card uses the same swing/settle gesture as the record and CD cards.
   Without JS the card simply shows the first shirt — no broken half-state. */
.merch-img--swap .merch-swap-hover {
  position: absolute;
  inset: 0;
  opacity: 0;         /* only ever visible if JS never runs to remove it */
}

/* --------------------------------------------------------------------------
   Crossfade product cards (Coffee, Cruiser Kit) — front/back or label/scene,
   both real opaque photos, so this is a plain CSS opacity swap on hover, not
   a JS-driven gesture (nothing to desync, nothing to gate with the hover-
   intent delay the disc cards need — nothing swings, so a same-scroll flash
   is a no-op, not a jarring animation like the disc cards had).
   -------------------------------------------------------------------------- */
.merch-img--crossfade { position: relative; }
.merch-img--crossfade img {
  object-fit: contain;
  padding: 6% 8%;
  background: transparent;
}
.merch-img--crossfade .merch-crossfade-hover {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.5s var(--ease);
}
.merch-card:hover .merch-crossfade-hover,
.merch-card:focus-within .merch-crossfade-hover {
  opacity: 1;
}

/* --------------------------------------------------------------------------
   Respect the visitor. Reduced motion => none of the above motion runs.
   (JS also bails, but this is the belt to that suspenders.)
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .js-enhanced .grain.is-live { animation: none; }
  .js-enhanced .hero h1 .hn-l1 > .draw-in,
  .js-enhanced .hero h1 .hn-l2 > .draw-in { transform: none; }
  /* enhance.js bails entirely under reduced motion, so no disc is ever built —
     this only covers a disc left in the DOM if the query flips after load. */
  .disc { display: none; }
}
