/* ============================================================
   CAPELY: 01 hero
   Two columns at desktop (cape stage left, copy right), stacking to one
   column on mobile with the stage first in source order so it lands above
   the copy. Everything under .hero-section is scoped to that class, never
   the bare #hero id, so nothing here can bleed onto another section.
   ============================================================ */

.hero-section {
  position: relative;
  min-height: min(100vh, 860px);
  display: flex;
  align-items: center;
  /* A whisper of the current cape's own colour behind the whole section, not
     just behind the cape stage, so the hero reads as belonging to that cape
     and the copy gets a little more separation from a flat background.
     --cape-accent is set once by js/01-hero.js on this same element (the
     design contract's per-product decoration carve-out); this rule just reads
     that same custom property, it does not add a second mechanism. This wash
     is background, not light: the cape's glow comes off the cape itself, see
     .hero-stage-bloom below. */
  background: radial-gradient(ellipse 65% 55% at 50% 40%, color-mix(in srgb, var(--cape-accent, var(--accent)) 7%, var(--base)) 0%, var(--base) 72%);
  transition: background var(--t-slow) var(--e-out);
  /* The blend group for .hero-stage-bloom's screen blend, and the reason it
     is HERE rather than on .hero-stage.

     Screen only cancels the cape's dark pixels against a real backdrop. On
     .hero-stage, which has no background of its own, the blend group's
     backdrop was transparent, screen degraded to an ordinary paint and the
     dark half of the artwork came back as a grey haze. With no isolation
     anywhere the blend is correct but its group becomes the page, so the
     compositor has the whole document as a backdrop to keep around while the
     hero scrolls. Isolating the section gives the blend this element's own
     opaque wash to work against and stops the group there. */
  isolation: isolate;
}

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(28px, 5vw, 64px);
  width: 100%;
}

@media (max-width: 900px) {
  .hero-grid { grid-template-columns: minmax(0, 1fr); gap: 44px; }
}

/* ---------- cape stage ---------- */
.hero-stage-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  min-width: 0;
  /* The swipe surface (js/01-hero.js). pan-y hands vertical scrolling to the
     browser unconditionally, so the page still scrolls on the compositor
     through the hero and the JS never has to preventDefault a touch. */
  touch-action: pan-y;
}

.hero-stage {
  position: relative;
  width: min(100%, 460px);
  aspect-ratio: 4 / 5;
  display: grid;
  place-items: center;
  /* No overflow:hidden here on purpose. .hero-stage-bloom below is exactly
     this box's size and cannot exceed it, so there is nothing to clip. An
     earlier glow did bleed past it (inset: -12%) and the clip that hid it is
     what showed up as a hard rectangle behind the cape. If light ever spills
     past this box again, shrink the light rather than reaching back for this
     clip: removing it was a deliberate fix, not an oversight. */
  /* Deliberately NOT isolated: this element has no background, so isolating
     here would hand .hero-stage-bloom's screen blend a transparent backdrop
     and turn it back into an ordinary paint. The blend group is .hero-section,
     which owns the opaque wash the light falls on. */
}

/* There is no coloured ellipse behind the cape any more.

   One used to sit here: a radial gradient in the cape's accent, blurred 36px.
   It was the single biggest reason the hero read as "a glow around the cape"
   rather than a glowing cape, because a blob of flat colour behind an object
   is exactly that, and it stayed put while the cape moved. The cape's light
   now comes only from the cape: an emissive map on the material
   (js/cape-viewer.js) plus .hero-stage-bloom, which is a copy of the rendered
   frame. --cape-accent still drives the section's ambient wash and the
   headline, which are typography and background, not light. */

/* The cape's own light: a copy of the frame js/cape-viewer.js just rendered,
   painted into this canvas every frame at 110px wide.

   It covers the stage exactly, at scale 1, because it is a copy of the whole
   frame. That is the entire point. The old version was a flat, face on crop
   of the texture at scale 1.34 with a 30px blur, and it had to be that vague:
   a face on crop and a cape held at three quarters do not share a silhouette,
   so any legible version of it visibly disagreed with the cape it was meant to
   be coming off. Blurred into a cloud the disagreement was hidden, and what
   was left was a coloured haze sitting around the cape, which is what the
   owner kept asking to be fixed. A copy of the real frame agrees with the cape
   by construction, at its angle, at its position, drifting with its float, so
   the blur can come right down and the light stays on the artwork.

   The blur is small because the copy is already soft: 110px stretched across a
   ~460px stage is a 4x upscale the compositor smooths for free. Screened, so
   only the bright pixels add light and the dark half of the cape adds nothing.
   Stays BEHIND the crisp canvas (z-index 0 against 1): over the top, a blurred
   copy would soften the pixel art's edges, and blocky edges are the product. */
