:root {
  --film-carbon: #030405;
  --film-graphite: #15181c;
  --film-titanium: #d8dbde;
  --film-muted: #8f949a;
  --film-orange: #f07b1c;
  --film-amber: #ffb45c;
  --film-hairline: rgba(216, 219, 222, 0.18);
  --film-soft: rgba(216, 219, 222, 0.08);
  --film-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --film-gutter: clamp(20px, 5vw, 80px);
}

html,
body {
  overflow-x: clip;
}
html.film-part-modal-open,
body.film-part-modal-open {
  overflow: hidden;
  overscroll-behavior: none;
}

body.film-hero-active .nav,
body.film-hero-active .nav.scrolled {
  background: rgba(3, 4, 5, 0.96) !important;
  border-color: var(--film-soft) !important;
  box-shadow: none !important;
  mix-blend-mode: normal !important;
  transform: none !important;
}

body.film-hero-active .nav-links a {
  color: var(--film-titanium) !important;
}

body.film-hero-active .nav-logo img {
  filter: none !important;
  transform: none !important;
}

body.film-hero-active .mobile-menu-btn span {
  background: var(--film-titanium) !important;
}

body.film-hero-active .mobile-action-bar,
body.film-hero-active .rw-channel-btn,
body.film-hero-active .rw-popup,
body.film-hero-active .rw-chat-label,
body.film-hero-active .rc-fab-label,
body.film-hero-active .rocket-btn,
body.film-hero-active .rw-rocket-btn {
  opacity: 0 !important;
  visibility: hidden !important;
  pointer-events: none !important;
  transform: translateY(16px) !important;
}

.mobile-nav-overlay {
  visibility: hidden;
}

.mobile-nav-overlay.active {
  visibility: visible;
}

.film-hero {
  position: relative;
  z-index: 1;
  /* min() cap: on very tall / portrait monitors an uncapped 100svh stretches
     the stage into a void between the copy and the car (CEO-reported at
     ~1850x3690); 1080px keeps the one-screen feel on standard desktops
     (<=1080 tall) while letting tall screens flow to the next section. */
  min-height: min(100svh, 1080px);
  color: var(--film-titanium);
  background: var(--film-carbon);
  isolation: isolate;
  outline: none;
}

.film-hero:focus-visible {
  outline: 1px solid var(--film-orange);
  outline-offset: -1px;
}

.film-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: min(100svh, 1080px);
  overflow: clip;
  /* Amendment 5: stops widened/darkened to match the photo's own studio
     backdrop at the frame region (a very dark neutral with a subtle
     center-mass glow, not the wider mid-tone band this had before) so the
     photograph's masked edges (film-body above) fade into a backdrop of
     the same tone instead of a visibly different gradient stepping in
     behind them. */
  background:
    radial-gradient(ellipse at 60% 62%, rgba(216, 219, 222, 0.065), transparent 38%),
    linear-gradient(180deg, var(--film-carbon) 0%, #0c0e10 55%, var(--film-carbon) 100%);
}

.film-stage::before,
.film-stage::after {
  position: absolute;
  inset: 0;
  content: "";
  pointer-events: none;
}

.film-stage::before {
  z-index: 12;
  background:
    linear-gradient(90deg, rgba(3, 4, 5, 0.82) 0%, transparent 36%, transparent 78%, rgba(3, 4, 5, 0.46) 100%),
    radial-gradient(ellipse at center, transparent 52%, rgba(3, 4, 5, 0.7) 100%);
}

.film-stage::after {
  z-index: 13;
  opacity: 0.022;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.film-fog {
  position: absolute;
  inset: -12%;
  z-index: 1;
  pointer-events: none;
  opacity: 0.48;
}

.film-fog span {
  position: absolute;
  top: -8%;
  left: 4%;
  width: 58vw;
  height: 58vw;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(216, 219, 222, 0.07), transparent 62%);
  mix-blend-mode: screen;
  animation: film-fog-drift 52s ease-in-out infinite alternate;
}

.film-fog span:last-child {
  top: auto;
  right: 0;
  bottom: -10%;
  left: auto;
  width: 68vw;
  height: 48vw;
  animation-duration: 76s;
  animation-delay: -34s;
}

@keyframes film-fog-drift {
  from { transform: translate3d(-4vw, 2vh, 0) scale(1); }
  to { transform: translate3d(5vw, -3vh, 0) scale(1.12); }
}

/* ---- Merged pit-in stage (Amendment 2): the opening sits in a compact,
   normal-flow band at the top of .film-stage; the transplanted parts-stage
   (vehicle + dots + hotspot layer) fills the remaining flex space below it.
   There is no separate hero-only camera/world/vehicle assembly any more --
   .film-parts-vehicle (further down this file) is the single vehicle stage
   for the one surviving G80 photo. */
.film-opening {
  /* --nav-clear: the fixed site nav is ~72px tall (22px padding × 2 + 26px
     logo, common.css .nav) and has no height variable of its own; every
     padding-top on this element must clear it or the overline hides under
     the nav. */
  --nav-clear: 72px;
  position: relative;
  z-index: 20;
  flex: 0 0 auto;
  width: min(820px, calc(100% - (var(--film-gutter) * 2)));
  margin: 0 0 0 var(--film-gutter);
  padding: calc(var(--nav-clear) + clamp(12px, 2.5vh, 28px)) 0 0;
}

/* ---- Amendment 5: two-state headline (desktop, tall viewports only) ----
   Pre-scroll calm state lowers the opening block to sit just above the
   car's hood so headline + car compose as one unit (CEO: "headline floats
   too high, disconnected from the car"). translateY is a 2-point linear
   vh fit, calibrated from real getBoundingClientRect() measurements
   (Playwright, localhost:8899) of .film-opening and .film-frame at
   1440x900 and 1440x800 -- the hood line taken at 47.5% down the frame
   (mid of the spec's 45-50% range), the opening's target bottom edge set
   32px above it (mid of the spec's 24-40px range):
     900vh: opening bottom 339.83px, frame top 301.95px, frame height
       566.05px -> hood 301.95 + .475*566.05 = 570.83px -> target bottom
       570.83 - 32 = 538.83px -> translateY 538.83 - 339.83 = 199.0px
     800vh: opening bottom 337.33px, frame top 291.97px, frame height
       476.03px -> hood 291.97 + .475*476.03 = 518.08px -> target bottom
       518.08 - 32 = 486.08px -> translateY 486.08 - 337.33 = 148.75px
   The hood's pixel position has THREE regimes as viewport height grows
   (empirically measured, full table below), so the offset is a bounded
   two-piece fit, not one line -- a reviewer caught the original
   single-line version (calc(50.25vh - 253px), unbounded) putting the
   headline 51.5px INTO the car body at 1440x1400, because the fit was
   calibrated only at 800/900 where the first regime holds:
     1. vh <= ~914: .film-frame's width formula tracks vh (the
        152.5vh - 413.4px term binds below the 980px cap), so the frame
        grows with the viewport and the hood moves at ~0.5025px per px
        of viewport height.
     2. ~914 < vh <= 1080: the frame is width-capped at 980px (constant
        size) but bottom-anchored in a stage still growing with the
        viewport, so the whole frame slides DOWN at ~0.975px per px.
     3. vh > 1080: the stage caps at 1080px (min(100svh,1080px)) and the
        hood freezes entirely.
   Measured on the real served page (Playwright, 1440 wide; hood = frame
   top + 47.5% of frame height; needed = hood - 32px - opening bottom):
     H=760:  opening-bottom 336.3, hood 497.0, needed 128.7
     H=800:  opening-bottom 337.3, hood 518.1, needed 148.8
     H=900:  opening-bottom 339.8, hood 570.8, needed 199.0
     H=1000: opening-bottom 342.3, hood 664.4, needed 290.0
     H=1080: opening-bottom 344.3, hood 744.4, needed 368.0
     H=1200/1400/1600: opening-bottom 345.3, hood 744.4, needed 367.0
   Piece 1 (regime 1): 0.5025px/px, intercept -253 -> calc(50.25vh - 253px)
   (the vh coefficient is the raw slope x100 -- CSS 1vh is already 1% of
   viewport height; an earlier draft wrote 0.5vh and moved the copy the
   wrong way by two orders of magnitude before Playwright caught it).
   Piece 2 (regime 2): slope (368-290)/(1080-1000) = 0.975px/px, intercept
   290 - 0.975*1000 = -685 -> calc(97.5vh - 685px). The two lines cross at
   ~914px viewport height -- exactly the regime boundary -- so max() picks
   the right piece automatically. Regime 3's freeze is the outer min()
   cap at 367px (needed value at every height >= 1200).
   Recomputed clearances with min(max(p1, p2), 367px): 31.8 @ 760/800,
   31.8 @ 900, 32.0 @ 1000, 33.0 @ 1080, 32.0 @ 1200/1400/1600 -- all
   inside the 24-40px target band (acceptance band 20-48px). Re-measure
   the table and refit all three pieces if .film-opening's height, the
   frame width formula, or the stage cap changes.
   Scoped to min-height:760px: below that the frame is already floored
   (max(560px, ...) in .film-frame, and the max-height:720px band further
   down widens label offsets) and lowering the opening on top of that
   would collide with the frame per the spec's own "short viewports skip
   the lowered state" direction. */
