html{ height:100%; margin:0; background:#000000; transition: background-color .5s ease; }
  body{ margin:0; background:#000000; transition: background-color .5s ease; }
  /* Scrolling is genuinely disabled (not just visually hidden) until the
     hero has finished its automatic sequence -- see .scroll-armed below,
     added once heroSettled fires (same gate the nav-mode transition
     already uses). Before that, .content-below exists in the DOM (so its
     layout is ready) but contributes no scrollable height, so there is
     nothing to scroll to at all. Without this, the page was scrollable
     from the very first frame, letting a user scroll straight into the
     content cards while the idle video and Enter prompt were still
     sitting there untouched -- confusing and, worse, skips the entire
     intended sequence. */
  html{ overflow:hidden; }
  body{ overflow:hidden; }
  /* Only the root scrolls. `overflow:auto` on BODY as well made body its
     own scroll container, and a sticky element's nearest scrollport is
     then body rather than the viewport -- so the highlights section's
     pin never engaged and its frame scrolled away with the page
     (measured: frame top -7646 where it should have been 0). Body goes
     back to visible; html still does the scrolling, and the pre-armed
     lock above is unaffected because html:hidden alone locks it. */
  html.scroll-armed{ overflow:auto; }
  html.scroll-armed body{ overflow:visible; }
  /* This only hides the browser's own scrollbar track/thumb graphic
     (which would otherwise render as a light-colored bar standing out
     against the all-black page) -- the custom "Scroll Down" cue is the
     intended affordance in its place, and only appears once scrolling
     is actually armed. */
  html{ scrollbar-width: none; -ms-overflow-style: none; }
  html::-webkit-scrollbar{ display: none; width: 0; height: 0; }
  .stage{
    position: relative;
    height:100vh; height:100dvh;
    display:flex; align-items:center; justify-content:center;
    padding: 4vmin;
    box-sizing: border-box;
  }

  /* Wraps the existing stage so it can be scaled down automatically once
     the third video completes its first rotation. A transformed element
     becomes the containing block for any position:fixed descendants (per
     the CSS Transforms spec), so once this has a transform, the videos
     inside .stage -- still styled exactly as before -- resolve their
     left:50%/top:50% positioning against THIS box instead of the true
     viewport. Since this box is itself fixed and sized to fully cover
     the viewport, that's the same reference frame as before; only the
     added scale() changes anything.
       transform-origin is bottom-center, not center: the video's own
     bottom edge sits at a near-constant ~100.9% of viewport height
     (verified directly, holds across viewport sizes), so anchoring the
     scale there keeps that edge fixed in place as it shrinks, instead of
     shrinking symmetrically toward the middle of the screen. No scroll
     is involved anywhere -- this is a one-time, timed transition. */
  #heroScale{
    position: fixed;
    top:0; left:0; width:100%; height:100%;
    transform: scale(1) translateZ(0);
    transform-origin: 50% 100%;
    will-change: transform;
  }
  #heroScale.shrunk{
    transform: scale(0.6667) translateZ(0);
    /* Defined here, not on .nav-mode, deliberately: .shrunk stays
       applied permanently once the hero settles, while .nav-mode is
       what toggles back and forth. A transition scoped only to
       .nav-mode stops applying the instant that class is removed --
       which meant reversal (scrolling back to the top) wasn't
       animating at all, just jumping instantly to the resting state,
       since no transition rule was left active by the time the class
       was gone. Keeping it here means it's active either way. */
    transition: transform 1s cubic-bezier(.65,0,.35,1), opacity .4s ease-out;
  }
  /* The shrink is a declarative CSS animation, NOT JS setting style
     values per frame. Browsers run transform keyframe animations on the
     compositor thread: after the single class change that starts it,
     the main thread does no per-frame work at all, and the playing
     video's presentation pipeline is never repeatedly invalidated by
     style recalcs. This is the standard fix for animation-induced
     video stutter. */
  @keyframes hero-shrink{
    from{ transform: scale(1) translateZ(0); }
    to{ transform: scale(0.6667) translateZ(0); }
  }
  #heroScale.shrinking{
    animation: hero-shrink 2s cubic-bezier(.65,0,.35,1) forwards;
  }

  /* Logo reveal: fixed near the top, independent of the scaling wrapper
     above so it never shrinks itself. Appears once, automatically, at
     the same moment the video finishes shrinking -- triggered by a
     timer (see script below), not by any scroll. The SVG's own full
     entrance sequence (arrow, rays, letters, HARMAN) plays from the
     start via the .playing class added at that moment. */
  .logo-reveal{
    position: fixed;
    top: 17vh; left:50%;
    transform-origin: top left;
    width: clamp(300px, 54vmin, 640px);
    transform: translate(-50%, 0);
    opacity: 0;
    /* Above theme-burst's z-index (25) so the star -- which now
       visibly grows during stage 2 instead of the old ring effect --
       stays on top as it expands, rather than being painted over by
       the white cover catching up underneath it. */
    z-index: 26;
    pointer-events: none;
    transition: opacity .35s ease;
  }
  .logo-reveal svg{
    width:100%; height:auto; display:block;
    /* SVGs clip their own content to their viewport by default. Once
       the star's counter-transform correctly keeps it at its original,
       larger on-screen position while its parent SVG shrinks down to
       nav-bar size, that position sits far outside the SVG's own now-
       tiny bounds -- and gets silently clipped away without this. */
    overflow: visible;
  }
  /* The actual branded "HARMAN Forum" logo file -- used once settled
     in light mode, in place of the hand-built SVG recreation. The SVG
     drives the whole animated entrance (letters traveling, the star
     detaching, growing, bursting) and stays essential for that, but
     once that sequence is done, the real logo asset is what should
     actually sit in the nav bar rather than a single-color CSS
     recoloring of a hand-drawn approximation. Position/size are set
     via JS to exactly match logoReveal's own measured box at the
     moment of the swap, not hardcoded here, since that box's exact
     dimensions depend on the nav bar's own responsive sizing. */
  .nav-logo-image{
    position: fixed;
    opacity: 0;
    /* Above the nav in every state, including the one where the nav is
       itself lifted to 260 to stay clickable over the full-bleed film.
       At 20 it lost to that lift and the nav painted over its own logo,
       leaving an empty gap for the whole takeover -- elementFromPoint at
       the logo's centre returned the nav. It only ever occupies space
       inside the nav bar, so sitting above it costs nothing. */
    z-index: 265;
    pointer-events: none;
    transition: opacity .35s ease;
  }
  .nav-logo-image.visible{ opacity: 1; }
  /* Nav-mode: the SAME element travels here -- not a second copy. A
     plain transform/opacity transition (reversible by nature: remove
     the class and it eases straight back), toggled by scroll position
     crossing a threshold, not a one-shot trigger. Values tuned
     empirically against the actual rendered result. */
  .logo-reveal{
    transition: opacity .35s ease, transform .9s cubic-bezier(.65,0,.35,1);
  }
  .logo-reveal.nav-mode{
    transform: translate(-50%, 0) translate(var(--nav-logo-tx, 0px), var(--nav-logo-ty, 0px)) scale(var(--nav-logo-scale, 1));
  }
  /* Date/location fade with the nav travel -- reversible the same way.
     Only meaningful once heroSettled locks in plain inline opacity/
     transform (see script), freeing these from the entrance animation's
     hold so a simple CSS transition can drive them from here on. */
  #event-date, #event-location{
    transition: opacity .5s ease;
  }
  .logo-reveal.nav-mode #event-date,
  .logo-reveal.nav-mode #event-location{
    opacity: 0 !important;
  }

  /* Both videos render at the exact same CSS size — no zoom, no scale
     difference between them at all. This width was chosen so the orb
     inside the idle video's own frame lands at clamp(140px, 52vmin,
     620px) — the project's original design size for the visible orb —
     and since the reveal video's orb occupies almost the same fraction
     of its own frame (31.72% vs 32.38%), it lands within ~2% of that
     same size at this identical width. Because both elements are always
     this one fixed size, swapping which one is visible is a plain,
     instant cut — nothing to animate, nothing to keep in sync. */
  .video-frame{
    position:fixed; left:50%; top:50%;
    width: clamp(432.4px, 160.6vmin, 1914.8px);
    height: auto;
    display:block;
    pointer-events:none;
  }
  /* Each video's own orb sits at a slightly different fractional position
     within its native frame (measured directly from each source file).
     Translating each by its own measured fraction keeps the orb itself
     anchored at the same screen position across the cut, even though the
     two frames' internal layouts aren't pixel-identical. */
  .idle-video{
    transform: translate(-50.14%, -43.68%);
    z-index:1;
  }
  .reveal-video{
    transform: translate(-50.13%, -43.84%);
    z-index:1;
    opacity:0;
  }
  .reveal-video.active{ opacity:1; }
  /* Third video: the final resting loop, taking over once the reveal
     finishes. Its content is a direct continuation of the reveal video's
     own last frame -- confirmed by measuring both directly: the
     illuminated content's bounding box matches to within 0.03% between
     reveal's last frame and this video's first frame. Because the reveal
     video's own window was never re-centered on its content (it's fixed,
     calibrated only to the tiny orb at t=0), its last frame lands
     off-center on screen -- so this video must inherit that exact same
     landing spot, not be independently centered on its own content. */
  .final-video{
    transform: translate(-50.13%, -43.84%);
    z-index:1;
    opacity:0;
  }
  .final-video.active{ opacity:1; }

  /* Skip button: top-right, hidden until Enter is clicked, hidden again
     once its job is done (either clicked, or scroll naturally unlocks
     anyway at the normal 8s mark). */
  /* Scroll-down cue: left edge, hidden through the whole hero sequence,
     fades in only once the final state has settled (video shrunk, logo
     formed). Vertical "SCROLL DOWN" text with a thin track line and a
     bright segment that continuously travels downward along it. */
  .scroll-cue{
    position: fixed;
    /* Above content-below's z-index (10): content-below isn't fixed,
       so as scrollY increases its own top edge moves further up the
       viewport, and with a higher z-index and a solid opaque
       background, it was rendering directly on top of this fixed cue
       from the bottom up -- covering the line first, then progressively
       more of the text itself the deeper into stage 1 you scrolled.
       Confirmed directly: at every scroll depth tested, content-below's
       top edge sat above the line's bottom edge, fully opaque. */
    left: 3vmin; bottom: 6vmin; z-index: 11;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    background: none; border: none; padding: 0; margin: 0;
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transition: opacity .6s ease;
  }
  .scroll-cue.visible{ opacity: 1; pointer-events: auto; }
  /* Hidden while the photo ribbon is on screen: the strip runs full-bleed,
     so the cue would otherwise sit on top of the leftmost card. Declared
     after .visible so it wins whenever both apply. */
  .scroll-cue.over-ribbon{ opacity: 0; pointer-events: none; }
  /* The cue's ink is a variable so the text, its track and the dropping
     pulse all switch together in light mode -- it was white-on-white and
     invisible there, sitting at full opacity the whole time. Colours are
     HARMAN Blue for the label and Ideate Gray for the track, and the
     .5s ease matches the colour-transition timing used site-wide. */
  html.light-mode .scroll-cue{
    --cue-ink: #00609C;
    --cue-track: rgba(90,111,122,.35);
  }
  .scroll-cue-text{
    writing-mode: vertical-rl;
    color: var(--cue-ink, #fff);
    transition: color .5s ease;
    font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
    font-size: clamp(10px, 1.5vmin, 13px);
    font-weight: 500;
    letter-spacing: .35em;
    text-transform: uppercase;
  }
  .scroll-cue-line{
    width: 1px;
    height: clamp(40px, 8vmin, 72px);
    background: var(--cue-track, rgba(255,255,255,.25));
    transition: background-color .5s ease;
    position: relative;
    overflow: hidden;
  }
  .scroll-cue-line::after{
    content: '';
    position: absolute;
    left: 0; top: -40%;
    width: 100%; height: 40%;
    background: var(--cue-ink, #fff);
    animation: cue-drop 1.8s cubic-bezier(.65,0,.35,1) infinite;
  }
  @keyframes cue-drop{
    from{ top: -40%; }
    to{ top: 110%; }
  }

  .skip-btn{
    position: fixed;
    top: 3vmin; right: 3vmin; z-index: 6;
    padding: .6em 1.2em;
    background: none;
    border: 1px solid rgba(255,255,255,.5);
    color: #fff;
    font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
    font-size: clamp(11px, 1.6vmin, 14px);
    font-weight: 500;
    letter-spacing: .2em;
    text-transform: uppercase;
    cursor: pointer;
    opacity: 0;
    pointer-events: none;
    transition: opacity .3s ease, border-color .2s ease;
  }
  .skip-btn:hover{ border-color: rgba(255,255,255,.9); }
  .skip-btn.visible{ opacity: 1; pointer-events: auto; }

  /* "Click to enter" — no button chrome at all. The clickable hit-area
     (.enter-hit) is sized to the visible orb (not the full video frame)
     and clipped to a circle, so the orb itself is the click target. It
     sits on top in z-index but is fully transparent — the video shows
     through untouched. The corner brackets + "Enter" text are a purely
     visual, non-boxed indicator (.enter-reticle) centered inside it. */
  .enter-hit{
    position:absolute; top:50%; left:50%; z-index:2;
    transform:translate(-50%,-50%);
    width: clamp(140px, 52vmin, 620px);
    height: clamp(140px, 52vmin, 620px);
    display:flex; align-items:center; justify-content:center;
    background:none; border:none; padding:0; margin:0;
    cursor:pointer;
    clip-path: circle(50%);
    -webkit-tap-highlight-color: transparent;
  }
  .enter-hit:focus-visible{ outline:2px solid #fff; outline-offset:6px; border-radius:50%; }
  .enter-reticle{
    position:relative;
    display:inline-flex; align-items:center; justify-content:center;
    padding: clamp(10px,2vmin,16px) clamp(26px,5.5vmin,42px);
    color:#fff;
    font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
    font-size: clamp(11px, 2.1vmin, 14px);
    font-weight:500;
    letter-spacing:.3em;
    text-transform:uppercase;
    transition: transform .25s ease, opacity .25s ease;
  }
  .enter-hit:hover .enter-reticle,
  .enter-hit:focus-visible .enter-reticle{ transform:scale(1.08); }
  .enter-hit:active .enter-reticle{ transform:scale(.94); }
  .enter-corner{ position:absolute; width:.8em; height:.8em; border:1.5px solid rgba(255,255,255,.9); pointer-events:none; }
  .enter-corner-tl{ top:-.5em; left:-.5em; border-right:none; border-bottom:none; }
  .enter-corner-br{ bottom:-.5em; right:-.5em; border-left:none; border-top:none; }

  /* Custom cursor: a small dot that tracks the pointer exactly, and a
     larger ring that trails it with an eased lag. mix-blend-mode:
     difference keeps both visible everywhere -- white over the black
     page, inverted over the white logo/text. Gated to fine-pointer
     hover devices only: touch users keep their native behavior, and
     the native cursor is only hidden where the custom one exists.
       Per-frame movement is transform-only on these two tiny,
     independently-composited elements (will-change) -- never touching
     layout or any ancestor of the videos, so it cannot disturb video
     frame presentation the way ancestor style churn did. */
  @media (hover: hover) and (pointer: fine){
    html, body, .enter-hit, .skip-btn, .scroll-cue{ cursor: none; }
    .cursor-dot, .cursor-ring{
      position: fixed; top: 0; left: 0;
      pointer-events: none;
      z-index: 9999;
      border-radius: 50%;
      opacity: 0;
      mix-blend-mode: difference;
      will-change: transform;
    }
    .cursor-dot{
      width: 8px; height: 8px;
      margin: -4px 0 0 -4px;
      background: #fff;
      transition: opacity .25s ease, width .2s ease, height .2s ease, margin .2s ease;
    }
    .cursor-ring{
      width: 38px; height: 38px;
      margin: -19px 0 0 -19px;
      border: 1px solid rgba(255,255,255,.65);
      transition: opacity .25s ease, width .2s ease, height .2s ease, margin .2s ease, border-color .2s ease;
    }
    .cursor-dot.active, .cursor-ring.active{ opacity: 1; }
    /* The cursor's mix-blend-mode:difference (below) is what keeps it
       visible against varying page content elsewhere on the site --
       but it's also exactly what turns it dark: a white cursor
       differenced against a white background computes to black. At
       z-index 9999 it sits above every element in the whole burst
       sequence, including the rings -- so wherever the pointer happens
       to be (often near center, since that's what's being watched)
       during or after the transition to a white background, it
       appears as an unrelated dark ring layered on top of the actual,
       correctly-light burst rings. This was the real "dark ring" --
       not a color or z-index problem in the rings themselves at all.
       html.burst-active forces it hidden for the duration, overriding
       the mousemove-driven .active class regardless of pointer
       movement during that window. */
    html.burst-active .cursor-dot,
    html.burst-active .cursor-ring{ opacity: 0 !important; }
    .cursor-ring.hover{
      width: 58px; height: 58px;
      margin: -29px 0 0 -29px;
      border-color: rgba(255,255,255,.95);
    }
    .cursor-dot.hover{
      width: 4px; height: 4px;
      margin: -2px 0 0 -2px;
    }
  }

  /* Content below the hero. The hero (#heroScale, .logo-reveal,
     .skip-btn, .scroll-cue) is entirely position:fixed, so it takes zero
     space in normal document flow and its own internals are completely
     untouched by anything here. This section starts one viewport-height
     down (margin-top:100vh) so the hero gets its full, uninterrupted
     moment first -- then, as the user scrolls, this section's own
     opaque background scrolls up from below and progressively covers
     the pinned hero, which keeps quietly playing underneath the whole
     time. z-index just needs to clear the hero's fixed elements
     (5/6) and stay under the cursor (9999). */
  /* Nav bar background: just the fixed bar itself. The logo that
     appears "docked" in it isn't a second copy -- it's the same
     .logo-reveal element, transformed to sit here (see .logo-reveal.
     nav-mode below). Reversible: this is a plain opacity/transform
     transition toggled by a class, not a one-shot animation, so it
     runs backward automatically when the class is removed. */
  .site-nav{
    position: fixed; top: 2.5vmin; left:50%; z-index:15;
    transform: translateX(-50%) translateY(-150%);
    width: fit-content;
    max-width: 92vw;
    height: clamp(64px, 9vh, 84px);
    display: flex;
    align-items: center;
    gap: clamp(14px, 1vw, 16px);
    padding-top: 0;
    padding-bottom: 0;
    padding-right: clamp(24px, 2vw, 32px); /* small -- items sit close to the right border, not floating with dead space after them */
    padding-left: clamp(320px, 22vw, 380px); /* logo clearance; all the "slack" lives here, before the menu, not after it */
    border: 1px solid rgba(255,255,255,.55);
    border-radius: 16px;
    background: rgba(0,0,0,.82);
    backdrop-filter: blur(6px);
    opacity: 0;
    pointer-events: none;
    transition: opacity .8s cubic-bezier(.65,0,.35,1), transform .8s cubic-bezier(.65,0,.35,1),
                background-color .5s ease, border-color .5s ease;
  }
  .site-nav.visible{
    opacity: 1;
    transform: translateX(-50%) translateY(0);
    /* The base rule sets pointer-events:none so the parked, invisible
       nav cannot swallow clicks over the hero. Restoring it here was
       missing, which meant the nav was never actually clickable once it
       appeared -- links included, not just the new Intro button. */
    pointer-events: auto;
  }
  .nav-link{
    color: rgba(255,255,255,.85);
    font-family: 'Century Gothic', 'Futura', 'Trebuchet MS', ui-sans-serif, sans-serif;
    font-size: clamp(15px, 1.3vw, 17px);
    font-weight: 500;
    letter-spacing: .03em;
    text-decoration: none;
    white-space: nowrap;
    /* .5s to match site-nav's own background/border-color transition
       and content-below's background-color transition below -- these
       were at .25s, finishing twice as fast as the background they
       sit on top of, which is what let text settle into its new color
       while the background underneath it was still visibly mid-shift. */
    transition: color .5s ease;
  }
  .nav-link:hover{ color: #40F8F7; }
  .nav-cta{
    color: #000;
    background: #ffffff;
    padding: .55em 1.15em;
    border-radius: 8px;
    font-weight: 600;
    /* Same .5s unification as .nav-link above. */
    transition: background .5s ease, color .5s ease;
  }
  .nav-cta:hover{ background: #40F8F7; color:#000; }

  /* Hero video: reversible slide-away, toggled the same way. Reuses
     #heroScale's existing scale(0.6667) resting state and adds a
     downward slide + fade on top of it. Transform/opacity only, so
     this stays compositor-friendly the same way the original shrink
     keyframes did -- the difference here is a transition (which
     naturally reverses) instead of a one-shot @keyframes animation. */
  #heroScale.nav-mode{
    transform: scale(0.6667) translateY(45vh) translateZ(0);
    opacity: 0;
    /* Opacity is intentionally much faster than the slide: .content-
       below sits at z-index:10 starting exactly at the 100vh mark (by
       design, so real content can cover the hero once it exists there
       again) -- but that means as the video slides down into that same
       region, if it's still substantially opaque when it crosses that
       boundary, content-below's panel clips it mid-fade, reading as a
       hard cutoff rather than a disappearance. Finishing the fade in
       well under half the slide's duration means the video is already
       invisible long before it would ever reach that boundary, so
       there's nothing left for the overlay to visibly cut off -- it
       simply fades away as it starts to move, then continues sliding
       (invisibly) for the rest of the 1s. The transition itself lives
       on .shrunk (see above), not here, so it stays active on reversal. */
  }

  /* Ambient background effect for stage 1 (star detached, sitting at
     center): thin dashed lines sweep in from the left and right edges
     and curve toward the star's resting position, with a small
     particle traveling along each one -- adapted from a reference
     canvas animation, re-pointed at the star instead of a blank
     center point, and recolored to HARMAN's blue palette. Sits above
     the plain background/content-below but below the nav, beat-lines,
     and the star itself, so those all render cleanly on top of it
     and the lines read as flowing toward and behind the star, not
     over it. Only active during stage 1 -- hidden during the hero's
     own video sequence, during the burst, and once settled in light
     mode, since content-below and the branded (colored) logo replace
     it as the visual focus at that point. */
  /* ---- Dark-mode surface (hero stages) ----------------------------
     The same grid + aurora used on the light side, inverted, so the
     dark stages and the light ones read as one continuous system
     rather than two unrelated backgrounds.

     It lives INSIDE #heroScale on purpose. mix-blend-mode only blends
     with what sits below it inside the nearest stacking context, and
     #heroScale is transformed, which makes it one -- a surface placed
     outside it would be invisible to the videos' blending. The cost of
     living inside is that it inherits the hero's shrink, so it carries
     an exact counter-scale below.

     Hidden the moment light mode takes over: the light canvas on
     .content-below continues the same surface from there. */
  .hero-surface{
    position: fixed; inset: 0; z-index: 0;
    pointer-events: none; overflow: hidden;
    opacity: 1; transition: opacity .4s ease;
  }
  html.light-mode .hero-surface{ opacity: 0; }
  /* Matched at the depth of the rules that set the animations
     (.hs-orb.oN .tN is three classes), or it loses on specificity. */
  html.light-mode .hero-surface .hs-orb,
  html.light-mode .hero-surface .hs-orb .tint{ animation-play-state: paused; }

  /* The composite happens on the hero as a whole, not on each video.
     mix-blend-mode only blends with what is beneath it inside the
     nearest stacking context, and #heroScale is transformed, which
     makes it one -- so a surface placed inside it would be the only
     thing the videos could blend with, and a surface placed outside
     would be invisible to them. Blending the wrapper instead lets it
     composite against the page itself. The wrapper holds nothing but
     the videos and the Enter control, both of which are light-on-black
     already, so screen treats all of it correctly: black drops out,
     the hologram and the white reticle survive.
     This also means the surface no longer inherits the hero's shrink,
     so it needs no counter-scale, and it stays visible through stage 1
     where the hero itself fades to nothing. */
  #heroScale{ mix-blend-mode: screen; }

  .hs-aurora{ position:absolute; inset:0; z-index:0; }
  .hs-orb{ position:absolute; border-radius:50%; will-change:transform; }
  .hs-orb .tint{ position:absolute; inset:0; border-radius:50%; opacity:0; will-change:opacity; }
  @keyframes hs-tint-a{ 0%,100%{opacity:1} 28%{opacity:1} 39%{opacity:0} 89%{opacity:0} }
  @keyframes hs-tint-b{ 0%{opacity:0} 28%{opacity:0} 39%{opacity:1} 61%{opacity:1} 72%{opacity:0} 100%{opacity:0} }
  @keyframes hs-tint-c{ 0%{opacity:0} 61%{opacity:0} 72%{opacity:1} 89%{opacity:1} 100%{opacity:0} }
  .hs-orb.o1{ width:58vw; height:58vw; left:-16vw; top:-20vw;   opacity:.30; animation:hs-d1 21s ease-in-out infinite; }
  .hs-orb.o2{ width:50vw; height:50vw; right:-14vw; top:2vh;    opacity:.26; animation:hs-d2 27s ease-in-out infinite; }
  .hs-orb.o3{ width:54vw; height:54vw; left:12vw; bottom:-24vw; opacity:.24; animation:hs-d3 24s ease-in-out infinite; }
  .hs-orb.o4{ width:38vw; height:38vw; right:4vw; bottom:-12vw; opacity:.22; animation:hs-d1 30s ease-in-out infinite reverse; }
  .hs-orb.o1 .t1{ background:radial-gradient(closest-side,#00609C 0%,#00609C99 42%,transparent 100%); animation:hs-tint-a 15s ease-in-out infinite; }
  .hs-orb.o1 .t2{ background:radial-gradient(closest-side,#00416A 0%,#00416A99 42%,transparent 100%); animation:hs-tint-b 15s ease-in-out infinite; }
  .hs-orb.o1 .t3{ background:radial-gradient(closest-side,#3E6A81 0%,#3E6A8199 42%,transparent 100%); animation:hs-tint-c 15s ease-in-out infinite; }
  .hs-orb.o2 .t1{ background:radial-gradient(closest-side,#40F8F7 0%,#40F8F799 42%,transparent 100%); animation:hs-tint-a 19s ease-in-out infinite; }
  .hs-orb.o2 .t2{ background:radial-gradient(closest-side,#00609C 0%,#00609C99 42%,transparent 100%); animation:hs-tint-b 19s ease-in-out infinite; }
  .hs-orb.o2 .t3{ background:radial-gradient(closest-side,#00416A 0%,#00416A99 42%,transparent 100%); animation:hs-tint-c 19s ease-in-out infinite; }
  .hs-orb.o3 .t1{ background:radial-gradient(closest-side,#00416A 0%,#00416A99 42%,transparent 100%); animation:hs-tint-a 22s ease-in-out infinite; }
  .hs-orb.o3 .t2{ background:radial-gradient(closest-side,#3E6A81 0%,#3E6A8199 42%,transparent 100%); animation:hs-tint-b 22s ease-in-out infinite; }
  .hs-orb.o3 .t3{ background:radial-gradient(closest-side,#00609C 0%,#00609C99 42%,transparent 100%); animation:hs-tint-c 22s ease-in-out infinite; }
  .hs-orb.o4 .t1{ background:radial-gradient(closest-side,#40F8F7 0%,#40F8F799 42%,transparent 100%); animation:hs-tint-a 26s ease-in-out infinite; }
  .hs-orb.o4 .t2{ background:radial-gradient(closest-side,#00609C 0%,#00609C99 42%,transparent 100%); animation:hs-tint-b 26s ease-in-out infinite; }
  .hs-orb.o4 .t3{ background:radial-gradient(closest-side,#3E6A81 0%,#3E6A8199 42%,transparent 100%); animation:hs-tint-c 26s ease-in-out infinite; }
  @keyframes hs-d1{ 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(5vw,3vh) scale(1.1)} }
  @keyframes hs-d2{ 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(-4vw,5vh) scale(1.12)} }
  @keyframes hs-d3{ 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(4vw,-4vh) scale(1.07)} }

  .hs-grid{
    position:absolute; inset:0; z-index:1;
    background-image:
      linear-gradient(to right,  rgba(255,255,255,.07) 0 1.25px, transparent 1.25px),
      linear-gradient(to bottom, rgba(255,255,255,.07) 0 1.25px, transparent 1.25px);
    background-size:120px 120px;
  }
  /* Sinks the surface back to black under the subject so the hologram
     keeps its contrast. Sits lower in the final state, where the
     skyline settles below the logo. */
  .hs-pool{
    position:absolute; inset:0; z-index:2;
    background:radial-gradient(circle at 50% var(--hs-py,50%), #000 0%, #000 var(--hs-pi,24%), transparent var(--hs-po,58%));
    transition:background 1s ease;
  }
  html.hero-final .hs-pool{ --hs-py:56%; --hs-pi:26%; --hs-po:64%; }

  @media (prefers-reduced-motion: reduce){
    .hs-orb, .hs-orb .tint{ animation:none !important; }
    .hs-orb .t1{ opacity:1; } .hs-orb .t2, .hs-orb .t3{ opacity:0; }
  }

  .converge-canvas{
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 12;
    opacity: 0;
    pointer-events: none;
    transition: opacity .6s ease;
  }
  .converge-canvas.visible{ opacity: 1; }

  /* Welcome/intro band at the top of content-below: the branded logo,
     event dates and location, a short line about the forum, and the
     two primary calls to action. Extends the same brand system
     already established elsewhere on the page (HARMAN Blue as the
     primary accent, Innovation Dark Blue for body text, the Century
     Gothic fallback stack already used for the beat-lines) rather
     than introducing anything new. Starts hidden and slides up into
     place the first time it scrolls into view (see the
     IntersectionObserver near the bottom of the script) -- a single,
     understated reveal, not an attempt at anything like the hero's
     own animation sequence. */
  .intro-section{
    position: relative;
    z-index: 1;              /* above the shared background canvas */
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    width: 100%;
    margin: 0 auto;
    padding: 40px 0;
    box-sizing: border-box;
    text-align: center;
    opacity: 0;
    transition: opacity .4s ease;
  }
  /* The content keeps its old measure; only the background goes full width. */
  .intro-inner{
    position: relative;
    z-index: 2;
    display: flow-root;   /* child margins must not collapse out of the
                             wrapper -- they did, shrinking its measured
                             height and pushing the centred block down */
    max-width: 900px;
    width: 100%;
    margin: 0 auto;
    padding: 0 24px;
    box-sizing: border-box;
  }

  /* ---- Shared light-mode background: hairline grid + colour aurora ----
     One canvas for the whole light surface rather than one per section.
     Repeating it per section would restart the grid's phase at every
     boundary and duplicate the orbs, which reads as two backgrounds
     butted together instead of one continuous surface. Living on
     .content-below, it spans the intro, the About section and anything
     added below them, and it inherits that container's own light-mode
     gate so it cannot leak into the dark stages.

     Colour underneath, structure on top, the grid multiplied over the
     colour so the hairlines darken through it rather than sitting on it
     as a separate sheet -- that is what fuses the two.

     The colour moves by cross-fading three stacked brand tints inside
     each orb, not by animating a colour value: every state it passes
     through is a real palette colour (a hue-rotate would swing into
     off-brand greens), and cross-fading opacity is composited rather
     than repainted. Orbs drift and cycle on their own clocks, so the
     combination effectively never repeats. */
  .lc-canvas{
    position: absolute; inset: 0; z-index: 0;
    pointer-events: none; overflow: hidden;
    isolation: isolate;                 /* contains the grid's multiply */
    opacity: 0; transition: opacity .4s ease;
  }
  html.light-mode .lc-canvas{ opacity: 1; }
  .lc-aurora{ position: absolute; inset: 0; z-index: 1; opacity: 1.7; }  /* master colour weight */
  .lc-orb{ position: absolute; border-radius: 50%; will-change: transform; }
  /* Softness comes from a radial falloff, not filter:blur(). A 90px
     Gaussian across several half-viewport layers is genuinely expensive
     -- it starved the intro's own fade-in badly enough that the copy
     took ~6s to appear. A radial-gradient is painted once, costs
     essentially nothing to composite, and at these alphas is
     indistinguishable. */
  .lc-orb .tint{ position: absolute; inset: 0; border-radius: 50%; opacity: 0; will-change: opacity; }

  @keyframes lc-tint-a{ 0%,100%{opacity:1} 28%{opacity:1} 39%{opacity:0} 89%{opacity:0} }
  @keyframes lc-tint-b{ 0%{opacity:0} 28%{opacity:0} 39%{opacity:1} 61%{opacity:1} 72%{opacity:0} 100%{opacity:0} }
  @keyframes lc-tint-c{ 0%{opacity:0} 61%{opacity:0} 72%{opacity:1} 89%{opacity:1} 100%{opacity:0} }

  /* Spread down the whole surface, not clustered at the top, so the
     colour carries through the About section too. */
  .lc-orb.o1{ width:54vw; height:54vw; left:-14vw; top:1%;  opacity:.17; animation:lc-drift1 20.5s ease-in-out infinite; }
  .lc-orb.o2{ width:46vw; height:46vw; right:-12vw; top:12%; opacity:.15; animation:lc-drift2 26.5s ease-in-out infinite; }
  .lc-orb.o3{ width:50vw; height:50vw; left:8vw;  top:27%;  opacity:.13; animation:lc-drift3 23.5s ease-in-out infinite; }
  .lc-orb.o4{ width:40vw; height:40vw; right:0vw; top:43%;  opacity:.30; animation:lc-drift1 29.5s ease-in-out infinite reverse; }
  .lc-orb.o5{ width:48vw; height:48vw; left:-10vw; top:60%; opacity:.15; animation:lc-drift2 24.5s ease-in-out infinite reverse; }
  .lc-orb.o6{ width:44vw; height:44vw; right:-8vw; top:78%; opacity:.13; animation:lc-drift3 27.5s ease-in-out infinite; }

  .lc-orb.o1 .t1{ background:radial-gradient(closest-side, #00609C 0%, #00609C99 42%, transparent 100%); animation:lc-tint-a 14.5s ease-in-out infinite; }
  .lc-orb.o1 .t2{ background:radial-gradient(closest-side, #3E6A81 0%, #3E6A8199 42%, transparent 100%); animation:lc-tint-b 14.5s ease-in-out infinite; }
  .lc-orb.o1 .t3{ background:radial-gradient(closest-side, #00416A 0%, #00416A99 42%, transparent 100%); animation:lc-tint-c 14.5s ease-in-out infinite; }
  .lc-orb.o2 .t1{ background:radial-gradient(closest-side, #40F8F7 0%, #40F8F799 42%, transparent 100%); animation:lc-tint-a 18.5s ease-in-out infinite; }
  .lc-orb.o2 .t2{ background:radial-gradient(closest-side, #C8FFFF 0%, #C8FFFF99 42%, transparent 100%); animation:lc-tint-b 18.5s ease-in-out infinite; }
  .lc-orb.o2 .t3{ background:radial-gradient(closest-side, #00609C 0%, #00609C99 42%, transparent 100%); animation:lc-tint-c 18.5s ease-in-out infinite; }
  .lc-orb.o3 .t1{ background:radial-gradient(closest-side, #00416A 0%, #00416A99 42%, transparent 100%); animation:lc-tint-a 21.5s ease-in-out infinite; }
  .lc-orb.o3 .t2{ background:radial-gradient(closest-side, #00609C 0%, #00609C99 42%, transparent 100%); animation:lc-tint-b 21.5s ease-in-out infinite; }
  .lc-orb.o3 .t3{ background:radial-gradient(closest-side, #40F8F7 0%, #40F8F799 42%, transparent 100%); animation:lc-tint-c 21.5s ease-in-out infinite; }
  .lc-orb.o4 .t1{ background:radial-gradient(closest-side, #D9E2E5 0%, #D9E2E599 42%, transparent 100%); animation:lc-tint-a 26.5s ease-in-out infinite; }
  .lc-orb.o4 .t2{ background:radial-gradient(closest-side, #C8FFFF 0%, #C8FFFF99 42%, transparent 100%); animation:lc-tint-b 26.5s ease-in-out infinite; }
  .lc-orb.o4 .t3{ background:radial-gradient(closest-side, #3E6A81 0%, #3E6A8199 42%, transparent 100%); animation:lc-tint-c 26.5s ease-in-out infinite; }
  .lc-orb.o5 .t1{ background:radial-gradient(closest-side, #00609C 0%, #00609C99 42%, transparent 100%); animation:lc-tint-a 16.5s ease-in-out infinite; }
  .lc-orb.o5 .t2{ background:radial-gradient(closest-side, #40F8F7 0%, #40F8F799 42%, transparent 100%); animation:lc-tint-b 16.5s ease-in-out infinite; }
  .lc-orb.o5 .t3{ background:radial-gradient(closest-side, #00416A 0%, #00416A99 42%, transparent 100%); animation:lc-tint-c 16.5s ease-in-out infinite; }
  .lc-orb.o6 .t1{ background:radial-gradient(closest-side, #3E6A81 0%, #3E6A8199 42%, transparent 100%); animation:lc-tint-a 22.5s ease-in-out infinite; }
  .lc-orb.o6 .t2{ background:radial-gradient(closest-side, #C8FFFF 0%, #C8FFFF99 42%, transparent 100%); animation:lc-tint-b 22.5s ease-in-out infinite; }
  .lc-orb.o6 .t3{ background:radial-gradient(closest-side, #00609C 0%, #00609C99 42%, transparent 100%); animation:lc-tint-c 22.5s ease-in-out infinite; }

  @keyframes lc-drift1{ 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(5vw,3vh) scale(1.1)} }
  @keyframes lc-drift2{ 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(-4vw,5vh) scale(1.12)} }
  @keyframes lc-drift3{ 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(4vw,-4vh) scale(1.07)} }

  /* The grid sits UNDER the colour, not over it with a multiply blend.
     Same fused result -- the washes tint the hairlines as they pass --
     but measurably cheaper: blending a layer this tall cost ~6 px/s of
     the photo ribbon's crawl, and the drift animation cost far more
     than that (see below).

     No drift. Animating background-position repaints the full 2800px
     layer every frame, which dropped the ribbon from 34 px/s to 1.4 --
     a 24x slowdown for a movement of one cell per 110 seconds that
     nobody would notice. The orbs still drift and cross-fade, and those
     are transform/opacity only, so they stay on the compositor. */
  .lc-grid{
    position: absolute; inset: 0; z-index: 0;
    /* Tiled, not repeating-gradient. A repeating gradient is computed
       across the element's whole width in one pass, so at a fractional
       display scale the rounding error accumulates until a hairline
       lands between device pixels and disappears -- measured on a 1906px
       capture, lines dropped at x=717 and x=1797, exactly nine cells
       apart, which widened those cells into rectangles. A background
       tile is rasterised once at 120x120 and repeated, so no error can
       build up across the row. The 1.25px line is deliberate too: a
       hairline under 1 device pixel is the thing that can vanish, and
       the alpha is trimmed to keep the same visual weight. */
    background-image:
      linear-gradient(to right,  rgba(51,70,80,.085) 0 1.25px, transparent 1.25px),
      linear-gradient(to bottom, rgba(51,70,80,.085) 0 1.25px, transparent 1.25px);
    background-size: 120px 120px;
    /* Two static mask layers: a radial hole over the first viewport, so
       the grid clears the intro's centred content, and a solid block for
       everything below it, so the rest of the surface stays even. */
    -webkit-mask-image:
      radial-gradient(ellipse 62% 55% at 50% 46%, transparent 0%, rgba(0,0,0,.55) 45%, #000 78%),
      linear-gradient(#000, #000);
    -webkit-mask-size: 100% 100vh, 100% calc(100% - 100vh);
    -webkit-mask-position: top, bottom;
    -webkit-mask-repeat: no-repeat, no-repeat;
    mask-image:
      radial-gradient(ellipse 62% 55% at 50% 46%, transparent 0%, rgba(0,0,0,.55) 45%, #000 78%),
      linear-gradient(#000, #000);
    mask-size: 100% 100vh, 100% calc(100% - 100vh);
    mask-position: top, bottom;
    mask-repeat: no-repeat, no-repeat;
  }

  /* Nothing animates while the surface is hidden. Declared after the orb
     rules, and matched at their depth: the per-orb selectors that set the
     animations are .lc-orb.oN .tN (three classes), so a shorter selector
     here loses on specificity even though it comes later -- which is
     exactly what happened first time round. */
  html:not(.light-mode) .lc-canvas .lc-orb,
  html:not(.light-mode) .lc-canvas .lc-orb .tint{ animation-play-state: paused; }

  @media (prefers-reduced-motion: reduce){
    .lc-orb, .lc-orb .tint{ animation: none !important; }
    .lc-orb .t1{ opacity: 1; }
    .lc-orb .t2, .lc-orb .t3{ opacity: 0; }
  }

  /* Light mode is the only gate on the section itself; the reveal
     choreography lives on the individual elements inside (see .reveal). */
  html.light-mode .intro-section{ opacity: 1; }
  .intro-logo{
    width: 100%;
    height: auto;
    display: block;
  }
  .intro-logo-wrap{
    position: relative;
    width: clamp(320px, 38vw, 600px);
    margin: 0 auto 30px;
    line-height: 0;
  }
  /* Precisely measured against the actual logo asset by detecting the
     whitespace gaps between "F", the star, and "RUM" (not eyeballed):
     the star's own box comes out to 287x294px within the 1568x728
     source, a near-perfect square as expected for a radially
     symmetric shape. These are fractions of that source, so they
     hold regardless of what size the logo is displayed at. The logo
     image used here has this same region cut out entirely (not just
     background-removed), so there's nothing underneath left to mask --
     a separate white mask div used to serve that purpose, but stood
     out as an obvious box in any scenario where this might render
     against a non-white background, which the logo no longer has one
     of in the first place. */
  .intro-star-svg{
    position: absolute;
    left: 21.43%;
    top: 36.26%;
    width: 18.30%;
    height: 40.38%;
    overflow: visible;
  }
  .intro-ray{
    transform-origin: 2450px 2086px;
    opacity: 0;
  }
  #introRayE{ transform: rotate(0deg); }
  #introRayNe{ transform: rotate(-45deg); }
  #introRayN{ transform: rotate(-90deg); }
  #introRayNw{ transform: rotate(-135deg); }
  #introRaySe{ transform: rotate(45deg); }
  #introArrow{ transform: translate(-2500px, 2500px); }

  /* Entrance animation: the same "arrow slides in, then rays fly open"
     sequence as the hero's own logo, replayed here on this separate
     element the first time the section reveals -- not connected to
     the hero's own animation state or timing, just the same visual
     idea reused. */
  html.light-mode .intro-section.uncovered .intro-logo-wrap.in #introArrow{
    animation: intro-arrow-in .8s cubic-bezier(.2,.7,.2,1) .45s both;
  }
  html.light-mode .intro-section.uncovered .intro-logo-wrap.in #introRayE{
    animation: intro-ray-e-open .9s cubic-bezier(.34,1.56,.64,1) .85s both;
  }
  html.light-mode .intro-section.uncovered .intro-logo-wrap.in #introRayNe{
    animation: intro-ray-ne-open .9s cubic-bezier(.34,1.56,.64,1) .85s both;
  }
  html.light-mode .intro-section.uncovered .intro-logo-wrap.in #introRayN{
    animation: intro-ray-n-open .9s cubic-bezier(.34,1.56,.64,1) .85s both;
  }
  html.light-mode .intro-section.uncovered .intro-logo-wrap.in #introRayNw{
    animation: intro-ray-nw-open .9s cubic-bezier(.34,1.56,.64,1) .85s both;
  }
  html.light-mode .intro-section.uncovered .intro-logo-wrap.in #introRaySe{
    animation: intro-ray-se-open .9s cubic-bezier(.34,1.56,.64,1) .85s both;
  }
  @keyframes intro-arrow-in{ 0%{ opacity:0; transform: translate(-2500px,2500px); } 35%{ opacity:1; } 100%{ opacity:1; transform: translate(0,0); } }
  @keyframes intro-ray-e-open  { 0%{opacity:0;transform:rotate(-45deg);} 12%{opacity:1;} 100%{opacity:1;transform:rotate(0deg);} }
  @keyframes intro-ray-ne-open { 0%{opacity:0;transform:rotate(-45deg);} 12%{opacity:1;} 100%{opacity:1;transform:rotate(-45deg);} }
  @keyframes intro-ray-n-open  { 0%{opacity:0;transform:rotate(-45deg);} 12%{opacity:1;} 100%{opacity:1;transform:rotate(-90deg);} }
  @keyframes intro-ray-nw-open { 0%{opacity:0;transform:rotate(-45deg);} 12%{opacity:1;} 100%{opacity:1;transform:rotate(-135deg);} }
  @keyframes intro-ray-se-open { 0%{opacity:0;transform:rotate(-45deg);} 12%{opacity:1;} 100%{opacity:1;transform:rotate(45deg);} }

  .intro-date{
    font-family: 'Century Gothic Pro Gold','Century Gothic Pro','Century Gothic','Futura','Trebuchet MS',sans-serif;
    font-weight: 700;
    letter-spacing: .08em;
    font-size: clamp(14px, 1.6vw, 18px);
    color: #00609C;
    text-transform: uppercase;
    margin: 0 0 22px;
  }
  .intro-copy{
    font-family: 'Century Gothic', 'Futura', 'Trebuchet MS', ui-sans-serif, sans-serif;
    font-size: clamp(17px, 2vw, 21px);
    line-height: 1.55;
    color: #334650;
    max-width: 620px;
    margin: 0 auto 38px;
    font-weight: 400;
  }
  .intro-ctas{
    display: flex;
    gap: 16px;
    justify-content: center;
    flex-wrap: wrap;
  }
  .intro-btn{
    font-family: 'Century Gothic', 'Futura', 'Trebuchet MS', ui-sans-serif, sans-serif;
    display: inline-block;
    padding: 14px 34px;
    border-radius: 999px;
    font-weight: 700;
    font-size: 15px;
    letter-spacing: .02em;
    text-decoration: none;
    transition: background .25s ease, color .25s ease, border-color .25s ease;
    cursor: pointer;
  }
  .intro-btn-primary{
    background: #00609C;
    color: #ffffff;
    border: 1px solid #00609C;
  }
  .intro-btn-primary:hover{
    background: #00416A;
    border-color: #00416A;
  }
  .intro-btn-secondary{
    background: transparent;
    color: #00609C;
    border: 1px solid #00609C;
  }
  .intro-btn-secondary:hover{
    background: rgba(0,96,156,.08);
  }

  .intro-countdown{
    display: flex;
    gap: clamp(20px, 4vw, 44px);
    justify-content: center;
    margin-top: 48px;
  }
  .countdown-unit{
    display: flex;
    flex-direction: column;
    align-items: center;
    min-width: 60px;
  }
  .countdown-number{
    font-family: 'Century Gothic Pro Gold','Century Gothic Pro','Century Gothic','Futura','Trebuchet MS',sans-serif;
    font-weight: 700;
    font-size: clamp(32px, 4.2vw, 52px);
    color: #00609C;
    line-height: 1;
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
  }
  .countdown-label{
    font-family: 'Century Gothic', 'Futura', 'Trebuchet MS', ui-sans-serif, sans-serif;
    font-size: clamp(11px, 1.1vw, 13px);
    text-transform: uppercase;
    letter-spacing: .1em;
    color: #5A6F7A;
    margin-top: 8px;
    font-weight: 600;
  }

  /* Softens the shared grid behind this section's copy, the same way the
     intro's mask clears it behind the logo block. It lives on the section
     rather than as another mask layer on the canvas so it tracks this
     section's own height -- the canvas has no idea where the section
     starts or how tall it grew. z-index:-1 keeps it behind the section's
     own content while staying inside the section's stacking context, so
     it is still above the background canvas. */
  .themes-section::before{
    content: '';
    position: absolute; inset: 0; z-index: -1; pointer-events: none;
    background: radial-gradient(ellipse 58% 66% at 50% 54%,
      #fff 0%, rgba(255,255,255,.72) 42%, rgba(255,255,255,0) 76%);
  }
  .themes-section{
    position: relative;
    z-index: 1;
    width: 100%;
    margin: 0 auto;
    padding: 100px 0 120px;
    box-sizing: border-box;
    text-align: center;
  }
  .themes-eyebrow{
    font-family: 'Century Gothic Pro Gold','Century Gothic Pro','Century Gothic','Futura','Trebuchet MS',sans-serif;
    font-weight: 700;
    letter-spacing: .08em;
    font-size: clamp(13px, 1.4vw, 15px);
    color: #00609C;
    text-transform: uppercase;
    margin: 0 24px 18px;
  }
  .themes-heading{
    font-family: 'Century Gothic Pro Gold','Century Gothic Pro','Century Gothic','Futura','Trebuchet MS',sans-serif;
    font-weight: 700;
    font-size: clamp(32px, 4.2vw, 52px);
    color: #001824;
    margin: 0 24px 8px;
    line-height: 1.1;
  }
  /* Curved photo ribbon (v4 spec): the heading and eyebrow are dropped
     INTO the bowl via `top` on a relative box -- paint-only, so the wrap
     keeps its exact flow position and the arc maths is untouched. The
     negative margins pull the heading into the arc's hollow and the body
     copy up under the short centre cards. */
  .themes-section{ --rb-drop: clamp(0px, calc((100vw - 1024px) * .115), 88px); }
  .themes-eyebrow, .themes-heading{ position: relative; z-index: 5; top: var(--rb-drop); }
  #rbWrap{
    position: relative;
    width: 100%;
    overflow: hidden;
    margin: clamp(-48px, -3vh, 0px) auto 0;
  }
  .rb-card{
    position: absolute; top: 0; left: 0; overflow: hidden; background: #d9e2e5;
    will-change: transform, clip-path, width, height;
    visibility: hidden; /* every card starts hidden; the tick decides */
  }
  .rb-card.rb-noimg{ background: #3E6A81; }
  /* The image gets its OWN fixed box (set on resize only) and is never
     resized by the card. Sized by the card -- whose height is rewritten
     every frame -- the browser would re-resample it from source 60x/s at a
     new sub-pixel phase and the whole ribbon would boil. With a constant
     box, object-fit:cover resolves once and the crawl is pure compositing.
     max-width/max-height:none are load-bearing. */
  .rb-card img{
    position: absolute; left: 0; top: 0; display: block;
    width: var(--rb-iw); height: var(--rb-ih);
    object-fit: cover; transform-origin: 0 0;
    will-change: transform; backface-visibility: hidden;
    max-width: none; max-height: none;
    pointer-events: none; user-select: none;
  }
  .themes-copy{
    font-family: 'Century Gothic', 'Futura', 'Trebuchet MS', ui-sans-serif, sans-serif;
    font-size: clamp(16px, 1.3vw, 19px);
    line-height: 1.65;
    color: #334650;
    max-width: 700px;
    margin: clamp(-72px, -5vh, -32px) auto 0;
    padding: 0 24px;
    position: relative; z-index: 5;
    font-weight: 400;
  }
  .themes-copy p{ margin: 0 0 18px; }
  .themes-copy p:last-child{ margin-bottom: 0; }
  /* Below the breakpoint the strip is flat (curve = 0) and there is no
     bowl for the negative margins to nest text into -- neutralise them
     there only. */
  @media (max-width: 639px){
    #rbWrap{ margin-top: 12px; }
    .themes-copy{ margin-top: 28px; }
  }


  /* =====================================================================
     EVENT HIGHLIGHTS — pinned card train that hands off to the film.
     Ported from the standalone. Two things are scoped deliberately:
     the theme tokens live on #turn rather than :root so they cannot
     collide with the site's own light surface, and the frame's
     background starts fully transparent so the page's existing grid and
     aurora show through until this section decides to darken.
     ===================================================================== */
  /* =====================================================================
     Every colour in this section is a token, and the tokens are written
     from JS as a continuous function of scroll -- never toggled by a
     class. That is the whole trick to a flash-free theme change: there
     is no moment where a rule swaps, only values sliding between two
     sets. The defaults below are the LIGHT end; the dark end lives in
     the THEME table in the script.
     ===================================================================== */
  #turn{
    --frame-bg:      rgba(0,18,28,0);
    /* The frame paints its OWN surface, opaque, at every theme value.
       It was transparent at the light end -- which did fix a seam, but
       created a worse problem: the frame is pinned for thousands of
       pixels of scrolling, and a transparent frame is a window onto the
       page surface behind it, so the grid and aurora slid upward behind
       cards that were being held still (measured: canvas moving 2992 ->
       5016px while the frame sat at viewport 0). A pinned frame has to
       hold its background as still as it holds its content. Opaque also
       means no doubled grid at the boundary: this surface replaces the
       page's rather than overlaying it. */
    /* Matched to the page's own surface exactly, so the frame's top edge
       is invisible. Measured at the boundary before this: 255 above the
       join, 250 below, plus a 232 line right on it -- a tonal band and a
       hard line where the About section ends and this frame begins. Two
       causes, both here: this frame was painting its own aurora on top of
       an opaque white (the 250), and its grid line alpha was .10 against
       the page's .085 with a fresh line landing on the frame's own top
       edge (the 232). The frame keeps its opaque background -- that is
       what stops it sliding while pinned -- and simply stops adding
       anything the page does not already have. */
    --grid-line:     rgba(51,70,80,.085);
    --aurora-op:     0;
    --noise-op:      0;
    --kick:          #00609C;
    --head-ink:      #001824;
    --plate:         #FFFFFF;   /* the lit card */
    --plate-ink:     #001824;
    --plate-sub:     #334650;
    --quiet:         rgba(226,235,240,1);   /* the not-yet-lit card */
    --quiet-ink:     rgba(0,24,36,.72);
    --quiet-sub:     rgba(51,70,80,.62);
    --quiet-border:  rgba(0,24,36,.08);
    /* No drop shadow on the cards. Adjacent shadows overlapped in the
       gaps and stacked into a dark vertical band between every pair,
       which read as a smudge rather than depth. A defined border does
       the separating instead. */
    --plate-border:  rgba(0,24,36,.26);
    --photo-dim:     .84;      /* brightness of a not-yet-lit photo */
    --photo-sat:     .55;
  }
  *{ box-sizing:border-box; }
  /* No overflow-x:hidden here. It makes <body> a scroll container,
     which silently stops position:sticky from pinning against the
     viewport -- the frame just scrolled away and the section rendered
     empty. Horizontal overflow is contained by .scroller's own
     overflow:hidden instead. */
  body{ font-family:'Century Gothic','Futura','Trebuchet MS',ui-sans-serif,sans-serif; }


  /* ---------------------------------------------------------------
     The pin. #turn's height IS the scroll budget: JS sets it to the
     summed phase lengths plus one viewport. #turnFrame sticks to the
     top for the whole of it, and every phase below is a pure function
     of one scalar -- progress through that pin -- which is what makes
     scrolling back up reverse exactly, for free.
     --------------------------------------------------------------- */
  #turn{ position:relative; }
  #turnFrame{ position:sticky; top:0; height:100vh; overflow:hidden; isolation:isolate;
    display:flex; flex-direction:column; background:var(--frame-bg,transparent);
    /* Top-aligned with explicit nav clearance rather than vertically
       centred. Centred, the group landed with its eyebrow at y=81 and
       its title at y=109 -- the nav occupies 23 to 104, so the eyebrow
       sat entirely behind it and the title started 5px below its edge.
       There was 238px of unused space under the cards, so the whole
       group simply moves down into it. Explicit start + padding rather
       than more padding on a centred box: centring only shifts content
       by half of whatever padding is added, so it needs twice as much
       and collapses the content box on short viewports. */
    justify-content:flex-start;
    padding-top:clamp(240px, 36vh, 340px); }

  /* Glass reads only if there is something behind it worth refracting.
     On flat navy, backdrop-filter produces nothing visible -- so the
     frame carries colour pools, the same 120px hairline grid used
     elsewhere on the site, and a little noise to stop the large dark
     areas banding. */
  .bg-glow{ position:absolute; inset:0; z-index:0; pointer-events:none; opacity:var(--aurora-op); }
  .bg-glow i{ position:absolute; display:block; border-radius:50%; }
  .bg-glow .g1{ width:52vw; height:52vw; left:-12vw; top:-18vw;
    background:radial-gradient(closest-side, rgba(64,248,247,.28), transparent 100%); animation:g1 34s ease-in-out infinite; }
  .bg-glow .g2{ width:46vw; height:46vw; right:-14vw; top:6%;
    background:radial-gradient(closest-side, rgba(0,96,156,.52), transparent 100%); animation:g2 41s ease-in-out infinite; }
  .bg-glow .g3{ width:44vw; height:44vw; left:26vw; bottom:-20vw;
    background:radial-gradient(closest-side, rgba(0,65,106,.6), transparent 100%); animation:g3 37s ease-in-out infinite; }
  @keyframes g1{ 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(5vw,3vh) scale(1.12)} }
  @keyframes g2{ 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(-4vw,4vh) scale(1.08)} }
  @keyframes g3{ 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(4vw,-3vh) scale(1.1)} }
  .bg-grid{ position:absolute; inset:0; z-index:1; pointer-events:none;
    background-image:
      linear-gradient(to right,  var(--grid-line) 0 1.25px, transparent 1.25px),
      linear-gradient(to bottom, var(--grid-line) 0 1.25px, transparent 1.25px);
    background-size:120px 120px; }
  .bg-noise{ position:absolute; inset:0; z-index:2; pointer-events:none; opacity:var(--noise-op);
    background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='.22'/%3E%3C/svg%3E"); }

  .scroller{ position:relative; z-index:3; touch-action:pan-y; cursor:grab; }
  .scroller.is-grabbing{ cursor:grabbing; }
  /* Once the film is expanding the strip is behind it and no longer the
     thing being pointed at. The film is pointer-events:none (it is
     background, not a player), so hover falls straight through to the
     strip -- which still had its grab cursor and was still popping the
     DRAG hint over the video. Measured: at y=500, inside the strip's
     324-748 band, elementFromPoint returned #scroller with the film at
     full bleed. */
  body.is-filming .scroller{ cursor:default; }
  body.is-filming .drag-hint{ opacity:0 !important; }
  /* Nothing inside the strip should show a text caret and imply the
     cursor means selection rather than dragging. */
  .scroller, .scroller *{ user-select:none; -webkit-user-select:none; }

  /* A visible affordance as well as the cursor, since a grab cursor is
     easy to miss. It follows the pointer, appears only on hover, and
     retires for good after the first drag -- once someone knows, it is
     just clutter. */
  .drag-hint{
    position:fixed; z-index:9; pointer-events:none;
    display:flex; align-items:center; gap:8px;
    padding:8px 14px; border-radius:999px;
    background:rgba(0,24,36,.62); border:1px solid rgba(255,255,255,.22);
    -webkit-backdrop-filter:blur(10px); backdrop-filter:blur(10px);
    color:#EAF6FA; font-size:11px; letter-spacing:.18em; text-transform:uppercase;
    white-space:nowrap;
    opacity:0; transform:translate(-50%,-50%) scale(.92);
    transition:opacity .3s ease, transform .3s cubic-bezier(.22,1,.36,1);
  }
  .drag-hint.on{ opacity:1; transform:translate(-50%,-50%) scale(1); }
  .drag-hint svg{ display:block; }
  @media (prefers-reduced-motion: reduce){ .drag-hint{ display:none; } }
  @media (hover:none){ .drag-hint{ display:none; } }   /* touch gets no cursor hint */
  /* Sits below the fixed nav, which occupies roughly the top 104px at a
     900px viewport. At the old clamp(48px,9vh,110px) the eyebrow landed
     at y=81 -- entirely behind the nav -- and the title at y=109, five
     pixels under its edge. This clamp keeps the whole group clear of it
     at every height, and the frame's padding-top below is set to start
     the cards underneath this rather than through it. */
  .turn-head{ position:absolute; top:clamp(140px,22vh,210px); left:0; right:0; text-align:center;
    z-index:3; pointer-events:none; will-change:transform,opacity; }
  .turn-kick{ margin:0 0 12px; color:var(--kick); font-weight:700; font-size:clamp(12px,1.3vw,14px);
    letter-spacing:.22em; text-transform:uppercase; }
  .turn-title{ margin:0; color:var(--head-ink); font-weight:700; font-size:clamp(28px,3.6vw,46px); line-height:1.1; }

  .scroller{ overflow:hidden; }
  #track{
    display:flex; gap:clamp(18px,2.4vw,30px);
    padding:0 24px 0 56vw;          /* card 01 starts at the third slot */
    will-change:transform;
  }

  /* Cards default to LIT and JS adds .dim, so with no JS -- or under
     reduced motion -- the reader gets a legible strip rather than a
     dark one. */
  /* In light mode the two states cannot be "dark vs bright" -- on white,
     translucency reads as nothing at all, which is why the glass version
     only works on a dark field. So the states swap roles: a card that
     has not been reached is QUIET (flat tinted plate, no shadow, photo
     pulled back), and the lit card GAINS PRESENCE (true white, full
     colour photo, real shadow, one aqua edge). Same mechanic, expressed
     as substance rather than brightness -- and because every value here
     is a token, the same rules keep working as the page darkens. */
  .card{
    position:relative;
    flex:0 0 clamp(320px,27vw,400px);
    border-radius:26px; overflow:hidden;
    background:var(--plate);
    border:1px solid var(--plate-border);
    box-shadow:none;
    transition:background .55s ease, border-color .55s ease, box-shadow .55s ease,
               transform .7s cubic-bezier(.22,1,.36,1), opacity .7s ease;
    will-change:transform,opacity;
    display:flex; flex-direction:column;
  }
  .card.dim{
    background:var(--quiet);
    border-color:var(--quiet-border);
    box-shadow:none;
  }
  .card.dim::before{ content:none; }
  .card:not(.dim)::before{
    content:''; position:absolute; left:0; top:0; height:2px; width:38%; z-index:2;
    background:linear-gradient(90deg,#40F8F7,transparent); opacity:.95;
  }
  .well{ position:relative; width:100%; aspect-ratio:4/3; overflow:hidden; border-radius:18px;
    margin:14px 14px 0; width:calc(100% - 28px); background:#001824; }
  .well img,.well video{ width:100%; height:100%; object-fit:cover; display:block;
    transition:filter .7s ease; }
  /* An iframe has no object-fit, so cover is done with the box itself:
     fixed 16:9 ratio, forced to be at least as large as the well in both
     axes, centred and letting the excess spill outside the well's
     overflow. This has to hold while the well changes shape from the
     card's 4:3 to the viewport's ratio during the zoom, which is why it
     is expressed as constraints rather than fixed dimensions.
     pointer-events:none is load-bearing, not cosmetic: an iframe
     swallows pointer events, so without it the embed eats every
     pointerdown over the card and drag-to-scrub silently stops working
     on the one card people are most likely to grab. */
  .well .film, .well .film-local{
    position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
    width:auto; height:auto; min-width:100%; min-height:100%;
    aspect-ratio:16/9; border:0; display:block;
    pointer-events:none; transition:filter .7s ease;
  }
  .card.dim .well .film, .card.dim .well .film-local{
    filter:brightness(var(--photo-dim)) saturate(var(--photo-sat)); }
  .card.dim .well img, .card.dim .well video{
    filter:brightness(var(--photo-dim)) saturate(var(--photo-sat)); }

  .card-body{ padding:18px 22px 24px; }
  .num{ display:block; font-size:12px; font-weight:700; letter-spacing:.16em; color:var(--kick);
    margin-bottom:8px; transition:color .7s ease, opacity .3s ease; }
  .card.dim .num{ color:var(--quiet-sub); }
  .card.dim .well{ background:rgba(0,24,36,.10); }
  .card h3{ margin:0 0 6px; font-size:clamp(17px,1.5vw,21px); line-height:1.2; color:var(--plate-ink);
    transition:color .7s ease; }
  .card.dim h3{ color:var(--quiet-ink); }
  .card p{ margin:0; font-size:14px; line-height:1.55; color:var(--plate-sub); transition:color .7s ease; }
  .card.dim p{ color:var(--quiet-sub); }
  .card-body{ transition:opacity .3s ease; }

  /* The growing element. This rule is on the element itself, NOT a
     descendant selector like .card.filming .well -- the element gets
     moved to <body> when it starts growing, at which point a
     descendant rule stops matching and it silently falls back to
     position:relative, rendering full-size in the wrong place. */
  .well.is-filming{
    position:fixed; z-index:200; margin:0; border-radius:18px;
    will-change:left,top,width,height;
    /* Background, not a player: no zoom cursor, and click-through rather
       than a viewport-sized click target. */
    cursor:default;
    pointer-events:none;
  }
  /* The stitch. A gradient across the film's lower band, from clear to
     the closing block's exact surface colour, so when the film slides up
     and its bottom edge meets the marquee there is no edge to meet: the
     last pixel of the film IS the first pixel of what follows. It is
     part of the film element, so it travels with the film through the
     handoff instead of needing to be timed to it. --stitch is written
     from the script as the film opens, so it fades in over the last
     stretch of the expansion and is absent while the film is still
     card-sized, where a dark band would look wrong. */
  .well.is-filming::after{
    content:''; position:absolute; left:0; right:0; bottom:0;
    height:clamp(150px,24vh,240px);
    background:linear-gradient(to bottom, rgba(0,24,36,0) 0%, rgba(0,24,36,.55) 45%, #001824 100%);
    opacity:var(--stitch, 0);
    pointer-events:none;
  }

  @media (prefers-reduced-motion: reduce){
    /* Un-pin entirely: a horizontally scrollable strip with every card
       lit, never a blank or frozen section. */
    #turn{ height:auto !important; }
    #turnFrame{ position:static; height:auto; overflow:visible; padding:80px 0; }
    .turn-head{ position:static; margin-bottom:32px; transform:none !important; opacity:1 !important; }
    .scroller{ overflow-x:auto; }
    #track{ transform:none !important; padding-left:24px; }
    .card{ transform:none !important; opacity:1 !important; }
    .card.dim{ background:var(--plate); border-color:transparent; box-shadow:none; }
    .card.dim h3{ color:#001824; } .card.dim p{ color:#334650; } .card.dim .num{ color:#00609C; }
    .card.dim .well img,.card.dim .well video{ filter:none; }
  }

  /* ---------------------------------------------------------------
     Venue overlay — appears once the film has finished opening out.
     It rides on the film's own top offset rather than being pinned to
     the viewport, so when the pin releases and the film slides away the
     overlay travels with it instead of detaching and floating.
     --------------------------------------------------------------- */
  .venue{
    position:fixed; inset:0; z-index:210;
    display:flex; flex-direction:column; align-items:center; justify-content:flex-end;
    /* Bottom-anchored, and the side gutters are real padding on the
       overlay so the coverflow is sized against the space that is
       actually free rather than the full viewport. */
    /* Bottom padding lifted from clamp(26px,5vh,64px). The film is
       stitched into the closing block by a gradient across its lower
       band (see .well.is-filming::after); the controls sit above that
       band rather than in it, so they stay crisp and the join happens
       beneath them. */
    padding:0 clamp(20px,5vw,90px) clamp(28px,5vh,56px);
    gap:clamp(12px,2.2vh,26px);
    opacity:0; visibility:hidden; pointer-events:none;
    transition:opacity .55s ease, visibility .55s;
    text-align:center;
    /* a little floor shade so the type holds over any frame of the film */
    /* Shade weighted to the bottom, where the content now lives. */
    /* Clear across the bottom ~22%, where the film's stitch gradient
       lives: the shade sitting on top of it there tinted the film's last
       pixel to (0,17,26) against the closing block's (0,24,36), which was
       a visible step at the join. The content this shade protects now
       sits above that band anyway. */
    background:linear-gradient(to top, transparent 0%, transparent 22%, rgba(0,12,20,.52) 36%, rgba(0,12,20,.34) 52%, rgba(0,12,20,.06) 72%, transparent 88%);
  }
  /* Visible but click-through: as a full-viewport fixed layer at
     z-index 210 it otherwise swallowed every pointer event on the page,
     including the nav sitting beneath it. Only its own controls take
     clicks. */
  .venue.on{ opacity:1; visibility:visible; pointer-events:none; }
  .venue.on .venue-head,
  .venue.on .vc-card,
  .venue.on .vc-controls{ pointer-events:auto; }
  /* Each band rises from below in turn once the film has finished
     covering, rather than the whole overlay appearing at once. */
  .venue-head, .vc-stage, .vc-controls{
    transform:translateY(52px); opacity:0;
    transition:transform .85s cubic-bezier(.22,1,.36,1), opacity .7s ease;
  }
  .venue.on .venue-head{ transform:none; opacity:1; transition-delay:.10s; }
  .venue.on .vc-stage{   transform:none; opacity:1; transition-delay:.26s; }
  .venue.on .vc-controls{transform:none; opacity:1; transition-delay:.42s; }
  .venue-kick{ margin:0; color:#40F8F7; font-weight:700; font-size:clamp(11px,1.15vw,13px);
    letter-spacing:.26em; text-transform:uppercase; }
  .venue-name{ margin:6px 0 0; color:#fff; font-weight:400; font-style:italic;
    font-family:Georgia,'Times New Roman',serif; font-size:clamp(28px,4.2vw,56px); line-height:1.1; }
  .venue-addr{ margin:10px 0 0; color:rgba(255,255,255,.9); font-size:clamp(14px,1.4vw,19px); letter-spacing:.02em; }

  /* Coverflow. Every card is absolutely centred and then pushed out by
     its distance from the active one, so the active card is always dead
     centre no matter how many cards there are. */
  /* Height is set from the measured card width so the stage never
     reserves space the cards do not use. */
  .vc-stage{ position:relative; width:100%; }
  .vc-card{
    position:absolute; left:50%; top:50%;
    width:var(--vc-w,320px); aspect-ratio:16/10;
    margin:0; border-radius:14px; overflow:hidden;
    box-shadow:0 22px 50px rgba(0,8,14,.5);
    transition:transform .6s cubic-bezier(.22,1,.36,1), opacity .6s ease, filter .6s ease;
    will-change:transform,opacity;
  }
  .vc-card img{ width:100%; height:100%; object-fit:cover; display:block; }
  .vc-card figcaption{
    position:absolute; left:0; right:0; bottom:0; padding:22px 14px 12px;
    color:#fff; font-size:12px; letter-spacing:.16em; text-transform:uppercase;
    background:linear-gradient(to top, rgba(0,10,18,.75), transparent);
    opacity:0; transition:opacity .5s ease;
  }
  .vc-card.is-active figcaption{ opacity:1; }

  .vc-controls{ display:flex; align-items:center; gap:clamp(14px,2vw,26px); }
  .vc-arrow{
    width:44px; height:44px; border-radius:50%; cursor:pointer;
    background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.42);
    color:#fff; font-size:16px; line-height:1; display:grid; place-items:center;
    transition:background .3s ease, border-color .3s ease;
  }
  .vc-arrow:hover{ background:rgba(255,255,255,.18); border-color:#fff; }
  .vc-dots{ display:flex; gap:9px; }
  .vc-dot{ width:7px; height:7px; padding:0; border-radius:50%; cursor:pointer;
    background:rgba(255,255,255,.34); border:0; transition:background .3s ease, transform .3s ease; }
  .vc-dot.is-active{ background:#40F8F7; transform:scale(1.35); }

  @media (prefers-reduced-motion: reduce){
    .vc-card{ transition:none; }
    .venue{ transition:none; }
    .venue-head, .vc-stage, .vc-controls{ transform:none; opacity:1; transition:none; }
  }





  /* =====================================================================
     PAGE CLOSE — marquee, CTA and the GBT legal footer.
     Ported from the standalone. Tokens are scoped to #pageClose and
     .site-footer rather than :root for the same reason the card train's
     are scoped to #turn: two independent sets of --plate/--kick on the
     document root would silently fight. The footer class is renamed from
     the standalone's .footer to .site-footer so it cannot collide with
     anything later.
     ===================================================================== */
  /* The closing block is DARK, continuous with the takeover above it.
     Every value below was contrast-checked against its own background
     rather than picked by eye; the ratios are noted where they are not
     obvious. Circuit Aqua replaces HARMAN Blue for the small caps
     labels here for the same reason the card train's dark end does it:
     #00609C on #001824 is unreadable, aqua is 12.5:1. */
  #pageEnd, #pageClose, .site-footer{
    --kick:          #40F8F7;                 /* 12.5:1 on the plate */
    --head-ink:      #EAF6FA;                 /* 16.5:1 on the surface */
    --plate:         #06222E;                 /* lifted, so the card reads as a plane */
    --plate-ink:     #EAF6FA;                 /* 14.9:1 */
    --plate-sub:     rgba(234,246,250,.72);   /*  8.3:1 */
    --plate-border:  rgba(255,255,255,.16);
    --quiet-sub:     rgba(234,246,250,.55);   /*  5.6:1 */
    --aqua:          #40F8F7;
    --neural:        #00416A;
    --dark:          #001824;
    --close-bg:      #001824;                 /* the marquee's edge fades */
    --surface:       #001824;
    /* Same three values the card train's frame uses at its dark end, so
       the aurora, grid and noise layers below render identically here. */
    --grid-line:     rgba(255,255,255,.07);
    --aurora-op:     1;
    --noise-op:      .5;
  }
  /* The closing block and the footer carry the takeover's own surface
     rather than a flat fill: the same .bg-glow / .bg-grid / .bg-noise
     layers at the same token values. Flat #001824 read as a different
     material from the section immediately above it.
     ONE surface for both, not one each: given layers of its own the
     footer restarted the aurora at its top edge, putting a visible band
     across the join. This wrapper spans marquee, CTA and footer, so the
     wash runs continuously to the end of the page. */
  #pageEnd{ position:relative; overflow:hidden; background:var(--surface); }
  #pageEnd > .bg-glow, #pageEnd > .bg-grid, #pageEnd > .bg-noise{ z-index:0; }
  /* Both surfaces carry identical grid and glow settings, but each set of
     orbs is placed relative to its own box -- so this one opened at full
     strength on its top edge while the frame above it is dark at its
     bottom edge, and the marquee band appeared to have a lighter
     background of its own starting exactly at the join. Ramping the glow
     and the grid in over the first 240px means the surface begins by
     matching whatever is above it and builds from there, so there is
     nothing to line up and no edge to see. */
  #pageEnd > .bg-glow, #pageEnd > .bg-grid, #pageEnd > .bg-noise{
    -webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 240px);
    mask-image:linear-gradient(to bottom, transparent 0, #000 240px);
  }
  #pageClose, .site-footer{ position:relative; z-index:1; background:transparent; }
  /* ---------------------------------------------------------------
     MARQUEE
     One long line of type moving continuously. Two identical tracks
     sit side by side and the pair is translated by exactly the width
     of one; at -100% the second track is precisely where the first
     began, so resetting to 0 is invisible and the loop never seams.
     Speed is a base drift plus a term from scroll velocity, so it
     leans into the direction you are scrolling and settles back to
     its own pace -- the same idea as the photo ribbon, which keeps
     the two feeling related rather than like two separate widgets.
     --------------------------------------------------------------- */
  .marquee{
    position:relative; overflow:hidden;
    /* Extra room above the type only. The surface behind it is #pageEnd's
       and is untouched; this just holds the marquee further off the
       film's bottom edge. */
    padding:clamp(110px,16vh,170px) 0 clamp(28px,5vh,54px);
    background:transparent;
    user-select:none;
    /* The edges fade the TYPE, via a mask, rather than by laying two
       gradient panels of --close-bg over it. Those panels were a flat
       fill painted on top of the shared aurora surface, so the band
       read as its own lighter rectangle with a hard edge at each end --
       and the 1px rules top and bottom finished the job of making it
       look like a separate section. A mask removes pixels instead of
       adding colour, so whatever the surface is doing shows through
       untouched. */
    -webkit-mask-image:linear-gradient(90deg, transparent 0,
      #000 clamp(30px,6vw,120px), #000 calc(100% - clamp(30px,6vw,120px)), transparent 100%);
    mask-image:linear-gradient(90deg, transparent 0,
      #000 clamp(30px,6vw,120px), #000 calc(100% - clamp(30px,6vw,120px)), transparent 100%);
  }
  .marquee-track{
    display:flex; width:max-content; will-change:transform;
  }
  .marquee-seq{ display:flex; align-items:center; flex:0 0 auto; }
  .marquee-item{
    display:flex; align-items:center; gap:clamp(18px,2.4vw,34px);
    padding-right:clamp(18px,2.4vw,34px);
    font-family:'Century Gothic','Futura','Trebuchet MS',ui-sans-serif,sans-serif;
    font-weight:700;
    font-size:clamp(26px,5.4vw,72px);
    line-height:1;
    letter-spacing:-.01em;
    color:var(--head-ink);
    white-space:nowrap;
  }
  /* Every other phrase is outlined rather than filled: at this size a
     solid wall of type reads as noise, and alternating gives the line
     a rhythm you can actually follow as it passes. */
  .marquee-item.ghost{
    color:transparent;
    /* 45% not 30%: at 30% the outline measured 2.65:1 on this surface,
       under the 3:1 minimum for large text. At 45% it is 4.5:1 and
       still clearly secondary to the solid phrases. */
    -webkit-text-stroke:1px rgba(255,255,255,.45);
  }
  .marquee-star{
    flex:0 0 auto;
    width:clamp(16px,2.2vw,30px); height:clamp(16px,2.2vw,30px);
    color:var(--kick);
  }
  .marquee-star svg{ display:block; width:100%; height:100%; }
  /* Edge fading is handled by the mask on .marquee itself (above), not
     by overlay panels. */

  /* ---------------------------------------------------------------
     CTA
     --------------------------------------------------------------- */
  .cta-wrap{ padding:clamp(60px,12vh,140px) clamp(20px,5vw,80px); display:flex; justify-content:center; }
  .cta{
    position:relative; width:min(1040px,100%);
    background:var(--plate);
    border:1px solid var(--plate-border);
    border-radius:30px;
    padding:clamp(32px,5vw,60px);
    overflow:hidden;
    /* Two columns, because one was measurably wrong: every element was a
       full-width block but the type wrapped at a readable measure, so a
       third of the card's content width (284 of 852px) held nothing at
       all. Widening the measure to fill it would have been the other
       wrong answer -- 100+ character lines. So the space gets content
       that belongs there instead: the argument on the left, the
       commitment on the right. */
    display:grid;
    grid-template-columns:minmax(0,1.08fr) minmax(272px,.92fr);
    gap:clamp(26px,3.6vw,52px);
    align-items:start;
  }
  /* The one Circuit Aqua accent on the card. Guidelines are explicit
     that this colour is used sparingly and with intent, so it appears
     once, as an edge, and nowhere else. */
  .cta::before{
    content:''; position:absolute; left:0; top:0; height:3px; width:42%;
    background:linear-gradient(90deg, var(--aqua), transparent);
  }
  .cta-kick{
    margin:0 0 14px; color:var(--kick); font-weight:700;
    font-size:clamp(12px,1.3vw,14px); letter-spacing:.18em; text-transform:uppercase;
    font-family:'Century Gothic','Futura','Trebuchet MS',ui-sans-serif,sans-serif;
  }
  .cta-title{
    margin:0 0 10px; color:var(--head-ink);
    font-size:clamp(30px,4.4vw,58px); line-height:1.05; letter-spacing:-.01em;
  }
  .cta-sub{
    margin:0 0 clamp(24px,3.4vh,34px); color:var(--plate-sub);
    font-size:clamp(15px,1.5vw,18px); line-height:1.6; max-width:46ch;
  }
  /* The right column: what you are committing to, and by when. The
     deadline reads as the largest thing in the card after the headline,
     which is the correct hierarchy for a closing CTA -- previously it
     was a thin full-width strip that the eye skated straight past. */
  .cta-panel{
    padding:clamp(20px,2.4vw,28px);
    border-radius:20px;
    background:rgba(255,255,255,.05);
    border:1px solid rgba(255,255,255,.14);
  }
  .cta-panel .label{
    display:block; margin:0 0 10px;
    color:var(--kick); font-weight:700; font-size:11px;
    letter-spacing:.16em; text-transform:uppercase;
    font-family:'Century Gothic','Futura','Trebuchet MS',ui-sans-serif,sans-serif;
  }
  .cta-panel .date{
    display:block; color:var(--head-ink); font-weight:700;
    font-size:clamp(22px,2.5vw,30px); line-height:1.12; letter-spacing:-.01em;
    font-family:'Century Gothic','Futura','Trebuchet MS',ui-sans-serif,sans-serif;
  }
  .cta-panel .left{
    display:inline-block; margin-top:12px;
    padding:5px 12px; border-radius:999px;
    background:rgba(64,248,247,.12);
    color:var(--aqua); font-size:12.5px; font-weight:700;
    letter-spacing:.04em;
  }
  .panel-rule{ height:1px; background:rgba(255,255,255,.12); margin:clamp(18px,2.2vh,22px) 0; }
  .panel-facts{ margin:0; }
  .panel-facts div{ display:flex; gap:10px; margin:0 0 10px; font-size:13.5px; line-height:1.5; }
  .panel-facts div:last-child{ margin-bottom:0; }
  .panel-facts dt{
    flex:0 0 clamp(58px,5vw,70px); margin:0;
    color:var(--quiet-sub); font-size:11px; letter-spacing:.1em; text-transform:uppercase;
    font-family:'Century Gothic','Futura','Trebuchet MS',ui-sans-serif,sans-serif;
    padding-top:2px;
  }
  .panel-facts dd{ margin:0; color:rgba(234,246,250,.88); font-weight:600; }

  /* One column once the two would each be too narrow to read: the panel
     falls below the copy rather than being squeezed beside it. */
  @media (max-width:860px){
    .cta{ grid-template-columns:1fr; gap:clamp(22px,3vh,30px); }
  }
  .cta-actions{ display:flex; flex-wrap:wrap; gap:14px; align-items:center; }
  .btn{
    display:inline-flex; align-items:center; justify-content:center;
    padding:15px 30px; border-radius:999px; cursor:pointer;
    font-family:'Century Gothic','Futura','Trebuchet MS',ui-sans-serif,sans-serif;
    font-weight:700; font-size:clamp(14px,1.4vw,16px);
    text-decoration:none; border:1.5px solid transparent;
    transition:background-color .25s ease, color .25s ease, border-color .25s ease;
  }
  /* Same treatment the nav's own CTA uses when the surface is dark, so
     the two read as one system rather than two button styles. */
  .btn-primary{ background:#ffffff; color:#001824; }
  .btn-primary:hover{ background:var(--aqua); color:#001824; }
  .btn-ghost{ background:transparent; color:#EAF6FA; border-color:rgba(255,255,255,.55); }
  .btn-ghost:hover{ background:rgba(255,255,255,.08); border-color:#EAF6FA; }
  .cta-note{ margin:18px 0 0; color:var(--quiet-sub); font-size:13px; }

  /* ---------------------------------------------------------------
     FOOTER
     The lower block is GBT's standard legal footer, reproduced exactly
     as supplied -- wording, link targets and order all unchanged, since
     it is legal boilerplate rather than copy to be edited. Everything
     here is layout only.

     Fine print is the one place a dark surface usually fails: small
     low-contrast type on near-black. Colours were checked rather than
     eyeballed -- the legal paragraph runs at 6.9:1 and the links at
     13.8:1 against #001824, so the smallest text on the page still
     clears AA for body copy with room to spare.
     --------------------------------------------------------------- */
  .site-footer{
    position:relative; z-index:1;
    /* Transparent: the surface belongs to #pageEnd, which wraps this and
       the closing block so the aurora runs continuously. Its own opaque
       fill sat later in the stylesheet than the transparent rule above
       and won on source order, cutting the grid off at the footer's top
       edge. */
    background:transparent; color:rgba(234,246,250,.78);
    padding:clamp(40px,7vh,72px) clamp(20px,5vw,80px) clamp(30px,4vh,44px);
  }
  .site-footer-inner{ max-width:1240px; margin:0 auto; }


  /* Full-width rule above the fine print. Sits at the footer's own
     measure so it lines up with the content, not the viewport. */
  /* Stronger than the 14% it was. On this surface the grid lines are 7%
     white, so at 14% the rule was barely distinguishable from the grid
     it was supposed to sit above -- it read as one more box edge. At 34%
     it is clearly a rule and not a grid line, while still quieter than
     the type. */
  .footer-rule{ max-width:1240px; margin:0 auto; height:1px;
    background:rgba(255,255,255,.34); }
  .legal{ padding-top:clamp(22px,3vh,30px); text-align:center; }
  .legal-links{
    display:flex; flex-wrap:wrap; align-items:center; justify-content:center;
    gap:6px 12px; margin:0 0 14px;
    font-size:13px;
  }
  .legal-links a{
    color:rgba(234,246,250,.86); text-decoration:none;
    border-bottom:1px solid rgba(234,246,250,.28);
    padding-bottom:1px; transition:color .25s ease, border-color .25s ease;
  }
  .legal-links a:hover{ color:var(--aqua); border-color:var(--aqua); }
  .legal-links .sep{ color:rgba(234,246,250,.34); }

  .legal p{
    /* Wider than the 88ch it was. Centred text does want a tighter
       measure than the same copy set flush left, but 88ch turned the
       trademark paragraph into a tall narrow column sitting in the
       middle of a very wide footer -- more stack than paragraph. Given
       the room available, letting it run to 118ch takes several lines
       out of it and reads as a block of fine print rather than a
       column, while still stopping well short of the full 1240px. */
    margin:0 auto 12px;
    max-width:135ch;
    font-size:12px; line-height:1.75;
    color:rgba(234,246,250,.62);
  }
  .legal p.legal-assent{ color:rgba(234,246,250,.78); }
  .legal p.legal-copy{ margin-bottom:0; color:rgba(234,246,250,.55); }

  @media (max-width:860px){
    .footer-top{ grid-template-columns:1fr 1fr; }
    .footer-brand{ grid-column:1 / -1; }
  }
  @media (prefers-reduced-motion: reduce){
    .marquee-track{ transform:none !important; }
    .marquee{ overflow-x:auto; }
  }

  /* The fixed scroll cue and the Intro control live in the left margin
     for the whole page, which puts them straight on top of the footer at
     the bottom. Both retire once the closing block is reached -- there is
     nothing left to scroll to, so the cue is actively misleading there. */
  html.at-page-end .scroll-cue,
  html.at-page-end .nav-intro{ opacity:0 !important; pointer-events:none !important; }

  /* While the film is folding back into its card the page is held in
     place; this stops the browser turning the held input into overscroll
     or a pull-to-refresh gesture on the way. */
  html.wind-lock, html.wind-lock body{ overscroll-behavior:none; }

  /* Back to top. Deliberately quiet -- it is a utility, not a call to
     action -- and only present once there is something to come back
     from, so it never competes with the hero or the intro copy. */
  .to-top{
    position:fixed; right:3vmin; bottom:6vmin; z-index:16;
    width:44px; height:44px;                 /* 44px: minimum comfortable target */
    display:flex; align-items:center; justify-content:center;
    border-radius:999px; cursor:pointer;
    background:transparent;
    border:1px solid currentColor;
    color:var(--cue-ink, #ffffff);
    opacity:0; pointer-events:none;
    transition:opacity .4s ease, color .5s ease, border-color .5s ease, transform .25s ease;
  }
  .to-top:hover{ transform:translateY(-2px); }
  .to-top:focus-visible{ outline:2px solid var(--aqua,#40F8F7); outline-offset:3px; }
  html.light-mode .to-top{ --cue-ink:#00609C; }
  html.train-dark .to-top, html.at-page-end .to-top{ --cue-ink:#ffffff; }
  /* Shown only once the reader is past the first screen. */
  html.can-top .to-top{ opacity:.62; pointer-events:auto; }
  html.can-top .to-top:hover{ opacity:1; }
  @media (prefers-reduced-motion: reduce){ .to-top{ transition:none; } .to-top:hover{ transform:none; } }
  /* Below ~820px the legal copy runs wide enough to reach the button's
     corner -- measured: one text run intersecting it at 768, 560 and
     420px. The footer gains enough bottom padding for the button to sit
     below the last line rather than on top of it. */
  @media (max-width:820px){
    .site-footer{ padding-bottom:calc(6vmin + 68px); }
  }


  /* =====================================================================
     SMALL-SCREEN NAVIGATION
     Below 820px the nav becomes logo + a menu button, and the six items
     move into a panel that drops from under it. The panel takes its
     colours from the same tokens the nav does, so it follows every
     light/dark state for free. 44px targets throughout.
     ===================================================================== */
  .nav-menu-btn{ display:none; }
  .nav-menu{ display:none; }

  @media (max-width: 820px){
    .site-nav{
      /* Full available width, edge to edge with a margin, instead of a
         fit-content pill: there is nothing to fit. Logo clearance drops
         to what the logo actually needs on a phone. */
      left:16px; right:16px; transform:translateY(-150%);
      width:auto; max-width:none;
      padding-left:clamp(120px, 34vw, 150px);
      padding-right:10px;
      justify-content:flex-end;
      height:clamp(56px, 8vh, 64px);
    }
    .site-nav.visible{ transform:translateY(0); }
    .site-nav .nav-link{ display:none; }
    .nav-menu-btn{
      display:flex; flex-direction:column; justify-content:center; gap:5px;
      width:44px; height:44px; padding:0 10px;
      background:transparent; border:0; cursor:pointer; border-radius:10px;
      color:inherit;
    }
    .nav-menu-bar{ display:block; height:2px; width:22px; border-radius:2px; background:currentColor;
      transition:transform .25s ease, opacity .2s ease; }
    .site-nav .nav-menu-btn{ color:rgba(255,255,255,.9); }
    html.light-mode .site-nav .nav-menu-btn{ color:#001824; }
    html.train-dark .site-nav .nav-menu-btn,
    html.at-page-end .site-nav .nav-menu-btn{ color:#ffffff; }
    .nav-menu-btn[aria-expanded="true"] .nav-menu-bar:nth-child(1){ transform:translateY(7px) rotate(45deg); }
    .nav-menu-btn[aria-expanded="true"] .nav-menu-bar:nth-child(2){ opacity:0; }
    .nav-menu-btn[aria-expanded="true"] .nav-menu-bar:nth-child(3){ transform:translateY(-7px) rotate(-45deg); }

    .nav-menu{
      position:fixed; left:16px; right:16px; z-index:14;
      top:calc(2.5vmin + clamp(56px, 8vh, 64px) + 8px);
      display:flex; flex-direction:column; gap:2px;
      padding:10px;
      border:1px solid rgba(255,255,255,.55); border-radius:16px;
      background:rgba(0,0,0,.88); backdrop-filter:blur(8px);
      opacity:0; transform:translateY(-8px); pointer-events:none;
      transition:opacity .25s ease, transform .25s ease, background-color .5s ease, border-color .5s ease;
    }
    .nav-menu[hidden]{ display:flex; }              /* hidden attr only gates the transition */
    .nav-menu.open{ opacity:1; transform:none; pointer-events:auto; }
    .nav-menu-link{
      display:flex; align-items:center; min-height:44px; padding:0 14px;
      border-radius:10px; text-decoration:none; text-align:left;
      font-family:'Century Gothic','Futura','Trebuchet MS',ui-sans-serif,sans-serif;
      font-size:16px; color:rgba(255,255,255,.88);
      background:transparent; border:0; cursor:pointer; width:100%;
    }
    .nav-menu-link:hover{ background:rgba(255,255,255,.08); }
    .nav-menu-cta{ margin-top:6px; justify-content:center; background:#ffffff; color:#001824; font-weight:700; }
    .nav-menu-intro{ color:rgba(255,255,255,.62); font-size:14px; letter-spacing:.06em; text-transform:uppercase; }
    html.light-mode .nav-menu{ background:rgba(255,255,255,.96); border-color:rgba(0,24,36,.18); }
    html.light-mode .nav-menu-link{ color:#001824; }
    html.light-mode .nav-menu-link:hover{ background:rgba(0,24,36,.06); }
    html.light-mode .nav-menu-cta{ background:#00609C; color:#ffffff; }
    html.light-mode .nav-menu-intro{ color:rgba(0,24,36,.62); }
    html.train-dark .nav-menu, html.at-page-end .nav-menu{ background:rgba(0,18,28,.94); border-color:rgba(255,255,255,.22); }
    html.train-dark .nav-menu-link, html.at-page-end .nav-menu-link{ color:rgba(255,255,255,.88); }
    html.train-dark .nav-menu-cta, html.at-page-end .nav-menu-cta{ background:#ffffff; color:#001824; }

    /* Desktop affordances in a margin that does not exist on a phone:
       the cue and the Intro pill were sitting on top of the card text.
       Replay lives in the menu instead. */
    .scroll-cue, .nav-intro{ display:none !important; }
  }

  /* ---- Viewport reveal system (light mode) ----
     Every element carrying .reveal is its own unit: it enters by rising
     into place and fading up, staggered against its siblings through
     --reveal-delay, and exits by fading while drifting toward the edge it
     is leaving through. The direction is carried in --reveal-y, which the
     observer sets on the way OUT (negative when the element left via the
     top, positive via the bottom) -- so the exit drifts the right way and
     the next entrance comes back in from that same side.
     Entrance and exit use different curves and durations on purpose: the
     "in" state owns the entrance transition (long, expo-out settle, with
     the stagger), the base state owns the exit (short, ease-in, no
     stagger). Gated on light-mode so nothing here can leak into the dark
     stages. */
  .reveal{
    opacity: 0;
    transform: translateY(var(--reveal-y, 36px));
    transition: opacity .32s ease-in, transform .32s ease-in;
  }
  .reveal.reveal-scale{
    transform: translateY(var(--reveal-y, 36px)) scale(.96);
  }
  html.light-mode .reveal.in{
    opacity: 1;
    transform: none;
    transition:
      opacity .85s cubic-bezier(.22, 1, .36, 1) var(--reveal-delay, 0ms),
      transform .85s cubic-bezier(.22, 1, .36, 1) var(--reveal-delay, 0ms);
  }
  /* The intro's first reveal waits for the burst cover to shrink away
     (see .uncovered, set from the burst's own completion callback) --
     keyed to light-mode alone, the whole cascade would play out unseen
     behind the still-opaque cover. */
  html.light-mode .intro-section:not(.uncovered) .reveal.in{
    opacity: 0;
    transform: translateY(var(--reveal-y, 36px));
    transition: none;
  }
  /* Line-by-line copy. A paragraph marked .reveal-lines is rebuilt at
     runtime into one .rb-line per rendered line (see splitIntoLines);
     each line is a clipping box and the text inside it is the reveal
     unit, so a line rises up from behind its own bottom edge rather
     than just fading in place. --reveal-y is a percentage here, which
     resolves against the line's own height, so it always starts fully
     hidden regardless of font size. Re-derived on resize, since where
     the text wraps depends on the measured width. */
  .rb-line{ display: block; overflow: hidden; }
  .rb-line-inner{ display: block; }
  /* The line BOX is the observed reveal unit, not the text inside it.
     The text starts translated fully outside this box, which clips it --
     so the text's own visible area is zero, and an IntersectionObserver
     watching the text would never fire: the thing that is supposed to
     reveal could never trigger its own reveal. Confirmed directly: a
     line sitting at 736px in a 900px viewport reported not-intersecting.
     The box is always in flow and unclipped, so it triggers reliably,
     and --reveal-delay / --reveal-y inherit from it down to the text.
     These two rules neutralise the generic .reveal treatment on the box
     itself (higher specificity, no !important) so only the text moves. */
  .reveal.rb-line{ opacity: 1; transform: none; transition: none; }
  html.light-mode .reveal.rb-line.in{ opacity: 1; transform: none; }
  .rb-line > .rb-line-inner{
    opacity: 0;
    transform: translateY(var(--reveal-y, 105%));
    transition: opacity .32s ease-in, transform .32s ease-in;
  }
  html.light-mode .rb-line.in > .rb-line-inner{
    opacity: 1;
    transform: none;
    transition:
      opacity .85s cubic-bezier(.22, 1, .36, 1) var(--reveal-delay, 0ms),
      transform .85s cubic-bezier(.22, 1, .36, 1) var(--reveal-delay, 0ms);
  }
  /* Same cover gate as the rest of the intro cascade. */
  html.light-mode .intro-section:not(.uncovered) .rb-line.in > .rb-line-inner{
    opacity: 0;
    transform: translateY(var(--reveal-y, 105%));
    transition: none;
  }

  @media (prefers-reduced-motion: reduce){
    .reveal, .reveal.reveal-scale, html.light-mode .reveal.in{
      transform: none;
      transition: opacity .3s ease var(--reveal-delay, 0ms);
    }
    /* Clipping boxes would hide un-transformed text mid-reveal. */
    .rb-line{ overflow: visible; }
    .rb-line > .rb-line-inner,
    html.light-mode .rb-line.in > .rb-line-inner{
      transform: none;
      transition: opacity .3s ease var(--reveal-delay, 0ms);
    }
  }

  /* ---- Returning visitors: the intro is a one-off ----
     Everything belonging to the hero sequence is taken out of the page
     rather than merely hidden, and the 100vh spacer that reserved room
     for the fixed hero goes with it -- otherwise a skipped intro leaves
     a viewport of dead space above the content with nothing in it. */
  html.intro-skipped #heroScale,
  html.intro-skipped .hero-surface,
  html.intro-skipped .converge-canvas,
  html.intro-skipped .beat-line,
  html.intro-skipped .theme-burst,
  html.intro-skipped .burst-rings,
  html.intro-skipped #skipBtn{ display:none !important; }
  html.intro-skipped .content-below{ margin-top:0 !important; }
  /* The nav logo is normally a fixed-position image placed onto the nav
     by measuring the hero logo's box at the moment of the burst swap.
     With no hero, measuring the nav instead looked right but was not:
     the nav sits at translateY(-150%) until its slide-in transition
     runs, so the rect read one frame later put the logo off the top of
     the screen. On this path it simply becomes a child of the nav and
     is laid out by it -- no measuring, nothing to mistime. */
  html.intro-skipped .nav-logo-image{ opacity: 1 !important; }

  /* Replay control. Sits in the nav, quieter than the real links since
     it is a curiosity rather than a destination. */
  .nav-intro{
    /* Fixed to the left margin, stacked above the scroll cue and set
       vertically to match it. Its exact vertical offset is written from
       the script, measured off the cue so the two stay together at any
       viewport. */
    position:fixed; left:3vmin; top:18vmin; z-index:16;   /* refined from the script */
    writing-mode:vertical-rl;
    background:none; border:1px solid currentColor; border-radius:999px;
    padding:.9em .35em; cursor:pointer; opacity:.62;
    font-family:'Century Gothic','Futura','Trebuchet MS',ui-sans-serif,sans-serif;
    font-size:clamp(12px,1vw,13px); letter-spacing:.12em; text-transform:uppercase;
    color:inherit; transition:opacity .3s ease, color .5s ease, border-color .5s ease;
  }
  .nav-intro:hover{ opacity:1; }
  /* Colour now comes from the scroll cue's own token rather than from
     the nav, since the button no longer sits inside the nav -- those
     descendant selectors silently stopped matching when it moved, which
     would have left it taking whatever colour it inherited from body.
     Sharing --cue-ink means it inverts along with the cue wherever the
     surface goes dark, including the highlights takeover. */
  .nav-intro{ color: var(--cue-ink, #ffffff); }
  html.light-mode .nav-intro{ --cue-ink:#00609C; }
  html.train-dark .nav-intro{ --cue-ink:#ffffff; }
  /* Hidden until the page has actually arrived in light mode: during the
     opening it would be offering to replay a sequence that is still
     playing. Fades in with the cue rather than appearing abruptly. */
  .nav-intro{ opacity:0; pointer-events:none; transition:opacity .5s ease, color .5s ease, border-color .5s ease; }
  html.light-mode .nav-intro{ opacity:.62; pointer-events:auto; }
  html.light-mode .nav-intro:hover{ opacity:1; }

  .content-below{
    position: relative;
    z-index: 10;
    margin-top: 100vh;
    /* 80vh (not the earlier 10vh) specifically to give the star-burst
       stage enough scrollable room to actually be reachable -- with
       only 10vh available, the page hit its scroll limit at just 90px
       past the top, well short of the threshold meant to trigger stage
       2 at all. */
    /* Was 80vh. That existed only because the page had no real content
       below the hero and the burst stage needed somewhere to scroll to.
       There are now five sections and a footer underneath, so the prop
       is not just unnecessary -- it was rendering as 720px of blank
       white after the legal footer, i.e. the page appeared to keep
       going after it had ended. */
    padding-bottom: 0;
    /* Transparent while dark, so the fixed hero surface shows through
       continuously instead of being cut off by this panel's own black
       at the fold -- the page's black comes from html/body underneath.
       It still turns solid white in light mode (rule further down),
       where the light canvas takes the surface over. */
    background: transparent;
    /* Position still moves instantly with scroll (normal document
       flow) -- but opacity is what actually controls whether this
       panel is visible, and that's now sequenced by JS rather than
       tied directly to scroll position. Without this, the panel could
       already be covering the relevant area the instant the user
       scrolls, well before the hero's own 1s disappearing transition
       has had any time to run -- clipping it mid-fade instead of
       letting it finish on its own. */
    opacity: 0;
    pointer-events: none;
    transition: opacity .5s ease, background-color .5s ease;
  }
  .content-below.visible{
    opacity: 1;
    pointer-events: auto;
  }

  /* Light mode: only touches elements that are ever visible once past
     the hero (the hero itself is never on-screen at the same time as
     this, so it stays untouched). Toggled on <html>, alongside the
     theme-burst effect below, and reversed the same way scrolling back
     to the top reverses everything else. */
  html.light-mode, html.light-mode body{
    background: #ffffff;
  }
  html.light-mode .content-below{
    background: #ffffff;
  }
  html.light-mode .site-nav{
    background: rgba(255,255,255,.88);
    border-color: rgba(0,96,156,.35);
  }
  html.light-mode .nav-link{
    color: #001824;
  }
  html.light-mode .nav-link:hover{
    color: #00609C;
  }
  html.light-mode .nav-cta{
    background: #00609C;
    color: #ffffff;
  }
  html.light-mode .nav-cta:hover{
    background: #00416A;
  }
  /* The logo's SVG paths carry a hardcoded white fill attribute; a CSS
     fill rule (even without !important) takes precedence over that
     presentational attribute, so this recolors it to HARMAN Blue
     wherever light mode is active -- which in practice means only in
     its nav-bar position, since that's the only time the two states
     ever overlap. */
  html.light-mode .logo-reveal path,
  html.light-mode .logo-reveal rect,
  html.light-mode .logo-reveal text{
    fill: #00609C;
    transition: fill .5s ease;
  }

  /* Theme burst: the brand-colored ripple that triggers the light-mode
     switch. One solid circle does the actual work (expanding to cover
     the viewport so the color switch beneath it is never visible
     happening), painted first so the colored ring outlines -- added
     after it in the DOM, and so painted on top -- stay visible as
     decoration for the full burst even once the cover has caught up
     with them. */
  /* Two lines that frame the star once it's settled at center (stage
     1), before the burst (stage 2) begins -- sliding in from opposite
     horizontal directions at the same time, echoing the star/logo
     themselves splitting apart in different directions a moment
     earlier. Positioned above/below the star at symmetric 14vh offsets
     from the 50vh center it settles at. */
  .beat-line{
    position: fixed;
    left: 50%;
    z-index: 18;
    /* "Century Gothic Pro Gold" isn't a font name in wide circulation --
       included first, verbatim, in case it's actually installed
       somewhere; falls through to Century Gothic Pro (a real, if
       commercial, family) and then the same system stack used
       elsewhere on this page if neither is present. */
    font-family: 'Century Gothic Pro Gold', 'Century Gothic Pro', 'Century Gothic', 'Futura', 'Trebuchet MS', ui-sans-serif, sans-serif;
    font-style: italic;
    font-weight: 900;
    /* 900 is already the ceiling of the standard font-weight property
       (values run 100-900 in steps of 100 -- there's no higher numeric
       value to move to). A thin same-color stroke pushes the visual
       weight further still, thickening the letterforms beyond what the
       fallback font's own 900/bold rendering provides on its own. */
    -webkit-text-stroke: .02em currentColor;
    text-transform: uppercase;
    font-size: clamp(68px, 11vw, 122px);
    letter-spacing: .01em;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity .8s ease, transform .8s cubic-bezier(.65,0,.35,1), top .3s ease;
  }
  .beat-line-top{
    color: #40F8F7;
    transform: translate(calc(-50% - 70vw), 0);
  }
  .beat-line-bottom{
    color: #C8FFFF;
    transform: translate(calc(-50% + 70vw), 0);
  }
  .beat-line.visible{
    opacity: 1;
    transform: translate(-50%, 0);
  }

  .theme-burst{
    position: fixed;
    top: var(--burst-origin-y, 50%); left: var(--burst-origin-x, 50%);
    z-index: 25;
    pointer-events: none;
  }
  /* #burstRings shares the .theme-burst class for positioning, but
     needs a genuinely higher z-index than the star (26) to actually
     render above it -- an ID selector's higher specificity overrides
     the shared class's z-index just for this element. */
  #burstRings{ z-index: 28; }
  /* The white cover is still needed even though the rings are gone --
     it's what actually obscures the screen so the light-mode color
     switch happens invisibly. The star itself (see star-spin-anchored
     and burstStarExpand below) is what now visibly grows and drives
     the transition; this cover just catches up to fully solidify what
     the star's own gapped, ray-shaped growth can't cover on its own. */
  .burst-cover{
    position: absolute;
    top: 0; left: 0;
    width: 0; height: 0;
    border-radius: 50%;
    background: #ffffff;
    transform: translate(-50%, -50%);
  }
  /* Concentric rings: brought back as a second, sequenced burst that
     comes out after the star's own growth has had its moment (see the
     animation-delay values below), rather than replacing it -- both
     effects now play out from the same origin, one after the other. */
  .burst-ring{
    position: absolute;
    top: 0; left: 0;
    width: 0; height: 0;
    border-radius: 50%;
    border: 6px solid;
    opacity: 0;
    transform: translate(-50%, -50%);
    /* Above the star's own z-index (26) -- theme-burst's base z-index
       (25) keeps the cover correctly underneath the star, but the
       rings share that same parent and were inheriting the same
       stacking as the cover. Once the star grows large enough to
       cover the screen, it was rendering directly on top of the
       rings, hiding them completely regardless of their own color or
       visibility -- no amount of adjusting their color or timing could
       have fixed that, since it was a layering problem, not a color
       or contrast one. */
    z-index: 27;
  }
  /* By the time these rings are visible, the star has already grown
     large and faded toward white, so the background here is already
     light -- dark blues (the original HARMAN Blue / Neural Blue) would
     read as a jarring reversal rather than a continuation. All three
     rings now use light tones instead. */
  /* box-shadow glows added because the star's own growth (not the
     separate cover) turns out to already whiten most of the viewport
     well before the cover itself arrives -- a plain border, even in a
     correct light color, has very low contrast against a background
     that's already near-white by the time these are visible. A glow
     gives each ring presence regardless of what's actually behind it
     at any given moment. */
  /* The box-shadow glow this used to have was added to fix a
     visibility problem that turned out to be a genuine z-index
     stacking bug (see burstRings below), not a color-contrast issue --
     once the rings actually render above the star, the plain border
     alone is sufficient. The glow's own rgba values were deliberately
     darker than these border colors specifically to read against
     white, which is exactly what was producing a visible gray band
     around each ring -- itself a dark ring, just from a different
     source than the original one two turns ago. */
  .ring-aqua{ border-color: #40F8F7; }
  .ring-create{ border-color: #C8FFFF; }
  .ring-vision{ border-color: #D9E2E5; }

  /* The cover's size is now set via inline style, computed inside
     burstStarExpand's own rAF loop on the same clock as the star --
     see the comment there. No CSS animation drives it anymore, since
     that separate-timeline animation was the actual mechanism behind
     the gray-wash bug: a star fading on a JS clock has no guarantee of
     staying in step with a cover expanding on a CSS one. */
  /* Retimed to match the star's new, faster 700ms growth (previously
     1150ms) -- these start at roughly the same proportion into the
     star's own growth as before (~40-57%), so the "star grows, then
     rings follow as a second burst" sequencing still reads the same
     even though the whole thing is quicker now. */
  .theme-burst.bursting .burst-ring{
    animation: burst-ring-expand .6s ease-out .28s forwards;
  }
  .theme-burst.bursting .ring-create{ animation-delay: .34s; }
  .theme-burst.bursting .ring-vision{ animation-delay: .4s; }

  @keyframes burst-ring-expand{
    0%{ width:0; height:0; opacity:1; }
    80%{ opacity:.75; }
    100%{ width:130vmax; height:130vmax; opacity:0; }
  }

  /* Reverse of the above: rings collapse back inward when scrolling
     back up out of light mode, rather than just disappearing along
     with the rest of the instant reset. Starts from an explicitly
     visible, fully-expanded state set via inline style right before
     this class is added (see triggerRingContraction) -- by the time a
     reversal is possible the rings have long since finished their
     forward animation and sit at opacity:0, so animating from
     wherever they actually are wouldn't be visible at all. Delays
     stagger in the opposite order from the forward expansion (ring-
     vision first, base ring last), so the same three rings that
     appeared outer-to-inner also withdraw outer-to-inner. */
  .burst-ring.contracting{
    animation: burst-ring-contract .5s cubic-bezier(.55,0,.85,.35) forwards;
  }
  .ring-create.contracting{ animation-delay: .06s; }
  .burst-ring.contracting:not(.ring-create):not(.ring-vision){ animation-delay: .12s; }

  @keyframes burst-ring-contract{
    0%{ width:130vmax; height:130vmax; opacity:.6; }
    20%{ opacity:.9; }
    100%{ width:0; height:0; opacity:0; }
  }


  @media (prefers-reduced-motion: reduce){
    .enter-reticle{ transition: none; }
  }
  /* ---- Nav + cue while the train has darkened the page ----
     Deliberately last in the stylesheet. These tie on specificity with
     the html.light-mode nav rules above (both are one type + two
     classes), so source order is the only thing that decides -- placed
     earlier they simply lost, and the nav stayed white on a black
     section. A class is fine here, unlike for the surface itself: nav
     and cue both already carry a .5s colour transition, so they
     crossfade rather than snap. */
  /* Applies for the highlights takeover AND for the closing block, which
     is now dark too. Keyed off both states rather than just the first:
     the nav was left in its light treatment -- a white pill with dark
     links -- floating over the dark marquee, CTA and footer. */
  html.train-dark .site-nav,
  html.at-page-end .site-nav{ background: rgba(0,18,28,.72); border-color: rgba(255,255,255,.22); }
  html.train-dark .nav-link,
  html.at-page-end .nav-link{ color: rgba(255,255,255,.85); }
  html.train-dark .nav-link:hover,
  html.at-page-end .nav-link:hover{ color: #fff; }
  html.train-dark .nav-cta,
  html.at-page-end .nav-cta{ background:#ffffff; color:#001824; }
  html.train-dark .nav-cta:hover,
  html.at-page-end .nav-cta:hover{ background:#40F8F7; color:#000; }
  html.train-dark .site-nav .nav-logo path,
  html.at-page-end .site-nav .nav-logo path{ fill:#ffffff; }
  /* The nav logo is a PNG, so it cannot be recoloured by fill like the
     hero's SVG can -- and it is the dark-blue lockup, which simply
     disappeared against the dark nav during the takeover. Knocked out to
     a white silhouette instead, which is an approved colourway for use
     over imagery and gradients. */
  html.train-dark .nav-logo-image,
  html.at-page-end .nav-logo-image{ filter:brightness(0) invert(1); }
  html.train-dark .scroll-cue{ --cue-ink:#ffffff; --cue-track:rgba(255,255,255,.25); }
  /* The venue overlay sits at z-index 210 over the full-bleed film,
     which made the nav unreachable for the whole takeover. The nav is
     lifted above it for the duration; scoped to .is-filming so the
     burst cover can
     still hide it during the opening as before. */
  body.is-filming .site-nav{ z-index:260; }

/* ---- hero logo states ---- */
#harman, #f-letter, #rum-letters { opacity: 0; }
  #arrow { transform: translate(-2500px, 2500px); }
  .ray { transform-origin: 2450px 2086px; transform: rotate(-45deg); }
  #event-date, #event-location { opacity: 0; transform: translateY(-140px); }

  .logo-reveal.playing #arrow        { animation: arrow-in      0.8s cubic-bezier(.2,.7,.2,1)      0.0s both; }
  .logo-reveal.playing #ray-e        { animation: ray-e-open    0.9s cubic-bezier(.34,1.56,.64,1) 0.8s both; }
  .logo-reveal.playing #ray-ne       { animation: ray-ne-open   0.9s cubic-bezier(.34,1.56,.64,1) 0.8s both; }
  .logo-reveal.playing #ray-n        { animation: ray-n-open    0.9s cubic-bezier(.34,1.56,.64,1) 0.8s both; }
  .logo-reveal.playing #ray-nw       { animation: ray-nw-open   0.9s cubic-bezier(.34,1.56,.64,1) 0.8s both; }
  .logo-reveal.playing #ray-se       { animation: ray-se-open   0.9s cubic-bezier(.34,1.56,.64,1) 0.8s both; }
  .logo-reveal.playing #f-letter     { animation: f-slide 1.4s cubic-bezier(.16,1,.3,1) 1.7s both; }
  .logo-reveal.playing #rum-letters  { animation: rum-slide 1.4s cubic-bezier(.16,1,.3,1) 1.7s both; }
  .logo-reveal.playing #harman       { animation: harman-fade 0.75s ease-out 3.1s both,
                              harman-rise 0.75s cubic-bezier(.34,1.56,.64,1) 3.1s both; }
  .logo-reveal.playing #event-date     { animation: date-fade     0.75s ease-out 3.1s both,
                              date-drop     0.75s cubic-bezier(.34,1.56,.64,1) 3.1s both; }
  .logo-reveal.playing #event-location { animation: location-fade 0.75s ease-out 3.4s both,
                              location-drop 0.75s cubic-bezier(.34,1.56,.64,1) 3.4s both; }

  @keyframes arrow-in     { to { transform: translate(0,0); } }
  @keyframes ray-e-open   { 0%{opacity:0;transform:rotate(-45deg);} 12%{opacity:1;} 100%{opacity:1;transform:rotate(0deg);} }
  @keyframes ray-ne-open  { 0%{opacity:0;transform:rotate(-45deg);} 12%{opacity:1;} 100%{opacity:1;transform:rotate(-45deg);} }
  @keyframes ray-n-open   { 0%{opacity:0;transform:rotate(-45deg);} 12%{opacity:1;} 100%{opacity:1;transform:rotate(-90deg);} }
  @keyframes ray-nw-open  { 0%{opacity:0;transform:rotate(-45deg);} 12%{opacity:1;} 100%{opacity:1;transform:rotate(-135deg);} }
  @keyframes ray-se-open  { 0%{opacity:0;transform:rotate(-45deg);} 12%{opacity:1;} 100%{opacity:1;transform:rotate(45deg);} }
  @keyframes f-slide      { 0%{opacity:0;transform:translate(1196px,0px);} 16%{opacity:1;} 100%{opacity:1;transform:translate(0,0);} }
  @keyframes rum-slide    { 0%{opacity:0;transform:translate(-2802px,0px);} 16%{opacity:1;} 100%{opacity:1;transform:translate(0,0);} }
  @keyframes harman-fade  { from{opacity:0;} to{opacity:1;} }
  @keyframes harman-rise  { from{transform:translateY(140px);} to{transform:translateY(0);} }
  @keyframes date-fade      { from{opacity:0;} to{opacity:1;} }
  @keyframes date-drop      { from{transform:translateY(-140px);} to{transform:translateY(0);} }
  @keyframes location-fade  { from{opacity:0;} to{opacity:1;} }
  @keyframes location-drop  { from{transform:translateY(-140px);} to{transform:translateY(0);} }

  .logo-reveal.playing #star-assembly { transform-origin: 2450px 2086px; animation: star-spin 10s linear 3.85s infinite; }
  @keyframes star-spin { from{transform:rotate(0deg);} to{transform:rotate(360deg);} }

  /* Detached-star mode: the star keeps spinning in place on screen
     while its own parent (.logo-reveal) travels to the nav bar --
     everything else in the SVG (the letters, date, location) travels
     normally; only this element gets an added counter-transform.
       The counter values are computed in JS by measuring exactly how
     far the parent's own nav-mode transform would move the star, then
     canceling that out -- the same measurement-first approach used for
     positioning the logo in the nav, rather than deriving the inverse
     transform analytically (which, given the parent's translate AND
     scale both affect a descendant's rendered position, and the SVG's
     own internal scaling is also part of the chain, is exactly the
     kind of multi-step transform math that's proven error-prone
     everywhere else in this project). The rotation itself is
     unaffected by any of this -- it's the same 360deg spin, just
     riding on top of a translate/scale that keeps the whole thing
     visually anchored in place. */
  .logo-reveal.star-detached #star-assembly{
    transform-origin: 2450px 2086px;
    animation: star-spin-anchored 10s linear infinite;
  }
  @keyframes star-spin-anchored{
    from{ transform: translate(var(--star-cx, 0px), var(--star-cy, 0px)) scale(calc(var(--star-cs, 1) * var(--star-burst-scale, 1))) rotate(0deg); }
    to{ transform: translate(var(--star-cx, 0px), var(--star-cy, 0px)) scale(calc(var(--star-cs, 1) * var(--star-burst-scale, 1))) rotate(360deg); }
  }
  /* Stage 2, new approach: rather than the star fading out while an
     unrelated white circle appears in its place, the star itself grows
     -- burstStarExpand() (JS) drives --star-burst-scale up dramatically
     while the star stays visible, so it visually blooms into (rather
     than being replaced by) the white cover that catches up to fully
     obscure the screen a moment later. No CSS rule needed here for the
     scale or opacity -- both are driven frame-by-frame in JS for
     precise, synchronized timing with the cover's own expansion.
     */