.hero-stage-bloom {
  position: absolute;
  z-index: 0;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: none;
  filter: blur(7px) saturate(1.25) brightness(1.12);
  opacity: 1;
  mix-blend-mode: screen;
  pointer-events: none;
  transition: opacity var(--t) var(--e-out);
}
@media (prefers-reduced-motion: reduce) {
  .hero-stage-bloom { transition: none; }
}

.hero-stage-canvas {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
  min-width: 0;
}

.hero-stage-canvas canvas {
  display: block;
  width: 100% !important;
  height: 100% !important;
  transition: opacity var(--t) var(--e-out);
  /* No drop-shadow here on purpose. It tints only the silhouette, so it reads
     as an outline traced around the cape rather than light coming off it. The
     cape glows from within instead: js/cape-viewer.js gives the material an
     emissiveMap of the cape's own texture, so the artwork's own colours are
     what glow, and .hero-stage-bloom carries that same light a few pixels off
     the edges. */
}

.hero-stage-fallback {
  width: 100%;
  height: 100%;
  object-fit: contain;
  image-rendering: pixelated;
}

/* A gentle idle float for the 2D (no-WebGL) fallback image, since it has no
   render loop of its own to animate the float the way the 3D scene does.
   Matched to the 3D float rather than invented separately: js/cape-viewer.js
   runs a sine at 0.5 rad/s (a 12.6s round trip) with an amplitude that lands
   near 11px on a 460px stage, so the fallback travels the same distance at the
   same rate and the two paths cannot read as different products.
   Transform only, per contract §4, and fully killed under reduced motion
   rather than sped up or clamped. */
@keyframes hero-cape-float {
  0%, 100% { transform: translateY(11px); }
  50%      { transform: translateY(-11px); }
}
.hero-stage-canvas canvas.cv-2d { animation: hero-cape-float 12.6s var(--e-in-out) infinite; }
@media (prefers-reduced-motion: reduce) {
  .hero-stage-canvas canvas.cv-2d { animation: none; }
}

.hero-dots {
  display: flex;
  align-items: center;
  gap: 4px;
}
.hero-dot {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-pill);
}
.hero-dot::before {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--surface-3);
  border: 1px solid var(--hairline);
  transition: background var(--t-fast), border-color var(--t-fast), transform var(--t-fast);
}
.hero-dot:hover::before { border-color: var(--hairline-strong); }
.hero-dot[aria-current="true"]::before {
  background: var(--accent);
  border-color: var(--accent);
  transform: scale(1.35);
}

/* ---------- copy column ---------- */
.hero-copy { min-width: 0; }

.hero-feature {
  margin: 14px 0 20px;
  /* Eased and long enough to read as a crossfade rather than a cut. The copy
     also lifts slightly, so the swap has direction instead of just blinking. */
  transition: opacity var(--t) var(--e-out), transform var(--t) var(--e-out);
}
.hero-feature.is-fading { opacity: 0; transform: translateY(6px); }
@media (prefers-reduced-motion: reduce) {
  .hero-feature { transition: none; }
  .hero-feature.is-fading { transform: none; }
}

.hero-feature-name {
  font-size: clamp(38px, 5.4vw, 68px);
  font-weight: 800;
  line-height: 1.08;
  /* Reserves the TALLEST of the rotating titles, not the shortest, and sits
     the text at the bottom of that box.

     The carousel cycles three titles of different lengths: in the two-column
     layout "The Twitchcon 2026 Crafter's Cape" takes three lines and the other
     two take two. Reserving two lines meant the price, the blurb and the CTAs
     moved 31px every time that cape came round; reserving three without the
     bottom alignment just moves the same slack to where the old bug was, as a
     hole between the name and the price. Bottom-aligned, the slack lands above
     the headline in the hero's own whitespace and NOTHING below the title ever
     moves. The three spans live inside a single .hero-name-lines wrapper because
     every ELEMENT child of a grid container becomes its own grid item: without
     it, pre/key/post each took their own row and the headline stopped wrapping
     as one paragraph (measured: 7 line boxes instead of 3). With the wrapper
     there is exactly one grid item, so the text inside wraps normally and a
     line can still break between "Twitchcon" and "Crafter's". */
  min-height: 3.25em;
  display: grid;
  align-content: end;
  margin-bottom: 10px;
  overflow-wrap: break-word;
  /* The title itself stays on the ordinary heading colour. Only the cape's own
     name is tinted, which is the treatment Capeify gives its headline: the
     surrounding words read as copy, the name reads as the product. */
  color: var(--text);
}