@media (min-width: 769px) and (min-height: 760px) {
  .film-parts .film-opening {
    transition: transform 0.7s var(--film-ease);
  }

  .film-parts.film-callouts-armed:not(.film-callouts-live) .film-opening {
    transform: translateY(clamp(32px, 5vh, 56px));
  }

  /* While lowered, the opening overlaps .film-frame -- whose z-index 21
     (the Amendment 4 stacking-context fix) paints the semi-opaque masked
     photo OVER the copy at z20, veiling the sub line's tail and the
     secondary CTA. Raise the copy above the frame for the CALM state
     only: a global z-index change is forbidden (open panels must keep
     painting above the copy in the revealed state -- the frame subtree's
     z23 hotspot layer depends on frame(21) > opening(20)), but while the
     callouts are armed no panel can open (labels are visibility:hidden,
     unfocusable), so the raise is safe here.
     Scoped to .film-callouts-armed alone, NOT :not(.film-callouts-live)
     like the transform above: armed persists ~1.5s after live (both
     classes are removed together, initializeCalloutReveal), which covers
     the full 0.7s upward ease -- keying the z on the :not() variant
     would snap the copy back under the frame at the very start of the
     transition, while it still overlaps the car (a visible re-veil
     mid-motion, confirmed in the real browser). By the time armed is
     removed, the copy has settled at its upper position where nothing
     overlaps it, so the z22->z20 flip is invisible. Panels are openable
     during that trailing ~0.8s window in principle, but cannot overlap
     the copy at these (min-height:760px) viewports -- the top label row
     itself already clears the settled copy by >=16px (verified). */
  .film-parts.film-callouts-armed .film-opening {
    z-index: 22;
  }
}
/* ---- End Amendment 5 two-state headline. ---- */

@media (min-width: 769px) {
  .film-hero {
    min-height: min(calc(100svh + 160px), 1240px);
  }

  .film-stage {
    --film-caption-clearance: clamp(28px, 3vh, 40px);
    height: min(
      calc(100svh + 160px + var(--film-caption-clearance)),
      calc(1240px + var(--film-caption-clearance))
    );
    padding-bottom: var(--film-caption-clearance);
  }

  .film-opening {
    position: sticky;
    top: 0;
    width: min(980px, calc(100% - 128px));
    margin-left: clamp(40px, 3vw, 64px);
    transition: transform 0.7s var(--film-ease);
  }

  .film-parts .film-title {
    max-width: 980px;
    font-size: clamp(64px, 5vw, 96px);
    line-height: 0.92;
    letter-spacing: -0.07em;
  }

  .film-parts .film-opening-sub {
    margin-top: 24px;
    font-size: clamp(16px, 1.15vw, 20px);
  }

  .film-parts .film-opening-sub span + span,
  .film-parts .film-opening-proof,
  .film-parts .film-actions {
    display: none;
  }

  .film-parts .film-frame {
    width: min(1520px, 104vw, max(760px, calc(185vh - 300px)));
    margin-bottom: 48px;
  }

  .film-parts .film-overline,
  .film-parts .film-title,
  .film-parts .film-opening-sub {
    transition: opacity 0.55s var(--film-ease), transform 0.7s var(--film-ease);
  }

  .film-parts .film-scroll-title {
    position: absolute;
    top: calc(var(--nav-clear) + clamp(12px, 2.5vh, 28px) + 22px);
    left: 0;
    display: block;
    margin: 0;
    color: var(--film-titanium);
    font-family: var(--font-kr);
    font-size: clamp(40px, 3.2vw, 60px);
    font-weight: 700;
    line-height: 0.96;
    letter-spacing: -0.06em;
    opacity: 0;
    transform: translateY(20px);
  }

  .film-scroll-title span {
    color: var(--film-muted);
  }

  .film-parts[data-film-callouts-revealed] .film-overline,
  .film-parts[data-film-callouts-revealed] .film-title,
  .film-parts[data-film-callouts-revealed] .film-opening-sub {
    opacity: 0;
    transform: translateY(-20px);
  }

  .film-parts[data-film-callouts-revealed] .film-scroll-title {
    opacity: 1;
    transform: translateY(0);
  }

  .film-parts[data-film-callouts-revealed] .film-opening {
    pointer-events: none;
  }
}

.film-scroll-title {
  display: none;
}

.film-overline {
  margin: 0 0 12px;
  color: var(--film-muted);
  font-family: var(--font-en);
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.2em;
}

.film-overline::before {
  display: inline-block;
  width: 28px;
  height: 1px;
  margin-right: 12px;
  vertical-align: middle;
  content: "";
  background: var(--film-orange);
}

.film-title {
  max-width: 820px;
  margin: 0;
  color: var(--film-titanium);
  font-family: var(--font-kr);
  font-size: clamp(30px, 3.2vw, 42px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.055em;
  word-break: keep-all;
}

.film-title span {
  color: color-mix(in srgb, var(--film-titanium) 78%, var(--film-muted));
}

.film-opening-sub {
  margin: 10px 0 0;
  color: color-mix(in srgb, var(--film-titanium) 74%, var(--film-muted));
  font-family: var(--font-kr);
  font-size: clamp(13px, 1vw, 15px);
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: -0.02em;
}

.film-opening-sub span {
  display: block;
}

.film-opening-proof {
  display: flex;
  flex-wrap: wrap;
  gap: 7px 18px;
  margin: 10px 0 0;
  color: var(--film-muted);
  font-family: var(--font-kr);
  font-size: 11px;
  font-weight: 500;
  line-height: 1.4;
}

.film-opening-proof span {
  position: relative;
}

.film-opening-proof span + span::before {
  position: absolute;
  top: 0;
  left: -10px;
  color: var(--film-orange);
  content: "·";
}

.film-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}

.film-action {
  display: inline-flex;
  min-width: 152px;
  min-height: 44px;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 0 24px;
  border: 1px solid transparent;
  border-radius: 2px;
  font-family: var(--font-kr);
  font-size: 14px;
  font-weight: 700;
  text-decoration: none;
  transition: color 240ms var(--film-ease), background-color 240ms var(--film-ease), border-color 240ms var(--film-ease), transform 240ms var(--film-ease);
}

.film-action svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  transition: transform 240ms var(--film-ease);
}

.film-action--primary {
  color: var(--film-carbon);
  background: var(--film-orange);
}

