/* Greybox Phase 2 — cinematic dynamic hero (+ Phase 3 seamless trailer).
 * Consumes Phase 1 tokens (black / glass / amber). No red primary, no neon.
 * Backdrop: slow Ken Burns (100% → 106% + subtle drift). The trailer iframe
 * buffers invisibly (visibility:hidden layer) and crossfades above the still
 * only after the YouTube player confirms actual playback. Mute/unmute keeps
 * its existing styling. Respects prefers-reduced-motion. */

.gx-hero {
  position: relative;
  /* Paints ABOVE <main> (z-index 1): main::before + the shelf tuck live in
   * main's stacking context and would otherwise sit over the hero's action
   * row and swallow its clicks. Visuals are preserved because the blend was
   * designed continuous at the boundary (the hero's bottom tone and the
   * main::before ramp carry the same value there), and hit-testing falls
   * through the
   * hero's own transparent layers (see pointer-events rules below) so the
   * shelves underneath stay clickable. Navbar (40) / modal+player (50) stay
   * above the hero as before. */
  z-index: 2;
  /* Hero occupies ~96% of the viewport on desktop (svh/dvh-safe where
   * supported), ~84% on tablet and 68% on mobile (see media queries below).
   * Taller than the classic 90% so the composition reads cinematic and the
   * copy docks LOWER in frame — the bottom half is one long dissolve.
   * No min/max clamp so the spec holds on every screen size — the trailer
   * cover math in .gx-hero-trailer iframe depends on this exact height. */
  height: 96vh;
  height: 96svh;
  height: 96dvh;
  overflow: hidden;
  background: transparent;
  isolation: isolate;
}

/* Page ambient: the page's own BACKGROUND is the current hero artwork — one
 * fixed, viewport-sized, heavily blurred copy of the poster painted behind
 * every other layer (base body glass → this → hero media → main/footer).
 * FIXED rather than absolute on purpose: it does not move while the page
 * scrolls, so the backdrop stays perfectly still and only the cards slide
 * over it. js/hero.js keeps its source in sync with the still, so the whole
 * canvas re-colours automatically on every hero rotation — the background
 * changes with the title, not with the scroll position.
 * Because the hero above it is opaque, what actually shows is the canvas
 * around and below the hero: the band the hero's bottom dissolve melts into
 * is THIS colour, so the hand-off blends into the hero's own palette instead
 * of into a flat black plate.
 * The oversized box (-6% per side) is deliberate: detail.css scales this
 * layer to 0.94 when a modal opens, and the overscan keeps that recede from
 * ever exposing an edge. overflow:hidden + the overscanned image inside +
 * html/body overflow-x:clip means no horizontal scrollbar anywhere. */
html, body { overflow-x: clip; }
body { position: relative; }

.gx-page-ambient {
  position: fixed;
  inset: -6%;
  overflow: hidden;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 900ms ease;
}

.gx-page-ambient.is-visible { opacity: 1; }

/* The artwork itself: covers the oversized box, blurred until no detail is
 * recognisable (colour only, never a readable poster), oversaturated so the
 * hue survives both the blur and the scrim painted over it. A w342 TMDB
 * derivative is plenty (js/hero.js ambientUrlFrom). Painted once into its
 * own composited layer — static, no scroll or per-frame cost. */
.gx-page-ambient-img {
  position: absolute;
  inset: -8%;
  width: 116%;
  height: 116%;
  max-width: none;
  object-fit: cover;
  object-position: center 26%;
  filter: blur(90px) saturate(1.5) brightness(0.95);
  transform: scale(1.12);
  opacity: 1;
}

/* Accent light bleed — the SAME artwork painted as light instead of a
 * picture: oversaturated and screen-blended across the TOP 40vh of the
 * canvas, i.e. the band the opaque hero already covers. It lifts the hero's
 * own top edge from behind and deliberately never reaches the shelf rows,
 * so nothing bright blooms out from behind the front row of cards. It
 * re-lights on every hero change for free: js/hero.js keeps its src in sync
 * with #page-ambient-img. */
.gx-page-ambient-glow {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 40vh;
  max-width: none;
  object-fit: cover;
  object-position: center 26%;
  filter: blur(70px) saturate(1.9) brightness(1.1);
  mix-blend-mode: screen;
  opacity: 0.3;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
}