/* One column below 900, so the copy has the full width and the longest title
   fits in two lines (measured at 900, 560 and 390: 2/2/2). Reserving three
   would only push the buttons further down a phone screen. This has to sit
   AFTER the rule it overrides, not up with the other 900px rules: same
   specificity, so source order decides and the earlier one silently lost. */
@media (max-width: 900px) {
  .hero-feature-name { min-height: 2.2em; }
}

/* The single grid item inside the h1 (see above). A wrapper only: all the type
   is set on .hero-feature-name and inherited through this. */
.hero-name-lines { display: block; }

/* Only this span carries the cape's colour, driven off the same --cape-accent
   custom property js/01-hero.js sets on .hero-section (see the section's own
   background wash above). Legible on its own: every accent in data/products.json clears the
   3:1 floor for text this large as plain solid colour, so the shadow layers
   are pure decoration on an already-readable colour, never a substitute. */
.hero-name-key {
  color: var(--cape-accent, var(--accent));
  /* Radii cut from 60/120px, and text-shadow is no longer transitioned. A
     shadow that wide is repainted over a large area on every frame of the
     transition, and it was animating during the carousel swap, on the headline,
     while the momentum loop was also running. The colour still crossfades;
     nothing visible was lost. */
  text-shadow:
    0 0 42px color-mix(in srgb, var(--cape-accent, var(--accent)) 40%, transparent),
    0 0 80px color-mix(in srgb, var(--cape-accent, var(--accent)) 15%, transparent),
    0 4px 20px color-mix(in srgb, var(--base-deep) 55%, transparent);
  transition: color var(--t-fast) var(--e-out);
}

.hero-feature-price {
  font-family: 'Outfit', sans-serif;
  font-size: clamp(26px, 3vw, 38px);
  font-weight: 800;
  letter-spacing: -.01em;
  color: var(--accent);
  margin-bottom: 10px;
}

.hero-feature-blurb {
  /* Was --text-mute (4.6:1). This sits directly under the headline as the
     main supporting line for whichever cape is featured, so it was one of
     the two hero copy blocks reading as faded into the background; moved up
     a step to --text-dim (8.2:1). */
  color: var(--text-dim);
  font-size: 14.5px;
  line-height: 1.6;
  max-width: 48ch;
  min-height: 2.6em;
  overflow-wrap: break-word;
}

.hero-subcopy {
  /* Was --text-dim (8.2:1, already AA). Moved up to full --text so the hero's
     secondary line holds its own against the bigger, glowing headline above
     it and the section's own background wash, rather than reading as faded. */
  color: var(--text);
  font-size: 15.5px;
  line-height: 1.65;
  max-width: 46ch;
  margin-bottom: 30px;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-bottom: 24px;
}

/* ---------- touch and phones ----------
   The swipe hint is shown only where the gesture exists. A mouse user is told
   to swipe by nothing, and the dots remain the control on every device. */
.hero-swipe-hint { display: none; }
@media (hover: none) and (pointer: coarse) {
  .hero-swipe-hint {
    display: block;
    margin-top: -8px;
    color: var(--text-mute);
    font-size: 12px;
  }
}

@media (max-width: 640px) {
  /* On a phone the hero is a stacked column that already runs past the fold,
     so a 100vh floor only adds dead space under it: it centred the content in
     a taller box and left about 150px of nothing above the shop. Height comes
     from the content instead. The top padding is explicit because .section's
     72px would put the copy under the floating nav, which the centring was
     quietly handling before. */
  .hero-section {
    min-height: 0;
    padding-top: calc(var(--nav-h) + 40px);
    padding-bottom: 60px;
  }
  /* The stage was 354px wide on a 390px phone, which pushed the cape's own
     name and price most of a screen down the page. Smaller stage, same cape. */
  .hero-stage { width: min(100%, 300px); }
  .hero-stage-col { gap: 16px; }
  .hero-grid { gap: 32px; }
  /* Two 56px buttons wrapping to two rows at different widths reads as an
     accident; full width each is the phone convention and lines them up. */
  .hero-actions { gap: 10px; }
  .hero-actions .btn { flex: 1 1 100%; }
  /* 28px is under the 44px touch target floor, on the only control the hero
     has. The dot itself is unchanged; the button around it grew. */
  .hero-dot { width: 44px; height: 44px; }
  .hero-dots { gap: 0; }
}