.film-action--secondary {
  color: var(--film-titanium);
  background: rgba(3, 4, 5, 0.88);
  border-color: rgba(216, 219, 222, 0.28);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.film-action:hover {
  transform: translateY(-2px);
}

.film-action--primary:hover {
  background: var(--film-amber);
}

.film-action--secondary:hover {
  border-color: var(--film-orange);
}

.film-action:hover svg {
  transform: translateX(3px);
}

.film-action:focus-visible,
.film-hotspot:focus-visible,
.film-hotspot-close:focus-visible {
  outline: 2px solid var(--film-orange);
  outline-offset: 3px;
}

.film-scroll-next {
  position: absolute;
  bottom: clamp(26px, 4vh, 44px);
  left: var(--film-gutter);
  z-index: 24;
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  gap: 12px;
  color: var(--film-muted);
  font-family: var(--font-en);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-decoration: none;
}

/* While a part panel is open the cue is noise — and on mobile its z-index (24)
   would otherwise paint over the fixed bottom sheet (hotspot layer z 23). */
.film-parts[data-film-hotspot-open] .film-scroll-next {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.film-scroll-next svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: var(--film-orange);
  stroke-width: 1.5;
  transition: transform 220ms var(--film-ease);
}

.film-scroll-next:hover svg {
  transform: translateY(3px);
}

.film-scroll-next:focus-visible {
  outline: 2px solid var(--film-orange);
  outline-offset: 3px;
}

/* .film-hero already owns this section's background, focus ring, isolation,
   and z-index (base rule, top of this file) -- .film-parts used to duplicate
   them for the second, standalone section. Post-merge both classes live on
   the same <section>, so restating them here would just be a second,
   easy-to-drift copy of a rule .film-stage already paints over. Nothing
   below re-declares section-level chrome; .film-parts is kept only as a
   descendant-selector scope (.film-parts-stage, .film-parts.film-stage-armed,
   the :not(.film-hotspots-enhanced) fallbacks, etc.) further down this file. */

.film-stage-caption {
  flex: 0 0 auto;
  z-index: 20;
  margin: 14px 0 0 var(--film-gutter);
  color: var(--film-muted);
  font-family: var(--font-kr);
  font-size: 12px;
  line-height: 1.5;
  word-break: keep-all;
}

.film-parts-stage {
  /* Deliberately NO z-index and NO overflow clip: with no stacking context
     of its own, the children sort against .film-opening (z-index 20)
     individually — .film-hotspot-layer (23) floats open panels above the
     copy they can reach into on short stages, while .film-parts-vehicle (1)
     keeps the car photo underneath the copy. The outer .film-stage already
     clips the pit-in slide. A z-index here would flatten that split and
     paint the car over the CTAs (or, at a low value, shadow panel clicks
     under the copy).
     display:flex + align-items:flex-end bottom-anchors .film-frame (a
     normal-flow child, per Amendment 3) the same way the retired
     bottom:2% absolute rule anchored .film-parts-vehicle: on desktop the
     frame's own height (aspect-ratio × its width) is usually taller than
     this flex region, so it bleeds upward past the nominal top edge,
     clipped only by the outer .film-stage. margin-inline:auto on
     .film-frame absorbs the row's free space to center it horizontally.
     On mobile this is a no-op (single child, content-driven height, no
     surplus cross space to redistribute). */
  position: relative;
  display: flex;
  align-items: flex-end;
  flex: 1 1 auto;
  min-height: 0;
}

/* ---- Amendment 3: the aspect-ratio-locked frame ----
   Wraps the vehicle image exactly (aspect-ratio matches its intrinsic
   2528x1492 pixels), so every callout anchor, leader line, and label
   offset authored as a percentage of THIS box tracks the car at any
   viewport size or browser zoom -- no measurement JS, pure CSS. On
   desktop both children fill it via inset:0 (absolute), so only the
   frame's own aspect-ratio math determines its size. On mobile both
   children flow normally (static/relative), so the frame grows to fit
   the vehicle image plus the chip grid stacked beneath it -- aspect-ratio
   only governs sizing when there is no in-flow content driving it taller,
   which is exactly the desktop case (verified: a block box with
   aspect-ratio set and only out-of-flow children uses the ratio; the same
   box with real in-flow content grows to fit it instead of clipping).
   The six --dot-<part>-x/y coordinates live here now -- one coordinate
   set, replacing the old stage-relative set plus the separate mobile
   override block. Values are calibrated against the vehicle image's OWN
   box (measured at 1440x900: image 1120x661px within a 1440-wide stage),
   not the old, much taller .film-parts-stage box the pill positions used
   to reference. */
.film-frame {
  position: relative;
  aspect-ratio: 2528 / 1492;
  /* 980px, not the ~1120px the car rendered at pre-Amendment-3: at the
     old width the frame (bottom-anchored, per .film-parts-stage above)
     bleeds far enough above .film-parts-stage's available height that
     the new top-row label band (~12% down from the frame's own top
     edge) lands inside the opening copy's own box, overlapping the
     secondary CTA. A fixed width alone only fixes this at one specific
     viewport HEIGHT though -- .film-parts-stage's available height
     shrinks as the viewport gets shorter, independent of width, so a
     980px-wide frame that clears the CTA at 900px-tall viewports can
     still overlap it at 800px-tall ones.
     The third min() term is a HEIGHT budget expressed as its own
     equivalent width (multiplied through by 2528/1492 ~= 1.6944, since
     this element sets width, not height -- aspect-ratio then derives
     height FROM whichever width wins). calc(90vh - 244px) is the target
     frame HEIGHT, fit to two measured reference points -- .film-parts-
     stage's real available height at the 900px and 800px viewport
     heights this task's hit-matrix is verified at, each leaving a ~30px
     clearance margin against the CTA -- so it only binds (shrinking the
     car below the 980px/92% width figure) when a viewport is actually
     short enough to need it; 90vh*1.6944 ~= 152.5vh and
     244px*1.6944 ~= 413.4px carry that same budget in width terms.
     Tried a separate max-height/max-width pair first -- rejected: a
     <div> with aspect-ratio and BOTH width and (max-)height definite
     does NOT reconcile them (unlike an <img>'s intrinsic-ratio replaced-
     sizing algorithm), so the box silently stretched out of ratio
     (980x450 measured, vs. the intended 1.694:1). Keeping this as ONE
     width formula sidesteps that entirely. Re-verify the vh/px
     coefficients if .film-opening's own height changes materially.
     Critical fix (2026-07-14): the height term had NO floor, so at very
     short viewports (1024x600) it alone won the min() at ~502px --
     narrow enough that the injector/reducer label pair overlapped by
     double-digit pixels (a click aimed at injector's + control resolved
     to reducer's label instead). max(560px, ...) stops the term from
     collapsing past a size the callouts can still lay out in. 560px
     only binds below ~638px viewport height, so it never changes the
     already-verified 900/800/1080px-tall widths (959.1/806.6/745.2px,
     unchanged). Flooring the width alone would just re-open the ECU-
     label-vs-CTA overlap this same term was added to prevent (a taller
     floor -> a taller frame -> more upward bleed past the row above) --
     see the max-height:720px band in hero-film-responsive.css for the
     paired elbow-spread widen + top-row offset that keeps both
     invariants (label-vs-label AND label-vs-CTA) holding together. */
  width: min(1120px, 96%, max(600px, calc(175vh - 500px)));
  margin-inline: auto;
  /* Amendment 4 fix: the parallax `translate` (media block below) creates a
     stacking context on this element per CSS Transforms, even at its
     resting 0px value. Without an explicit z-index that scopes
     .film-hotspot-layer's z-index 23 INSIDE the frame and drops the whole
     frame subtree (leader lines, labels, anchor dots, open panels) to the
     frame's own z-auto level -- BELOW the stage vignette (z12), noise
     (z13), and the opening copy (z20). Declaring z-index here (the base
     rule, not the parallax media query) forms the same stacking context
     unconditionally, so paint order is identical whether or not parallax
     is active: 21 sits above the opening (20) and vignette/noise (12/13),
     below the corner marks (22) and scroll cue (24). Accepted trade-off,
     verified visually at 1440x900 and 390: the vehicle photo also rises
     above the vignette, but the vignette's darkening concentrates at the
     stage borders the frame doesn't reach, so the difference is
     imperceptible. */
  z-index: 21;
  /* Front-cluster anchors recalibrated 2026-07-14 (CEO): measured against the
     photograph's anatomy — engine bay spans x≈16-34%, side mirror sits at
     x≈42-45% / y≈51-54%. 스위치 connects just below the mirror per direction. */
  --dot-injector-x: 29%;     --dot-injector-y: 53%;
  --dot-ecu-x: 20%;          --dot-ecu-y: 57%;
  --dot-switch-x: 43.5%;     --dot-switch-y: 56%;
  --dot-reducer-x: 31.5%;    --dot-reducer-y: 63.5%;
  --dot-filler-x: 82.14%;    --dot-filler-y: 52.06%;
  --dot-trunk-x: 88.57%;     --dot-trunk-y: 60.85%;
}

/* ---- Amendment 4: mouse micro-parallax (desktop, fine pointer only) ----
   hero-film.js writes --plx-x/--plx-y (-1..1) on .film-stage from
   pointermove, rAF-throttled. .film-frame carries the WHOLE unit -- car,
   leader lines, and labels all move together as one translate on the one
   box that already wraps them (Amendment 3), so the six locked anchors
   never detach from their lines. .film-fog (the wrapper, not its spans,
   which already own their own drift transform/animation) moves at a
   deeper depth. Both are inert unless the pointer is fine and the visitor
   has not asked for reduced motion -- the single combined media query
   guards both requirements in one place instead of a separate
   reduced-motion override to remember to keep in sync. */
@media (min-width: 1025px) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .film-frame {
    transition: translate 0.6s var(--film-ease);
    translate: calc(var(--plx-x, 0) * 3px) calc(var(--plx-y, 0) * 2px);
  }

  .film-fog {
    transition: translate 0.6s var(--film-ease);
    translate: calc(var(--plx-x, 0) * 10px) calc(var(--plx-y, 0) * 10px);
  }
}
/* ---- End Amendment 4 parallax. ---- */