.gx-page-ambient-glass {
  position: absolute;
  inset: 0;
  /* Gradient only, deliberately NO backdrop-filter: this layer spans the
   * whole viewport behind the content, and a backdrop blur that large is
   * recomputed on every scroll frame (homepage jank). The #page-ambient-img
   * underneath is already a heavily blurred/saturated poster layer, so the
   * diffusion the blur used to add is preserved without the per-frame cost.
   *
   * One calm, uniform scrim — no bright pools anywhere. The poster copy
   * underneath carries all the colour (it IS the hero's colour theme); this
   * only holds card/text contrast on top of it, a touch heavier at the very
   * bottom so the footer keeps its depth while the mid-page stays visibly
   * tinted. The TOP stops are deliberately light (0.4/0.34, were 0.5/0.44):
   * the hero's light hand-off (main::before 0.58 ramp) lands on THIS layer,
   * so a light top is what lets the hero's blurred palette continue behind
   * the first shelves instead of dying into a dark band. */
  background: linear-gradient(to bottom,
    rgba(9, 11, 15, 0.4) 0%,
    rgba(9, 11, 15, 0.34) 34%,
    rgba(9, 11, 15, 0.48) 72%,
    rgba(9, 11, 15, 0.64) 100%);
}

/* Hero + page content paint above the ambient layer. */
main, footer { position: relative; z-index: 1; }

/* Hand-off continuation: a full-bleed dissolve strip at the top of <main>
 * that starts INSIDE the hero (negative margin pulls it up) and runs past
 * its bottom edge (extra height + matching negative bottom margin, zero net
 * layout shift). The hero paints above <main>, so the strip only ever shows
 * BELOW the hero boundary — and there it is continuous, not stepped: the
 * stop at exactly the boundary (270px into an 810px strip = 33.333%) carries
 * the same darkness the hero's own shade ends on, then the ramp eases back
 * out over the next ~540px. The colour underneath is the fixed poster
 * backdrop (see .gx-page-ambient), so this reads as the hero's colour
 * theme dimming away under the first shelves — no solid black plateau, no
 * rectangular band, no "dark void" between the hero and the content.
 * Content paints above the strip (z-index), so rows sit over the fade like
 * Netflix instead of leaving a void. Full-bleed (100vw breakout) so the
 * blend spans edge-to-edge even though <main> is a centered max-width
 * column. Harmless on non-hero routes: with no ambient art showing, the
 * wash eases over the base background and stays invisible.
 *
 * WHY THE UPPER 42.857% IS HELD AT ZERO: the hero's own layers are
 * translucent (shade 0.58 over a transparent poster/bg), so anything painted
 * UNDER the hero reads through it. A ramp above the boundary would stack
 * beneath the hero's shade and double-darken the hero's last 270px, while
 * the row just below the edge got only the single strip — that mismatch is
 * what draws a visible horizontal border between the hero poster and the
 * page. Holding the strip at zero until the boundary makes both sides
 * resolve to the identical tone: 0.58 shade over the backdrop above = 0.58
 * strip over the backdrop below, so the edge vanishes instead of being
 * drawn.
 *
 * LENGTH + HOLD (the seam-killer): the strip below the boundary used to
 * ease from 0.58 to 0 within ~360px and lifted within the first ~45px —
 * a quick brightening that read as "poster, then page" even when the tones
 * matched at the edge row. The strip now HOLDS the boundary tone for the
 * first ~65px below the edge and eases out over a full 540px, so the
 * darkened band under the hero feels like the poster's own shadow
 * continuing into the page rather than a separate layer's edge. Same
 * 0.58 boundary contract as before, just a longer melt (was 360px). */
main::before {
  content: "";
  display: block;
  position: relative;
  z-index: 0;
  width: 100vw;
  height: 810px;
  margin-top: -270px;
  margin-bottom: -540px;
  margin-left: calc(-50vw + 50%);
  background: linear-gradient(to bottom,
    rgba(11, 13, 18, 0) 0%,
    rgba(11, 13, 18, 0) 33.333%,
    rgba(11, 13, 18, 0.58) 33.333%,
    rgba(11, 13, 18, 0.56) 40%,
    rgba(11, 13, 18, 0.48) 52%,
    rgba(11, 13, 18, 0.36) 64%,
    rgba(11, 13, 18, 0.24) 76%,
    rgba(11, 13, 18, 0.12) 87%,
    rgba(11, 13, 18, 0.04) 95%,
    rgba(11, 13, 18, 0) 100%);
  pointer-events: none;
}

/* Content overlap: small tuck of the first shelf over the hero's bottom
 * fade (no black void). Kept deliberately shallow so the overlap NEVER
 * reaches the hero actions: buttons sit ~116px above the hero bottom while
 * the shelves overlap only ~40px net, leaving ~76px of breathing room.
 * Combined with the pointer-events pass-through below, hero buttons stay
 * clickable even on font/zoom variation. */
#home-sections:empty {
  display: none;
  margin: 0;
  padding: 0;
}

#home-sections:not(:empty) {
  position: relative;
  z-index: 1;
  margin-top: -60px;
  padding-bottom: 8px;
  /* Empty gutter above the first shelf must not swallow hero clicks:
   * only real shelf content is interactive, transparent gaps pass through. */
  pointer-events: none;
}

#home-sections:not(:empty) .gx-shelf,
#home-sections:not(:empty) a,
#home-sections:not(:empty) button {
  pointer-events: auto;
}

/* Tighten the first shelf so there is no empty band between the hero fade
 * and "Popular Movies": the tuck above already overlaps the hero, so the
 * shelf needs only a small breathing gap, not a full section margin. */
#home-sections:not(:empty) .gx-shelf:first-child {
  margin-top: 0.75rem;
}

/* Homepage discover hides the legacy list shell entirely (not just its
 * children) so its mt-8 margin can never leave an empty strip above the
 * first shelf. Restored automatically when leaving home (see pages.js). */
main > section.gx-shell-hidden {
  display: none;
}

#notice,
#collection-head {
  position: relative;
  z-index: 2;
}

main > section {
  position: relative;
  z-index: 1;
}

/* Reserve full space before art loads: grey glass placeholder, never a broken icon.
 * The whole background stack is click-through: with the hero now painting
 * above <main>, any pointer-events:auto layer here would swallow shelf
 * clicks in the overlap tuck AND sit between the buttons and nothing — so
 * every non-interactive hero media layer passes clicks straight through and
 * only .gx-hero-content buttons (pointer-events:auto) ever capture them. */
.gx-hero-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* Neutral studio base + a tinted rim in the top corner: the very first
   * pixels behind the artwork already agree with the hero's own palette
   * (--gx-hero-tint, js/hero.js), so nothing greys out at the edges.
   * It then LOSES opacity toward the bottom edge and ends fully
   * transparent, so the fixed poster backdrop underneath shows through the
   * lower part of the hero: the bottom dissolve lands on the hero's own
   * colour instead of on a flat black plate, which is what makes the hand-off
   * to the page read as one continuous blend. */
  background:
    radial-gradient(70% 50% at 50% 20%, rgba(255, 255, 255, 0.06), transparent 70%),
    radial-gradient(52% 38% at 80% 8%, rgba(var(--gx-hero-tint), 0.2), transparent 72%),
    linear-gradient(to bottom,
      #333842 0%,
      #1b1f27 26%,
      rgba(16, 19, 25, 0.94) 52%,
      rgba(11, 13, 18, 0.7) 74%,
      rgba(11, 13, 18, 0.34) 88%,
      rgba(11, 13, 18, 0) 100%);
}

.gx-hero-ambient,
.gx-hero-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Subtle artwork-derived atmosphere. Static blur — no canvas, no realtime
 * cost. It rides the still's long dissolve and is EMPTY at the hero's
 * bottom edge, so the boundary cuts through nothing but the 0.58 shade
 * over the fixed page backdrop (identical above and below — no step).
 * Its job is mid-frame colour: it keeps the poster's palette alive behind
 * the copy zone while the still itself has already faded to ~0.4 alpha,
 * and it hands its colour over to the page backdrop (same artwork, see
 * .gx-page-ambient) before it disappears. */