.film-parts-stage::before {
  position: absolute;
  right: 4%;
  bottom: -12%;
  left: 4%;
  height: 36%;
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(21, 24, 28, 0.92), transparent 68%);
  content: "";
  transform: rotateX(68deg);
}

/* Base (mobile-first): normal flow, matching the frame's own auto/aspect
   sizing. .film-parts-vehicle stays position:relative (not the literally
   static child described in the design note) because its OWN absolutely
   positioned children -- the pit light and the six xray-dot mirrors --
   need it as their containing block; a truly static box would push that
   containing-block role up to .film-frame instead. Since the two boxes
   are congruent in every layout this file produces, the rendered result
   is identical either way -- relative is simply the safer, unambiguous
   choice. Desktop (≥769px, further down this file) overrides both this
   and .film-hotspot-layer to position:absolute;inset:0, filling the frame
   exactly so every callout coordinate below is frame-relative. */
.film-parts-vehicle {
  position: relative;
  z-index: 1;
  pointer-events: none;
}

/* ---- Amendment 5: seamless photo blending ----
   ONE authoritative mask (the pre-Amendment-5 rule only faded the top/
   bottom edges, leaving the left/right photo edges hard against the
   stage -- the "pasted rectangle" the CEO flagged). Two gradient layers,
   composited with intersect/source-in so the two fades combine into one
   soft vignette on all four edges (~10% of each edge fades to
   transparent, inside the 8-12% band) instead of the union a second mask
   layer would otherwise produce. Do not add a second mask/filter rule
   elsewhere for this element -- consolidate here. */
.film-parts-vehicle .film-body {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 2528 / 1492;
  max-width: none;
  mask-image:
    linear-gradient(to right, transparent 0%, #000 10%, #000 90%, transparent 100%),
    linear-gradient(to bottom, transparent 0%, #000 10%, #000 90%, transparent 100%);
  mask-composite: intersect;
  -webkit-mask-image:
    linear-gradient(to right, transparent 0%, #000 10%, #000 90%, transparent 100%),
    linear-gradient(to bottom, transparent 0%, #000 10%, #000 90%, transparent 100%);
  -webkit-mask-composite: source-in;
  filter: brightness(0.82) contrast(1.06);
}

/* ---- Amendment 4: light ignition + sweep ----
   Coordinates calibrated against the photograph itself (pixel-cluster
   analysis of images/g80-hero-dark.webp, not a guess): the headlight's lit
   running-light strip centers at ~17%/60% of the frame, the taillight's lit
   strip at ~93%/58%. Both blooms are decorative children of the
   aria-hidden .film-parts-vehicle wrapper, positioned as a percentage of
   that same box -- which is frame-relative at every viewport (Amendment 3's
   "one coordinate set" contract) -- so they never drift off the lamps at
   any screen size. Colors are lifted from the photo's own lit lamps (warm
   white running-light strip / red taillight), not decorative orange --
   the sitewide --film-orange token is deliberately unused here. */
.film-lamp {
  position: absolute;
  z-index: 1;
  width: 16%;
  height: 16%;
  border-radius: 50%;
  pointer-events: none;
  mix-blend-mode: screen;
  filter: blur(7px);
  transform: translate(-50%, -50%);
  opacity: 0.5;
}

.film-lamp--head {
  left: 17%;
  top: 60%;
  background: radial-gradient(circle, rgba(255, 244, 219, 0.55) 0%, rgba(255, 244, 219, 0.2) 42%, transparent 72%);
}

.film-lamp--tail {
  left: 93%;
  top: 58%;
  background: radial-gradient(circle, rgba(255, 48, 48, 0.5) 0%, rgba(255, 48, 48, 0.18) 42%, transparent 72%);
}

/* Masked to the vehicle wrapper (not the whole frame -- .film-hotspot-layer,
   the leader lines/panels, must stay unclipped): the wrapper's own box is
   congruent with .film-frame at every viewport (see the position:relative
   note above), so this reads as "masked to the frame" without touching the
   frame's own overflow. */
.film-parts-vehicle {
  overflow: hidden;
}

.film-sheen {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(115deg, transparent 42%, rgba(255, 255, 255, 0.08) 50%, transparent 58%);
  transform: translate3d(-60%, 0, 0);
}

.film-parts.film-stage-armed:not(.film-stage-play) .film-lamp {
  opacity: 0;
}

.film-parts.film-stage-play .film-lamp--head {
  animation: film-lamp-ignite 1.3s var(--film-ease) 0.9s both;
}

.film-parts.film-stage-play .film-lamp--tail {
  animation: film-lamp-ignite 1.3s var(--film-ease) 1s both;
}

.film-parts.film-stage-play .film-sheen {
  animation: film-sheen-sweep 1.2s ease-out 1.3s 1 both;
}

@keyframes film-lamp-ignite {
  0% { opacity: 0; }
  60% { opacity: 1; }
  100% { opacity: 0.5; }
}

@keyframes film-sheen-sweep {
  0% { opacity: 0; transform: translate3d(-60%, 0, 0); }
  15% { opacity: 1; }
  85% { opacity: 1; }
  100% { opacity: 0; transform: translate3d(60%, 0, 0); }
}
/* ---- End Amendment 4 light ignition + sweep. ---- */

.film-hotspot-layer {
  position: relative;
  z-index: 23;
  pointer-events: none;
}

.film-hotspot-item {
  position: absolute;
  z-index: 2;
  margin: 0;
  /* none, not auto: on desktop each item's box now spans from its body
     anchor to its label row (see the @media(min-width:769px) block below)
     instead of sitting as a small pill-sized box, so several items'
     transparent boxes overlap. pointer-events re-enables on the specific
     visible children (.film-hotspot label, .film-hotspot-panel) so the
     empty span between anchor and label never intercepts clicks meant for
     a sibling label or the page. */
  pointer-events: none;
}

.film-hotspot-item[open] {
  z-index: 6;
}

.film-hotspot {
  display: flex;
  min-height: var(--callout-label-h, 44px);
  align-items: center;
  gap: 10px;
  padding: 0;
  border: 0;
  color: var(--film-titanium);
  background: transparent;
  text-align: left;
  list-style: none;
  cursor: pointer;
  /* auto, not inherited none: .film-hotspot-item (its parent) is
     pointer-events:none on desktop now that items span anchor-to-label
     instead of being small pill-sized boxes -- the label itself is the
     one part of that span that must stay clickable. */
  pointer-events: auto;
}

.film-hotspot::-webkit-details-marker {
  display: none;
}

.film-hotspot::marker {
  content: "";
}

/* Amendment 3: plain condensed text label, no pill background box -- the
   leader line and the frame's own dark studio floor are the only
   backdrop. A text-shadow (not a fill) keeps it legible over the
   photograph without reintroducing a box. */
.film-hotspot-copy {
  display: grid;
  gap: 2px;
  text-shadow: 0 1px 3px rgba(3, 4, 5, 0.9), 0 2px 14px rgba(3, 4, 5, 0.65);
}

.film-hotspot-copy small,
.film-hotspot-panel small {
  color: var(--film-orange);
  font-family: var(--font-en);
  font-size: 8px;
  font-weight: 600;
  letter-spacing: 0.18em;
}

.film-hotspot-copy strong {
  font-family: var(--font-kr);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.02em;
}

/* Bordered square, per the Tesla reference -- orange stays limited to
   this control and focus states, never the leader lines themselves. */
.film-hotspot-plus {
  position: relative;
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
  border: 1px solid var(--film-orange);
  background: rgba(3, 4, 5, 0.55);
  transition: background 220ms var(--film-ease), transform 260ms var(--film-ease);
}

.film-hotspot-plus::before,
.film-hotspot-plus::after,
.film-hotspot-close::before,
.film-hotspot-close::after {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 10px;
  height: 1px;
  content: "";
  background: currentColor;
  transform: translate(-50%, -50%);
}

.film-hotspot-plus::after {
  transform: translate(-50%, -50%) rotate(90deg);
}

.film-hotspot:hover .film-hotspot-plus,
.film-hotspot:focus-visible .film-hotspot-plus {
  background: rgba(240, 123, 28, 0.22);
}

.film-hotspot-item[open] .film-hotspot-plus {
  transform: rotate(45deg);
}

/* ---- Amendment 3: Tesla-style leader line ----
   .film-hotspot-line is a SIBLING of its <details> now, not a child --
   confirmed via testing that a native closed <details> collapses
   percentage/auto-based sizing (height:100%, or top+bottom-derived auto
   height) for its non-summary descendants to zero, even when those
   descendants are position:absolute (explicit pixel values like width:1px
   are unaffected; only percentage/auto resolution is). Since the line
   needs a scale-correct, frame-relative HEIGHT (or width, for the elbow
   run), it cannot live inside the <details> the way the label and panel
   do. It instead carries its own copy of the exact anchor-to-label span
   (top/bottom/left/width -- see the per-part rules in the
   @media(min-width:769px) block below, matching .film-hotspot-item--X's
   span) so it resolves those percentages against .film-hotspot-layer
   directly. The line element itself is just that positioning box, with
   no visible size of its own; ::before draws the actual 1px vertical
   stroke pinned to whichever edge is the body anchor, and ::after (the
   two elbowed parts only) draws the short horizontal run to the shifted
   label. Body-end anchor marker: intentionally none -- the line meeting
   the body IS the marker, as in the reference. Color is the existing
   --film-titanium token knocked back to ~40% opacity via color-mix, not
   a new token. */
.film-hotspot-line {
  position: absolute;
  pointer-events: none;
}

.film-hotspot-line::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 1px;
  background: color-mix(in srgb, var(--film-titanium) 40%, transparent);
}

.film-hotspot-line--ecu::before,
.film-hotspot-line--switch::before,
.film-hotspot-line--filler::before {
  /* top row: the span's bottom edge is the body anchor, its top edge (0%)
     is the label row -- the line must grow FROM the anchor, i.e. from
     the box's bottom. */
  transform-origin: bottom;
}

.film-hotspot-line--injector::before,
.film-hotspot-line--reducer::before,
.film-hotspot-line--trunk::before {
  /* bottom row: the span's top edge is the body anchor, so the line
     grows FROM the top. */
  transform-origin: top;
}

/* Elbowed parts (injector, reducer): the front-cluster body points sit
   only ~0.6% apart on the vehicle, so their labels are shifted apart
   horizontally (see the per-part span width/left/right rules below) and
   the leader line gets a right-angle jog -- a vertical run at the anchor
   x (::before, pinned to whichever span edge the anchor sits at), then a
   short horizontal run at the label row over to the shifted label x
   (::after, spanning the span's full width) -- like the angled leads in
   the Tesla reference. Two separate pseudo-elements so each run can
   carry its own reveal transform (scaleY vs scaleX) independently. */
.film-hotspot-line--injector::before {
  left: auto;
  right: 0;
}

.film-hotspot-line--injector::after,
.film-hotspot-line--reducer::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: color-mix(in srgb, var(--film-titanium) 40%, transparent);
}

.film-hotspot-line--injector::after {
  transform-origin: right;
}

.film-hotspot-line--reducer::after {
  transform-origin: left;
}

/* ---- Amendment 4: callout hover/focus response ----
   The anchor dot is its own element (not a third pseudo -- .film-hotspot-
   line already spends both ::before, the vertical stroke, and, on the two
   elbowed parts, ::after, the horizontal run -- so there is no free pseudo
   left to draw a circle from). It is a sibling positioned with the exact
   same --dot-<part>-x/y custom properties the line and label already read
   (declared on .film-frame, inherited down), so it sits precisely at the
   vehicle-body end of its line without touching that line's own geometry
   at all. Hidden (opacity 0) at rest; :has() on the hotspot layer detects
   hover/focus on the matching label without needing a JS-set class, and
   the existing data-film-active-part attribute (set by openHotspot() in
   hero-film.js) gets the identical treatment for the open-panel state. */
.film-hotspot-anchor {
  position: absolute;
  z-index: 3;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--film-titanium);
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, -50%);
  transition: opacity 0.2s var(--film-ease);
}