.gx-hero-ambient {
  object-fit: cover;
  object-position: center 20%;
  filter: blur(70px) saturate(1.15);
  opacity: 0;
  transform: scale(1.12);
  transition: opacity 900ms ease;
  /* Dissolves on the same long ramp as the still and, critically, reaches
   * ZERO at the hero's REAL bottom edge (94.6% of the scaled box — see the
   * NOTE below). An earlier revision deliberately carried residual colour
   * past the edge; that residue is exactly what drew a faint line on
   * saturated posters, because the leftover blur (0.36 base x ~0.35 mask
   * alpha) vanished in one pixel at the overflow:hidden cut while the page
   * below never had it. Zero-at-edge means every layer the boundary cuts
   * through is already empty there: the edge composites nothing but the
   * 0.58 shade over the fixed page backdrop, identical on both sides.
   * NOTE: the mask lives in the element's local box, and transform: scale(1.12)
   * pushes it 6% past each edge — so "transparent 96%" lands just before the
   * hero's real bottom edge (94.6% of the scaled box) and no colour survives
   * to be cut. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 58%, rgba(0, 0, 0, 0.58) 78%, rgba(0, 0, 0, 0.24) 90%, rgba(0, 0, 0, 0.06) 94%, transparent 96%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 58%, rgba(0, 0, 0, 0.58) 78%, rgba(0, 0, 0, 0.24) 90%, rgba(0, 0, 0, 0.06) 94%, transparent 96%);
}

.gx-hero-img {
  object-fit: cover;
  object-position: center 20%;
  opacity: 0;
  transition: opacity 900ms ease;
  /* Cinematic bottom dissolve: the artwork melts over the ENTIRE lower half
   * of the frame instead of cutting in the last few pixels. This was the
   * "visible image border" bug: the old feather held the poster opaque to
   * 70% and only then dropped it, and because .gx-hero-shade also dims that
   * band, the PERCEIVED detail-loss happened in the last ~6% — roughly a
   * 65px window on a tall viewport. Saturated posters (red/pink themes)
   * drew a clear horizontal line there: recognisable detail above, plain
   * page colour below, two obvious layers.
   * The new ramp takes the poster's alpha down gently from 48% so detail
   * gradually surrenders to colour across ~300px while the shade's scrim
   * keeps copy contrast. Fully transparent AT 100% (never an earlier stop)
   * so the last pixel row carries zero artwork — above the boundary the
   * reader sees the fixed page backdrop through the hero, and below it the
   * same backdrop under main::before's strip: one continuous colour field,
   * no line. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 48%, rgba(0, 0, 0, 0.86) 60%, rgba(0, 0, 0, 0.62) 72%, rgba(0, 0, 0, 0.4) 82%, rgba(0, 0, 0, 0.22) 90%, rgba(0, 0, 0, 0.08) 96%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 48%, rgba(0, 0, 0, 0.86) 60%, rgba(0, 0, 0, 0.62) 72%, rgba(0, 0, 0, 0.4) 82%, rgba(0, 0, 0, 0.22) 90%, rgba(0, 0, 0, 0.08) 96%, transparent 100%);
}

.gx-hero.is-ready .gx-hero-ambient { opacity: 0.36; }
.gx-hero.is-ready .gx-hero-img { opacity: 1; }

.gx-hero.is-ready .gx-hero-img {
  animation: gx-kenburns 24s ease-out forwards;
}

@keyframes gx-kenburns {
  from { transform: scale(1) translate(0, 0); }
  to { transform: scale(1.06) translate(-0.6%, 0.6%); }
}

/* Perf: no Ken Burns on the mobile band (<=859px). The hero still is a
 * full-viewport masked layer; animating it for 24s keeps a huge composited
 * layer churning on weak Android GPUs for the whole hero dwell. Static
 * poster on phones — the opacity fade-in reveal still runs. Desktop
 * (>=860px) keeps the slow zoom. */
@media (max-width: 859px) {
  .gx-hero.is-ready .gx-hero-img { animation: none; }
}

/* Trailer-playing state: the still fades out underneath the video so the
 * poster can never ghost through the trailer's bottom feather blend. Both
 * layers share the same dissolve mask (opaque to 50%, feathered out below),
 * so with the still left at opacity 1 the poster's detail showed through
 * the semi-transparent part of the video as a faint translucent shadow —
 * a straight tonal step where the mask starts fading. Fading the still out
 * leaves the video dissolving cleanly into the shade/base, exactly like the
 * poster does on its own. Pure :has() on the existing is-visible state: no
 * JS/markup change, the existing 900ms opacity transition crossfades both
 * ways automatically, and where :has() is unsupported the previous
 * composite simply remains. The blurred ambient layer stays — it carries
 * no detail and cannot ghost. */
.gx-hero:has(#hero-trailer-wrap.is-visible) .gx-hero-img {
  opacity: 0;
}

/* Layered cinematic shading: navbar-safe top, readable copy, and a TALL
 * bottom blend that dissolves the poster into the page background.
 * The bottom gradient runs the lower ~74% of the hero with eased multi-stops
 * (no banding steps) and ends at 0.58 — the same value main::before starts
 * from at the boundary, so hero edge and page ramp are one continuous tone.
 * 0.58 (was 0.86) is the LIGHT hand-off: the copy band keeps a ~0.5 scrim
 * for contrast while the last of the ramp is translucent enough for the
 * fixed poster backdrop (see .gx-hero-bg + .gx-page-ambient) to read
 * through, so the hero melts into the page's colour instead of into a dark
 * plateau. */
.gx-hero-shade {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* ONE vertical ramp only — deliberately nothing that varies across the
   * width, so every pixel on the hero's bottom edge carries the same tone
   * and main::before can start from exactly that tone (0.58) with no step
   * anywhere along the boundary. Long and gentle rather than short and
   * black: the artwork stays readable far down into the frame, the copy
   * zone keeps its ~0.5 scrim, and the ramp eases out so the hero's bottom
   * is the poster backdrop showing through, not a flat black plate. */
  background: linear-gradient(to top,
    rgba(11, 13, 18, 0.58) 0%,
    rgba(11, 13, 18, 0.58) 7%,
    rgba(11, 13, 18, 0.54) 13%,
    rgba(11, 13, 18, 0.46) 21%,
    rgba(11, 13, 18, 0.36) 29%,
    rgba(11, 13, 18, 0.26) 38%,
    rgba(11, 13, 18, 0.16) 48%,
    rgba(11, 13, 18, 0.08) 57%,
    rgba(11, 13, 18, 0.03) 66%,
    transparent 74%);
}

/* Frame shaping: the left-side wash (copy contrast) + top wash + vignette.
 * These all VARY across the width, so they live here rather than in
 * .gx-hero-shade and are faded out of the bottom band by the mask below —
 * otherwise the left edge of the hero would be darker than its right at the
 * boundary and the hand-off would draw a line. */
.gx-hero-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to right, rgba(11, 13, 18, 0.55) 0%, transparent 45%),
    linear-gradient(to bottom, rgba(11, 13, 18, 0.42) 0%, transparent 20%),
    radial-gradient(120% 95% at 50% 42%, transparent 52%, rgba(22, 25, 30, 0.55) 100%);
  /* Keep every one of them off the bottom blend zone: the shade + strip
   * below own that band, so the dissolve stays a single vertical ramp from
   * edge to edge. */
  -webkit-mask-image: linear-gradient(to top, transparent 0%, black 24%);
  mask-image: linear-gradient(to top, transparent 0%, black 24%);
}

/* Trailer sits between still and shade: crossfade, cover-fills the hero and
 * overscans it, so no YouTube player chrome is ever visible (see iframe rule) */
.gx-hero-trailer {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  opacity: 0;
  transition: opacity 1200ms ease;
  pointer-events: none;
  /* Video dissolves on the same long lower-half ramp as the still (a touch
   * earlier so any residual player edge has left before the copy zone) and
   * eases to fully transparent AT 100% so the video never ends in a line.
   * Both land on the same fixed poster backdrop underneath. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 40%, rgba(0, 0, 0, 0.7) 56%, rgba(0, 0, 0, 0.42) 70%, rgba(0, 0, 0, 0.2) 82%, rgba(0, 0, 0, 0.07) 93%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 40%, rgba(0, 0, 0, 0.7) 56%, rgba(0, 0, 0, 0.42) 70%, rgba(0, 0, 0, 0.2) 82%, rgba(0, 0, 0, 0.07) 93%, transparent 100%);
}

.gx-hero-trailer.is-visible { opacity: 1; }
.gx-hero-trailer.hidden { display: none; }

/* Instant cut for rotation transitions (set + removed synchronously inside
 * js/hero.js cutTrailer only). A finished trailer must disappear in the same
 * task as the `ended` event: fading the dead frame out over the 1200ms
 * opacity transition kept the YouTube endscreen/replay icon composited while
 * the two feathered bottom masks overlapped mid-fade — the centered reload
 * artifact and the thin horizontal seam. Fade-IN of a confirmed-playing
 * trailer (the transition above) is untouched. */
.gx-hero-trailer.is-cut { transition: none !important; }

/* Buffering layer: in layout (so the iframe loads) but paints nothing.
 * This is what makes the trailer Window-safe while it gets ready — no
 * YouTube chrome, spinner, or error surface can flash through. */
.gx-hero-trailer.is-buffering { visibility: hidden; }

.gx-hero-trailer iframe {
  position: absolute;
  top: 50%;
  left: 50%;
  border: 0;
  /* True fullscreen cover for the 96vh desktop hero (full-bleed width):
   * width covers max(hero width, hero-height * 16/9 = 96vh * 16/9 = 170.67vh),
   * height covers max(hero height, hero-width * 9/16 = 56.25vw).
   * svh/dvh fallbacks keep the cover exact when the mobile browser chrome
   * resizes the viewport. The extra scale(1.45) overscan pushes residual
   * YouTube edge chrome (title bar, center pause icon, progress bar,
   * endscreen edges) outside the visible hero. Greybox mute button lives
   * outside this iframe. */
  width: max(100%, 170.67vh, 170.67svh, 170.67dvh);
  height: max(100%, 56.25vw);
  min-width: 100%;
  min-height: 100%;
  transform: translate(-50%, -50%) scale(1.45);
  /* The trailer is background scenery: it must never be hoverable,
   * tappable, or focusable — any hover/tap is what summons YouTube's
   * center play/pause + prev/next (forward) icons. Unconditional. */
  pointer-events: none !important;
}

/* Interaction shield: sits ABOVE the iframe (below text/buttons) and
 * unconditionally swallows every hover/tap/touch so YouTube can never be
 * driven to reveal its center play/pause or prev/next icons — no matter
 * the device (mouse, touch, keyboard) or player state. Transparent visual,
 * opaque to hit-testing. Hero buttons live in .gx-hero-content (z-index 5)
 * above this layer, so Watch Now / More Info / My List stay clickable. */
.gx-hero-trailer-shield {
  position: absolute;
  inset: 0;
  z-index: 3;
  /* Transparent to visual, opaque to hit-testing. A thin gradient at the
   * bottom covers any residual YouTube progress bar / control chrome that
   * the overscan doesn't fully push out of the hero. */
  background: linear-gradient(to top, rgba(0,0,0,0.6) 0%, transparent 4%);
  pointer-events: auto !important;
  /* Swallow taps/hover (what summons YouTube's center icons) but never
   * trap scrolling: touch drags still scroll the page normally. */
  touch-action: pan-x pan-y;
  cursor: default;
}

/* Content — sits above the trailer/shade AND above the main::before blend
 * strip + shelf tuck (which live in <main>'s stacking context below). The
 * content box itself stays click-through so shelf gaps never die on it;
 * only real controls are interactive and they sit on top of everything. */
.gx-hero-content {
  position: relative;
  z-index: 5;
  /* SAME column as .gx-page and .gx-header-inner: the --gx-gutter pair (3%
   * per side, capped with --gx-content-max) + auto margins, so the badge,
   * logo, title, meta and actions all sit on the exact vertical line as the
   * navbar logo and the card rows below. Only the hero BACKGROUND stays
   * full-bleed — this box is the inset one. Lower-left cinematic
   * composition for the larger hero: content docks to the bottom clear of
   * the blend. */
  max-width: calc(var(--gx-content-max) + 2 * var(--gx-gutter));
  margin: 0 auto;
  box-sizing: border-box;
  padding: 0 var(--gx-gutter);
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* Buttons sit ~116px above the hero bottom: clear of the tall bottom
   * blend AND clear of the shallow shelf tuck below (~40px), so actions are
   * never covered — and the copy block reads as docked LOW in a taller
   * frame rather than floating in the middle of it. */
  padding-bottom: 116px;
  min-width: 0;
  /* Content box itself never swallows clicks meant for shelves below —
   * only real controls are interactive. */
  pointer-events: none;
}

/* Mobile keeps the shared 12px/10px pads (mirrors style.css + navbar.css)
 * so hero copy, logo and cards stay on ONE vertical line at every width. */
@media (max-width: 859px) {
  .gx-hero-content { padding-inline: 12px; }
}
@media (max-width: 420px) {
  .gx-hero-content { padding-inline: 10px; }
}

.gx-hero-content a,
.gx-hero-content button {
  pointer-events: auto !important;
}

.gx-hero-kicker {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gx-text-dim);
  margin-bottom: 10px;
}