.film-hotspot-anchor--injector { left: var(--dot-injector-x); top: var(--dot-injector-y); }
.film-hotspot-anchor--ecu      { left: var(--dot-ecu-x);      top: var(--dot-ecu-y); }
.film-hotspot-anchor--switch   { left: var(--dot-switch-x);   top: var(--dot-switch-y); }
.film-hotspot-anchor--reducer  { left: var(--dot-reducer-x);  top: var(--dot-reducer-y); }
.film-hotspot-anchor--filler   { left: var(--dot-filler-x);   top: var(--dot-filler-y); }
.film-hotspot-anchor--trunk    { left: var(--dot-trunk-x);    top: var(--dot-trunk-y); }

.film-hotspot-line::before,
.film-hotspot-line::after {
  transition: background 0.2s var(--film-ease);
}

.film-hotspot-layer:has(.film-hotspot-item--injector .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-line--injector::before,
.film-hotspot-layer:has(.film-hotspot-item--injector .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-line--injector::after,
.film-hotspot-layer:has(.film-hotspot-item--ecu .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-line--ecu::before,
.film-hotspot-layer:has(.film-hotspot-item--switch .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-line--switch::before,
.film-hotspot-layer:has(.film-hotspot-item--reducer .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-line--reducer::before,
.film-hotspot-layer:has(.film-hotspot-item--reducer .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-line--reducer::after,
.film-hotspot-layer:has(.film-hotspot-item--filler .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-line--filler::before,
.film-hotspot-layer:has(.film-hotspot-item--trunk .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-line--trunk::before,
.film-parts[data-film-active-part="injector"] .film-hotspot-line--injector::before,
.film-parts[data-film-active-part="injector"] .film-hotspot-line--injector::after,
.film-parts[data-film-active-part="ecu"] .film-hotspot-line--ecu::before,
.film-parts[data-film-active-part="switch"] .film-hotspot-line--switch::before,
.film-parts[data-film-active-part="reducer"] .film-hotspot-line--reducer::before,
.film-parts[data-film-active-part="reducer"] .film-hotspot-line--reducer::after,
.film-parts[data-film-active-part="filler"] .film-hotspot-line--filler::before,
.film-parts[data-film-active-part="trunk"] .film-hotspot-line--trunk::before {
  background: color-mix(in srgb, var(--film-titanium) 80%, transparent);
}

.film-hotspot-layer:has(.film-hotspot-item--injector .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-anchor--injector,
.film-hotspot-layer:has(.film-hotspot-item--ecu .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-anchor--ecu,
.film-hotspot-layer:has(.film-hotspot-item--switch .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-anchor--switch,
.film-hotspot-layer:has(.film-hotspot-item--reducer .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-anchor--reducer,
.film-hotspot-layer:has(.film-hotspot-item--filler .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-anchor--filler,
.film-hotspot-layer:has(.film-hotspot-item--trunk .film-hotspot:is(:hover, :focus-visible)) .film-hotspot-anchor--trunk,
.film-parts[data-film-active-part="injector"] .film-hotspot-anchor--injector,
.film-parts[data-film-active-part="ecu"] .film-hotspot-anchor--ecu,
.film-parts[data-film-active-part="switch"] .film-hotspot-anchor--switch,
.film-parts[data-film-active-part="reducer"] .film-hotspot-anchor--reducer,
.film-parts[data-film-active-part="filler"] .film-hotspot-anchor--filler,
.film-parts[data-film-active-part="trunk"] .film-hotspot-anchor--trunk {
  opacity: 1;
}
/* ---- End Amendment 4 callout hover/focus response. ---- */

.film-hotspot-panel {
  position: absolute;
  display: grid;
  width: min(320px, 26vw);
  grid-template-columns: 46% 1fr;
  gap: 20px;
  align-items: center;
  padding: 16px;
  border: 1px solid var(--film-hairline);
  background: rgba(3, 4, 5, 0.94);
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.52), inset 0 1px rgba(255, 255, 255, 0.04);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  opacity: 0;
  visibility: hidden;
  transform: translateY(12px);
  transition: opacity 220ms var(--film-ease), transform 300ms var(--film-ease), visibility 220ms;
  /* auto, not inherited none: pointer-events is an inherited property, so
     without this the whole panel (photo, copy, close button) would
     silently inherit none from .film-hotspot-item and become unclickable
     the moment .film-hotspot-item lost its own auto (see the item rule
     above). */
  pointer-events: auto;
}

/* Desktop-enhanced panel left/right/top/bottom positions live inside the
   @media(min-width:769px) block further down this file (with the item
   span and label positions they're paired with) -- NOT here unscoped,
   because .film-hotspot-item--X .film-hotspot-panel (two classes) would
   otherwise outrank the mobile bottom-sheet override's bare
   .film-hotspot-panel selector (one class) purely on specificity and
   silently break the mobile panel position regardless of source order. */

.film-hotspot-item[open] .film-hotspot-panel,
.film-hotspot-panel[data-open] {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.film-hotspot-media {
  min-width: 0;
}
.film-hotspot-panel img {
  width: 100%;
  height: 150px;
  object-fit: contain;
  filter: drop-shadow(0 12px 22px rgba(0, 0, 0, 0.5));
}

.film-hotspot-panel h3 {
  margin: 8px 0 7px;
  font-family: var(--font-kr);
  font-size: 20px;
  line-height: 1.2;
  letter-spacing: -0.04em;
}

.film-hotspot-panel p {
  margin: 0;
  color: var(--film-muted);
  font-family: var(--font-kr);
  font-size: 12px;
  line-height: 1.65;
  word-break: keep-all;
}

.film-hotspot-close {
  position: absolute;
  top: 4px;
  right: 4px;
  display: none;
  width: 44px;
  height: 44px;
  border: 0;
  color: var(--film-muted);
  background: transparent;
  cursor: pointer;
}

.film-hotspots-enhanced .film-hotspot-close {
  display: block;
}

.film-hotspot-close::before {
  transform: translate(-50%, -50%) rotate(45deg);
}

.film-hotspot-close::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.film-hotspot-scrim {
  display: none;
}
@media (min-width: 769px) {
/* ---- Amendment 3: enhanced-desktop callout geometry ----
   .film-parts-vehicle and .film-hotspot-layer fill the aspect-locked
   .film-frame exactly (inset:0), so every coordinate below is a
   percentage of the image itself -- the frame scales with the image, so
   this geometry cannot drift at any viewport size or browser zoom. */
/* Single source for the desktop label's rendered height (44px): both
   .film-hotspot's min-height and the bottom-row line-shortening rule
   further down used to duplicate this literal, able to drift out of
   sync if either changed alone. Declared on .film-hotspots-enhanced (the
   explorer root, an ancestor of both consumers) so it inherits to
   everything below without being a --dot-* coordinate itself. */
.film-hotspots-enhanced {
  --callout-label-h: 44px;
}
.film-parts-vehicle,
.film-hotspot-layer {
  position: absolute;
  inset: 0;
}

.film-parts-vehicle .film-body {
  height: 100%;
}

/* Each item's box spans from its exact body anchor point to its label
   row (~12% top / ~92% bottom of the frame) instead of sitting as a
   small pill-sized box at the anchor -- the .film-hotspot-line sibling
   (see above) carries the SAME span (combined selector below) so its own
   height/width IS the leader line's scale-correct length. Elbowed parts
   (injector, reducer) also span horizontally from anchor to a shifted
   label x, clearing the front-cluster crowding: injector and reducer sit
   only ~0.6% apart on the body, so their labels are pulled apart to x:2%
   and x:28% respectively via a right-angle leader line -- a 26-point
   spread, not the label-row's usual per-part gap, because label text is
   a fixed pixel width but this gap is a frame-relative percentage: at
   .film-frame's smallest supported size (~745-806px, the 1100x800 and
   810x1080 verification viewports) a narrower spread let reducer's
   label text reach back far enough to cover injector's + control,
   confirmed and fixed via the 6x5 hit-test matrix at those sizes. */
.film-hotspot-item--ecu,
.film-hotspot-line--ecu {
  top: 12%;
  bottom: calc(100% - var(--dot-ecu-y));
  left: var(--dot-ecu-x);
}
.film-hotspot-item--switch,
.film-hotspot-line--switch {
  top: 12%;
  bottom: calc(100% - var(--dot-switch-y));
  left: var(--dot-switch-x);
}
.film-hotspot-item--filler,
.film-hotspot-line--filler {
  top: 12%;
  bottom: calc(100% - var(--dot-filler-y));
  right: calc(100% - var(--dot-filler-x));
}
.film-hotspot-item--injector,
.film-hotspot-line--injector {
  top: var(--dot-injector-y);
  bottom: 8%;
  left: 6%;
  width: calc(var(--dot-injector-x) - 6%);
}
.film-hotspot-item--reducer,
.film-hotspot-line--reducer {
  top: var(--dot-reducer-y);
  bottom: 8%;
  left: var(--dot-reducer-x);
  width: calc(37% - var(--dot-reducer-x));
}
.film-hotspot-item--trunk,
.film-hotspot-line--trunk {
  top: var(--dot-trunk-y);
  bottom: 8%;
  right: calc(100% - var(--dot-trunk-x));
}

/* Bottom-row lines (injector/reducer/trunk) share their span's `bottom`
   with the item above (both anchored to the row's far/92% edge), but the
   label itself sits at `bottom:0` of that SAME item and grows UPWARD
   (toward the body) from there -- unlike the top row, where the label
   sits at `top:0` and grows DOWNWARD, away from the line. That asymmetry
   meant the line's far endpoint always coincided with the label's FAR
   edge rather than its NEAR one, so the line ran the label's full
   rendered height (44px, .film-hotspot's min-height below -- confirmed
   the floor for all six once the eyebrow is hidden and the Korean name
   can't wrap, see the label-text rules further down) instead of
   stopping at it. Shortening just the line (not the item, which must
   stay at the row's 8%/92% edge so the label's own position is
   unaffected) by that same height makes the line stop exactly at the
   label's near edge, mirroring the top row's existing zero-gap
   behavior. Reads --callout-label-h (declared on .film-hotspots-enhanced
   above) instead of repeating the 44px literal, so this can't drift out
   of sync with .film-hotspot's min-height the way a second copy could. */
.film-hotspot-line--injector,
.film-hotspot-line--reducer,
.film-hotspot-line--trunk {
  bottom: calc(8% + var(--callout-label-h, 44px) + 8px);
}

/* Junction grammar (both rows): the line stops a uniform 8px short of the
   label group, and the label group is centered on the line's terminal x
   (see the translate rules on the labels below) — the line, the text, and
   the + read as one composed unit instead of a line grazing a corner. */
.film-hotspot-line--ecu,
.film-hotspot-line--switch,
.film-hotspot-line--filler {
  top: calc(12% + var(--callout-label-h, 44px) + 8px);
}

/* Label position: sits at the item's label-row edge (top:0 for the top
   row, bottom:0 for the bottom row) and at whichever horizontal edge is
   the label x (left:0 if the label is the item's left edge, right:0 if
   it's the item's right edge) -- matching the per-part item spans above,
   where the label end is always the OPPOSITE end from the body anchor
   for the two elbowed parts, and the same end for the other four.
   Scoped to .film-hotspots-enhanced: unlike the item span and panel
   rules (which the existing :not(.film-hotspots-enhanced) fallback
   below re-overrides at higher selector specificity), the fallback never
   had to touch .film-hotspot's OWN position property before -- it was
   always a normal-flow child of the absolutely positioned item, never
   itself absolute. Without this scope, position:absolute here would
   leak into the no-JS/no-JS-desktop fallback's 3-column grid too (lower
   specificity than nothing still wins), pulling every label out of its
   grid cell's normal flow, collapsing the cell to near-zero height, and
   stacking rows on top of each other -- confirmed via the fallback's
   real-browser hit-testing regression (a sibling label ended up
   overlapping and intercepting clicks meant for another part). */
.film-hotspots-enhanced .film-hotspot-item--ecu .film-hotspot,
.film-hotspots-enhanced .film-hotspot-item--switch .film-hotspot,
.film-hotspots-enhanced .film-hotspot-item--injector .film-hotspot {
  position: absolute;
  left: 0;
  /* `translate` (not `transform`) so the centering composes with the
     reveal state's transform: translateY(8px) instead of being replaced
     by it — the label never slides horizontally during the fade-in. */
  translate: -50% 0;
}
.film-hotspots-enhanced .film-hotspot-item--filler .film-hotspot,
.film-hotspots-enhanced .film-hotspot-item--reducer .film-hotspot,
.film-hotspots-enhanced .film-hotspot-item--trunk .film-hotspot {
  position: absolute;
  right: 0;
  translate: 50% 0;
}
.film-hotspots-enhanced .film-hotspot-item--ecu .film-hotspot,
.film-hotspots-enhanced .film-hotspot-item--switch .film-hotspot,
.film-hotspots-enhanced .film-hotspot-item--filler .film-hotspot {
  top: 0;
}
.film-hotspots-enhanced .film-hotspot-item--injector .film-hotspot,
.film-hotspots-enhanced .film-hotspot-item--reducer .film-hotspot,
.film-hotspots-enhanced .film-hotspot-item--trunk .film-hotspot {
  bottom: 0;
}

/* Label text, desktop callouts only: Korean part names must never wrap
   (word-break defaults let CJK text break between any two syllables, so
   "충전구" was splitting into "충전"/"구"), and the English eyebrow above
   it was fragmenting into two or three stacked lines ("05 /" / "FUEL" /
   "DOOR") because both this label and its .film-hotspot-item ancestor
   are auto-width absolutely-positioned boxes with no in-flow content of
   their own -- every descendant (summary, panel) is itself absolutely
   positioned -- which collapses their shrink-to-fit width down to
   min-content instead of the intended single-line max-content.
   white-space:nowrap on the Korean name fixes that box-sizing math
   directly (min-content becomes equal to max-content once no break is
   allowed) rather than patching the wrap after the fact. The English
   eyebrow gets a leaner treatment instead of a width fix, matching the
   reference's clean single-line labels and the current strip-the-
   decoration direction: it is hidden outright on desktop callouts, not
   deleted, so the identical markup keeps serving the mobile chip grid
   and the .film-hotspot-panel small (a different element -- see the
   compound selector near .film-hotspot-copy's base rule) untouched. */
.film-hotspots-enhanced .film-hotspot-copy small {
  display: none;
}
.film-hotspots-enhanced .film-hotspot-copy strong {
  white-space: nowrap;
}

/* Panel position: opens from the same edge its label sits at (so it
   reads as growing out of the label, not the retired body-anchored
   pill), offset just past the label's own footprint. Verified via
   elementFromPoint hit-testing at 1440x900 and 1100x800: every other
   part's label and its + control stay clickable while any one panel is
   open. Scoped here (not unscoped near the rest of .film-hotspot-panel)
   so these two-class selectors never outrank the mobile bottom-sheet's
   bare .film-hotspot-panel override on specificity. */
.film-hotspot-item--ecu .film-hotspot-panel,
.film-hotspot-item--switch .film-hotspot-panel,
.film-hotspot-item--injector .film-hotspot-panel {
  left: 0;
}
.film-hotspot-item--filler .film-hotspot-panel,
.film-hotspot-item--reducer .film-hotspot-panel,
.film-hotspot-item--trunk .film-hotspot-panel {
  right: 0;
}
.film-hotspot-item--ecu .film-hotspot-panel,
.film-hotspot-item--switch .film-hotspot-panel,
.film-hotspot-item--filler .film-hotspot-panel {
  top: 44px;
}
.film-hotspot-item--injector .film-hotspot-panel,
.film-hotspot-item--reducer .film-hotspot-panel,
.film-hotspot-item--trunk .film-hotspot-panel {
  bottom: 44px;
}

.film-parts[data-film-hotspot-open] .film-hotspot-item:not([open]),
.film-parts[data-film-hotspot-open] .film-hotspot-line,
.film-parts[data-film-hotspot-open] .film-hotspot-anchor {
  opacity: 0.18;
  transition: opacity 220ms var(--film-ease);
}

.film-parts[data-film-active-part="injector"] .film-hotspot-line--injector,
.film-parts[data-film-active-part="ecu"] .film-hotspot-line--ecu,
.film-parts[data-film-active-part="switch"] .film-hotspot-line--switch,
.film-parts[data-film-active-part="reducer"] .film-hotspot-line--reducer,
.film-parts[data-film-active-part="filler"] .film-hotspot-line--filler,
.film-parts[data-film-active-part="trunk"] .film-hotspot-line--trunk,
.film-parts[data-film-active-part="injector"] .film-hotspot-anchor--injector,
.film-parts[data-film-active-part="ecu"] .film-hotspot-anchor--ecu,
.film-parts[data-film-active-part="switch"] .film-hotspot-anchor--switch,
.film-parts[data-film-active-part="reducer"] .film-hotspot-anchor--reducer,
.film-parts[data-film-active-part="filler"] .film-hotspot-anchor--filler,
.film-parts[data-film-active-part="trunk"] .film-hotspot-anchor--trunk {
  opacity: 1;
}

/* Callout reveal (armed/live defined near the pit-in sequence above):
   scoped here so it only ever hides the desktop label + leader line,
   never the mobile chip grid, which stays permanently visible per the
   proven mobile pattern (Amendment 3 point 5). The line's own box has no
   visible size (see the .film-hotspot-line rule far above) -- ::before
   (the vertical run, all six parts) and ::after (the elbow's horizontal
   run, injector/reducer only) carry the actual scale transforms. */
.film-parts.film-callouts-armed:not(.film-callouts-live) .film-hotspot {
  opacity: 0;
  visibility: hidden;
  transform: translateY(8px);
}
.film-parts.film-callouts-armed:not(.film-callouts-live) .film-hotspot-line::before {
  transform: scaleY(0);
}
.film-parts.film-callouts-armed:not(.film-callouts-live) .film-hotspot-line--injector::after,
.film-parts.film-callouts-armed:not(.film-callouts-live) .film-hotspot-line--reducer::after {
  transform: scaleX(0);
}
.film-parts.film-callouts-armed .film-hotspot {
  transition: opacity 0.45s ease-out, transform 0.45s ease-out, visibility 0.45s;
}
.film-parts.film-callouts-armed .film-hotspot-line::before,
.film-parts.film-callouts-armed .film-hotspot-line::after {
  transition: transform 0.45s ease-out;
}
.film-parts.film-callouts-armed .film-hotspot-item--injector .film-hotspot,
.film-parts.film-callouts-armed .film-hotspot-line--injector::before,
.film-parts.film-callouts-armed .film-hotspot-line--injector::after { transition-delay: 0ms; }
.film-parts.film-callouts-armed .film-hotspot-item--ecu .film-hotspot,
.film-parts.film-callouts-armed .film-hotspot-line--ecu::before { transition-delay: 60ms; }
.film-parts.film-callouts-armed .film-hotspot-item--switch .film-hotspot,
.film-parts.film-callouts-armed .film-hotspot-line--switch::before { transition-delay: 120ms; }
.film-parts.film-callouts-armed .film-hotspot-item--reducer .film-hotspot,
.film-parts.film-callouts-armed .film-hotspot-line--reducer::before,
.film-parts.film-callouts-armed .film-hotspot-line--reducer::after { transition-delay: 180ms; }
.film-parts.film-callouts-armed .film-hotspot-item--filler .film-hotspot,
.film-parts.film-callouts-armed .film-hotspot-line--filler::before { transition-delay: 240ms; }
.film-parts.film-callouts-armed .film-hotspot-item--trunk .film-hotspot,
.film-parts.film-callouts-armed .film-hotspot-line--trunk::before { transition-delay: 300ms; }
/* ---- End Amendment 3 enhanced-desktop block. ---- */

/* Before JavaScript enhances the section (or it never runs), the stage's
   cinematic one-viewport height+clip must not swallow expanding native
   <details> panels -- fall back to normal document flow so every part's
   photo and copy stay reachable and push the rest of the page down, exactly
   like the .film-parts-stage fallback immediately below. */
.film-parts:not(.film-hotspots-enhanced) .film-stage {
  height: auto;
  min-height: min(100svh, 1080px);
  overflow: visible;
}

.film-parts:not(.film-hotspots-enhanced) .film-parts-stage {
  min-height: 0;
  overflow: visible;
}

.film-parts:not(.film-hotspots-enhanced) .film-parts-vehicle {
  position: relative;
  inset: auto;
  height: clamp(360px, 42vw, 520px);
  overflow: hidden;
}

.film-parts:not(.film-hotspots-enhanced) .film-parts-vehicle .film-body {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
}

.film-parts:not(.film-hotspots-enhanced) .film-hotspot-layer {
  position: relative;
  inset: auto;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  padding: 36px var(--film-gutter) 80px;
  pointer-events: auto;
}

.film-parts:not(.film-hotspots-enhanced) .film-hotspot-item,
.film-parts:not(.film-hotspots-enhanced) .film-hotspot-item--injector,
.film-parts:not(.film-hotspots-enhanced) .film-hotspot-item--ecu,
.film-parts:not(.film-hotspots-enhanced) .film-hotspot-item--switch,
.film-parts:not(.film-hotspots-enhanced) .film-hotspot-item--reducer,
.film-parts:not(.film-hotspots-enhanced) .film-hotspot-item--filler,
.film-parts:not(.film-hotspots-enhanced) .film-hotspot-item--trunk {
  position: relative;
  top: auto;
  right: auto;
  bottom: auto;
  left: auto;
  width: 100%;
}

.film-parts:not(.film-hotspots-enhanced) .film-hotspot {
  width: 100%;
  min-height: 58px;
  justify-content: space-between;
  gap: 10px;
  padding: 8px;
  border: 1px solid var(--film-hairline);
  background: rgba(3, 4, 5, 0.78);
}

/* The leader line is a JS-enhanced/desktop affordance connecting an
   anchored label to the body -- meaningless once the label is back in
   normal document-flow grid cells, so it's hidden here the same way
   mobile already hides it. */
.film-parts:not(.film-hotspots-enhanced) .film-hotspot-line,
.film-parts:not(.film-hotspots-enhanced) .film-hotspot-anchor {
  display: none;
}

.film-parts:not(.film-hotspots-enhanced) .film-hotspot-item[open] {
  grid-column: 1 / -1;
}

.film-parts:not(.film-hotspots-enhanced) .film-hotspot-item .film-hotspot-panel {
  position: relative;
  inset: auto;
  width: 100%;
  margin-top: 12px;
  transform: none;
}
/* End desktop native-disclosure fallback. */
}
.film-corner {
  position: absolute;
  z-index: 22;
  width: 40px;
  height: 40px;
  opacity: 0.42;
  pointer-events: none;
}

.film-corner--top {
  top: clamp(92px, 11vh, 116px);
  right: var(--film-gutter);
  border-top: 1px solid var(--film-hairline);
  border-right: 1px solid var(--film-hairline);
}

.film-corner--bottom {
  right: var(--film-gutter);
  bottom: clamp(28px, 5vh, 52px);
  border-right: 1px solid var(--film-hairline);
  border-bottom: 1px solid var(--film-hairline);
}

@media (prefers-reduced-motion: reduce) {
  .film-fog span {
    animation: none;
  }

  .film-action,
  .film-hotspot,
  .film-hotspot-plus,
  .film-hotspot-line::before,
  .film-hotspot-line::after,
  .film-hotspot-panel {
    transition: none;
  }

  .film-parts-vehicle,
  .film-parts-vehicle .film-body,
  .film-pit-light,
  .film-xray-dot {
    transition: none;
  }

  /* Amendment 5: belt-and-suspenders -- initializeCalloutReveal() already
     never adds film-callouts-armed under reduced motion (same guard as
     the pit-in sequence), so the lowered state is never reachable, but
     pin the upper position and kill the transition explicitly here too. */
  .film-opening {
    transform: none;
    transition: none;
  }

  .film-lamp,
  .film-sheen {
    animation: none;
    opacity: 0.5;
  }

  .film-sheen {
    opacity: 0;
  }

  .film-hotspot-line::before,
  .film-hotspot-line::after,
  .film-hotspot-anchor {
    transition: none;
  }
}

/* ---- Pit-in stage: steady (final) state ----
   .film-parts-vehicle holds .film-body, the studio G80 photograph.
   Three classes deep so this outranks both index.css's sitewide
   pre-load hide (img[loading="lazy"] { opacity: 0 }, specificity 0-1-1)
   -- which stays in effect for no-JS visitors since nothing ever adds
   .loaded -- and its lazy-load fade-in (img[loading="lazy"].loaded
   { opacity: 1 }, specificity 0-2-1) once site-wide JS (index.js) does
   add .loaded on the image's "load" event. Either way this opacity
   wins and the photograph renders at its studio treatment. */
.film-parts .film-parts-vehicle .film-body {
  opacity: 0.88;
}

.film-pit-light {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: 2%;
  height: 2px;
  background: linear-gradient(90deg, transparent, rgba(240, 123, 28, 0.85), transparent);
  transform: scaleX(1);
}

/* ---- Pit-in entrance: armed = offset start, play = run to steady ---- */
.film-parts.film-stage-armed:not(.film-stage-play) .film-parts-vehicle {
  transform: translateX(58%);
}

.film-parts.film-stage-armed:not(.film-stage-play) .film-pit-light {
  transform: scaleX(0);
}

.film-parts.film-stage-armed .film-parts-vehicle {
  transition: transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}

.film-parts.film-stage-armed .film-pit-light {
  transform-origin: center;
  transition: transform 0.5s ease-out 0.55s;
}

/* ---- Amendment 3: Tesla-style callout reveal (desktop leader lines/
   labels only as of Amendment 5) ----
   The six leader lines/labels (desktop, scoped further down inside the
   @media(min-width:769px) block so the mobile chip grid is never touched)
   arm hidden on enhance and draw in once on the visitor's first scroll
   intent, mirroring the same armed/play idiom: film-callouts-armed hides
   everything behind visibility:hidden (so a hidden control is
   unfocusable, same as the old film-stage-armed pill hiding), and
   film-callouts-live runs the reveal. Both classes are removed ~1.5s
   after live so the classless steady state (no JS / reduced motion / the
   user never scrolls) is simply "always visible" -- see
   initializeCalloutReveal() in hero-film.js for the scroll-intent
   trigger and its guards.
   The six on-body mirror dots used to be wired into this same
   scroll-gated reveal (all six popping to full opacity on first scroll)
   -- Amendment 5 removes them from it: per CEO direction the mobile dots
   must stay hidden at every scroll position and only appear for the
   currently active part, so they get their own permanent
   data-film-active-part-driven visibility below, independent of
   film-callouts-armed/live. */

/* ---- Amendment 5: mobile mirror dots -- hidden by default, only the
   active part's dot appears while its chip/bottom-sheet is open. Desktop
   still hides these entirely (display:none, hero-film-responsive.css);
   the leader-line labels carry the callout there instead. Shrunk from a
   12px dot + 4px ring (the "six oversized overlapping orange blobs" CEO
   flagged, visible permanently because the old scroll-reveal set them
   to a standing 0.35 opacity with no active part) to an 11px dot + a
   subtle 2px ring, invisible until its own part is active. */
.film-xray-dot {
  position: absolute;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: #f07b1c;
  box-shadow: 0 0 0 2px rgba(240, 123, 28, 0.35);
  transform: translate(-50%, -50%);
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s var(--film-ease), visibility 0.2s;
}

.film-xray-dot--injector { left: var(--dot-injector-x); top: var(--dot-injector-y); }
.film-xray-dot--ecu      { left: var(--dot-ecu-x);      top: var(--dot-ecu-y); }
.film-xray-dot--switch   { left: var(--dot-switch-x);   top: var(--dot-switch-y); }
.film-xray-dot--reducer  { left: var(--dot-reducer-x);  top: var(--dot-reducer-y); }
.film-xray-dot--filler   { left: var(--dot-filler-x);   top: var(--dot-filler-y); }
.film-xray-dot--trunk    { left: var(--dot-trunk-x);    top: var(--dot-trunk-y); }

.film-parts[data-film-active-part="injector"] .film-xray-dot--injector,
.film-parts[data-film-active-part="ecu"] .film-xray-dot--ecu,
.film-parts[data-film-active-part="switch"] .film-xray-dot--switch,
.film-parts[data-film-active-part="reducer"] .film-xray-dot--reducer,
.film-parts[data-film-active-part="filler"] .film-xray-dot--filler,
.film-parts[data-film-active-part="trunk"] .film-xray-dot--trunk {
  opacity: 1;
  visibility: visible;
  box-shadow: 0 0 0 2px rgba(240, 123, 28, 0.55);
}

/* ---- Detach micro-motion: the panel grows out of its dot ----
   Scoped to desktop/tablet only: the mobile bottom-sheet in
   hero-film-responsive.css sets its own transform (translateY) on the
   same [data-open] state for the <=768px layout, and letting both
   apply at once would leave the result to file load order instead of
   a deliberate rule. */
@media (min-width: 769px) {
  .film-hotspots-enhanced .film-hotspot-panel {
    transform: scale(0.92);
    transform-origin: top left;
  }
  .film-hotspots-enhanced .film-hotspot-panel[data-open] {
    transform: scale(1);
  }

  /* Per-part detach origin: the panel must grow from whichever corner
     sits nearest its own label, matching the left/right/top/bottom
     panel-position rules above -- otherwise the scale motion reads as
     detaching from the wrong side of the label. Parts not listed (ecu,
     switch) keep the default top-left origin. */
  .film-hotspot-item--filler .film-hotspot-panel {
    transform-origin: top right;
  }
  .film-hotspot-item--injector .film-hotspot-panel {
    transform-origin: bottom left;
  }
  .film-hotspot-item--reducer .film-hotspot-panel,
  .film-hotspot-item--trunk .film-hotspot-panel {
    transform-origin: bottom right;
  }
}