.gx-hero-kicker:empty { display: none; }

.gx-hero-title {
  font-size: clamp(2rem, 5vw, 3.75rem);
  line-height: 1.04;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--gx-text);
  max-width: 16ch;
  text-wrap: balance;
}

/* Title/logo layer: the official TMDB title artwork replaces the text
 * title visually whenever a usable logo resolves (text stays in the DOM for
 * screen readers and as the no-logo fallback — never both visible). The
 * transparent artwork keeps its own aspect ratio inside these caps: it is
 * never stretched into a fixed box, cropped, or distorted. */
.gx-hero-logo {
  display: block;
  max-width: min(26rem, 72%);
  max-height: 7.5rem;
  width: auto;
  height: auto;
  object-fit: contain;
  object-position: left bottom;
  margin-bottom: 0.35rem;
  filter: drop-shadow(0 6px 24px rgba(0, 0, 0, 0.55));
}
.gx-hero-logo.hidden { display: none; }
.gx-hero-title.has-logo {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.gx-hero-meta {
  margin-top: 10px;
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--gx-text-dim);
}

.gx-hero-meta:empty { display: none; }

.gx-hero-meta .gx-star { color: var(--gx-accent-strong); }

/* Copy legibility: the reference keeps a drop shadow under the hero text
 * (cinema-title treatment) so the artwork can stay LIT right behind it —
 * the bottom blend deliberately no longer buries the poster under an opaque
 * black plate, and on phones the copy lands on the brightest part of the
 * frame. Shadow only: no scrim change, so the hand-off to the page stays
 * exactly as tuned in .gx-hero-shade / main::before. */
.gx-hero-kicker,
.gx-hero-title,
.gx-hero-meta {
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.55), 0 1px 4px rgba(0, 0, 0, 0.45);
}

.gx-hero-actions {
  margin-top: 22px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  position: relative;
  z-index: 6;
  pointer-events: auto !important;
}

.gx-btn {
  appearance: none;
  border: 1px solid transparent;
  border-radius: var(--gx-radius-pill);
  font-size: 14px;
  font-weight: 700;
  padding: 12px 26px;
  cursor: pointer;
  white-space: nowrap;
  pointer-events: auto !important;
  position: relative;
  z-index: 7;
  transition: background var(--gx-transition), border-color var(--gx-transition), transform var(--gx-transition);
}

.gx-btn:active { transform: translateY(1px); }

.gx-btn-primary {
  background: var(--gx-text);
  color: #0b0b10;
}

.gx-btn-primary:hover { background: #fff; }

.gx-btn-glass {
  background: rgba(255, 255, 255, 0.12);
  border-color: var(--gx-border-strong);
  color: var(--gx-text);
}

.gx-btn-glass:hover { background: rgba(255, 255, 255, 0.15); }

.gx-btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--gx-text-dim);
  font-weight: 600;
  padding: 12px 14px;
}

.gx-btn-ghost:hover { color: var(--gx-text); }
.gx-btn-ghost.is-in-list { color: var(--gx-accent-strong); }

/* Trailer control: one small circular glass button in the actions row.
 * Countdown ring while waiting → speaker icon while playing. Same component,
 * same position — never two controls at once. */
.gx-trailer-btn {
  position: relative;
  z-index: 7;
  pointer-events: auto !important;
  flex: none;
  width: 42px;
  height: 42px;
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: var(--gx-text);
  background: rgba(10, 10, 15, 0.7);
  border: 1px solid var(--gx-border-strong);
  cursor: pointer;
  transition: background var(--gx-transition), border-color var(--gx-transition);
}

.gx-trailer-btn:hover {
  background: rgba(255, 255, 255, 0.13);
  border-color: var(--gx-accent-border);
}

.gx-trailer-btn:focus-visible {
  outline: 2px solid var(--gx-accent);
  outline-offset: 2px;
}

.gx-trailer-btn.hidden { display: none; }

/* State visibility: icons only while playing (button hidden otherwise) */
.gx-trailer-btn .gx-icon {
  width: 18px;
  height: 18px;
}

.gx-trailer-btn.is-muted .gx-icon-unmuted { display: none; }
.gx-trailer-btn.is-unmuted .gx-icon-muted { display: none; }
.gx-trailer-btn.is-muted .gx-icon-muted,
.gx-trailer-btn.is-unmuted .gx-icon-unmuted { display: block; }

/* Loading state keeps structure, dark placeholder, no layout jump.
 * (The old loading ring and placeholder fetch copy are gone: the boot
 * loader — static #gx-boot + css/boot.css/js/boot.js — owns the loading
 * screen now, and js/hero.js setLoading clears the title.) */
.gx-hero.hero-loading .gx-hero-img,
.gx-hero.hero-loading .gx-hero-ambient { opacity: 0; }

/* Tablet composition — a step between the 96vh desktop hero and the 68vh
 * mobile hero. Copy gutters follow the shared --gx-gutter column; trailer
 * cover follows the same 16/9 rule (84vh * 16/9 = 149.33vh). */
@media (max-width: 1024px) and (min-width: 641px) {
  .gx-hero {
    height: 84vh;
    height: 84svh;
    height: 84dvh;
  }

  .gx-hero-trailer iframe {
    width: max(100%, 149.33vh, 149.33svh, 149.33dvh);
    height: max(100%, 56.25vw);
    min-width: 100%;
    min-height: 100%;
    transform: translate(-50%, -50%) scale(1.45);
  }
}

/* Mobile composition (not a shrunk desktop) — 68% of the screen. */
@media (max-width: 640px) {
  .gx-hero {
    height: 68vh;
    height: 68svh;
    height: 68dvh;
  }

  .gx-hero-img { object-position: center 15%; }

  /* Disable Ken Burns on mobile: the 24s transform animation keeps the
   * compositor busy during scroll and wastes GPU cycles on phones where
   * the hero is only 68vh. Static poster is visually identical at this
   * size and costs zero per-frame GPU work. */
  .gx-hero.is-ready .gx-hero-img { animation: none; }

  .gx-hero-content { padding-bottom: 44px; }

  /* Shorter continuation on small screens — same continuous blend (the ramp
   * starts at exactly the hero shade's bottom tone at the boundary — 160px
   * into a 460px strip = 34.783% — then eases out), no layout gap: the
   * heights and margins still net 0px. Overlap stays shallow so low-sitting
   * mobile buttons (44px up) are never covered. Upper plateau held at zero
   * for the same reason as desktop: anything above the boundary is read
   * THROUGH the hero's translucent shade, so a ramp there would stack under
   * it and redraw the seam (0.58 over 0.58 above vs 0.58 below). Light
   * hand-off values mirror the desktop strip. */
  main::before {
    height: 460px;
    margin-top: -160px;
    margin-bottom: -300px;
    background: linear-gradient(to bottom,
      rgba(11, 13, 18, 0) 0%,
      rgba(11, 13, 18, 0) 34.783%,
      rgba(11, 13, 18, 0.58) 34.783%,
      rgba(11, 13, 18, 0.46) 46%,
      rgba(11, 13, 18, 0.28) 60%,
      rgba(11, 13, 18, 0.14) 74%,
      rgba(11, 13, 18, 0.05) 88%,
      rgba(11, 13, 18, 0) 100%);
  }

  /* Mobile tuck: ~30px overlap leaves ~26px clearance below the buttons.
   * Pointer-events pass-through (base rule above) keeps buttons clickable. */
  #home-sections:not(:empty) {
    margin-top: -30px;
  }

  /* Mobile cover for the 68vh hero: width covers max(hero width,
   * 68vh * 16/9 = 120.89vh) so portrait phones get a true fullscreen
   * fill without the old 177vh over-zoom. dvh keeps it exact when the
   * mobile URL bar shows/hides. */
  .gx-hero-trailer iframe {
    width: max(100%, 120.89vh, 120.89svh, 120.89dvh);
    height: max(100%, 56.25vw);
    min-width: 100%;
    min-height: 100%;
    transform: translate(-50%, -50%) scale(1.45);
  }

  /* Title artwork stays proportional on small screens: slightly tighter
   * caps keep wide logos inside the content column without crowding the
   * meta/actions below. Aspect handling (contain, no crop) is unchanged. */
  .gx-hero-logo {
    max-width: min(19rem, 78%);
    max-height: 5.5rem;
    margin-bottom: 0.3rem;
  }

  .gx-btn { padding: 11px 20px; font-size: 13.5px; }
  .gx-btn-ghost { padding: 11px 10px; }

  /* Mobile hero ambient: reduced blur from 70px to 30px. The ambient layer
   * sits behind the hero poster - at 65vh and with the hero shade on top,
   * the heavy blur is imperceptible but costs GPU rasterization on mobile.
   * 30px provides the same color diffusion at a fraction of the cost. */
  .gx-hero-ambient { filter: blur(30px) saturate(1.1); }

  /* The backdrop is fixed and viewport-sized on phones too — the mobile
   * win is a cheaper blur (40px rasterises far faster than the desktop
   * 90px on a budget GPU) with the same colour diffusion. */
  .gx-page-ambient-img { filter: blur(40px) saturate(1.4) brightness(0.88); }

  /* Top light bleed stays even quieter on small screens. */
  .gx-page-ambient-glow { opacity: 0.22; }
}

@media (prefers-reduced-motion: reduce) {
  .gx-hero.is-ready .gx-hero-img { animation: none; }
  .gx-hero-ambient,
  .gx-hero-img,
  .gx-hero-trailer,
  .gx-page-ambient { transition: none; }
  .gx-btn:active { transform: none; }
}


/* ---- icon-only circle control (My List) ----
 * Hero and detail-modal add-to-list are pure icons: glass circle, thin plus
 * that rotates on hover, flipping to an accent check when .is-in-list.
 * Appended last so it overrides the .gx-btn-ghost padding above. */
.gx-btn-icon {
  width: 44px;
  height: 44px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.10);
  border-color: var(--gx-border-strong);
  color: var(--gx-text);
}
.gx-btn-icon:hover {
  background: rgba(255, 255, 255, 0.16);
  color: var(--gx-text);
}
.gx-btn-icon .gx-ico {
  width: 20px;
  height: 20px;
  transition: transform 340ms cubic-bezier(.22, 1, .36, 1);
}
.gx-btn-icon:not(.is-in-list):hover .gx-ico { transform: rotate(90deg); }
.gx-btn-icon.is-in-list {
  color: var(--gx-accent-strong);
  border-color: var(--gx-accent-border);
  background: rgba(208, 162, 79, 0.14);
}

