:root {
  --color-bg: #000;
  --color-accent: #ff0000; /* sampled from the drone.band logo (logo2-hq-short.png) — the actual brand red, not a desaturated approximation */
  --color-text: #ffffff;
  --color-text-secondary: #888888;
  /* Matches the Crown brochure's two-font system: Orbitron for headlines/
     step numbers, a monospace for body/description copy. */
  --font-display: 'Orbitron', sans-serif;
  --font-body: 'Share Tech Mono', monospace;
  /* Total scroll height of the combined capture-scene track (see
     .scrub-track below) — 100vh of that is the pinned/visible scene, the
     rest is the scroll distance it takes to play the whole capture video
     start to end, across all 4 stages (Aiming/Contact/Separation/
     Recovery). Bigger = more scroll required to scrub through the same
     video (finer control, slower-feeling); smaller = faster but coarser.
     As of 2026-06-22, src/app.js's initScrollerVideo() overrides this
     with an inline custom property on the actual .scrub-track element,
     sized per the active scrollerVideos entry's own trimmed span and
     scrollSpeed (config.js) — see that file's Notes. This value is only
     ever actually used as a fallback, for the unlikely case no enabled
     scrollerVideos entry exists to compute a real one from; halved from
     the old 1200vh (which made the scene "take a lot of effort to go
     through") to roughly match that same doubled-speed intent. */
  --scrub-track-height: 600vh;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* No overflow:hidden / fixed height here — this is the whole page now
   (served directly by Firebase Hosting, no Google Sites embed), so the
   document is taller than one viewport (5 stacked 100vh scenes) and
   scrolls natively top to bottom. */
html, body {
  width: 100%;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-display);
}

/* Each of the 5 scenes is a normal-flow, full-viewport-height block,
   stacked in document order (Hero, Aiming, Contact, Recovery, Logos).
   position:relative (not fixed) — fixed was only ever needed when each
   scene was its own separate Google Sites embed; now they're sections
   of one continuous page. width: 100% (NOT 100vw, fixed 2026-06-21,
   later same day) — this document is always taller than one viewport,
   so a vertical scrollbar is always present; 100vw is the viewport
   size INCLUDING the space a classic (non-overlay) scrollbar reserves,
   while html/body are width: 100% (which correctly excludes it). On a
   browser with a space-reserving scrollbar (e.g. Windows Chrome),
   100vw made every .scene a few px wider than the page's real visible
   width, which is exactly what forced a stray horizontal scrollbar —
   see Notes (styles.css.aii) for the knock-on effect this had on the
   capture scene's load bar. */
.scene {
  position: relative;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  background: var(--color-bg);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Fixed full-width header bar (logo + Home/About Us/Contact) —
   position: fixed, not absolute, so it stays on screen for the whole
   scroll instead of only within .scene-hero. z-index (2010, above the
   mobile menu's own .nav-links/2005 and .nav-overlay/2003 below) sits
   above every scene's vignette/edge-fade (5/6), since it has to stay
   clickable over every scene, not just the hero — and, as importantly,
   above the mobile menu panel/overlay too: this is its OWN stacking
   context (position: fixed + z-index create one), so .nav-menu-toggle's
   z-index inside it only matters relative to .nav-logo/.nav-links, not
   to .nav-overlay outside it — .site-nav's own z-index is what has to
   beat .nav-overlay's, or the toggle becomes unclickable once the panel
   is open (caught by tests/check-nav-menu.mjs, which clicks it twice).
   Background starts fully transparent
   (so it doesn't add a visible bar over the Hero scene before any
   scrolling) and eases to translucent black via .scrolled below once
   the user scrolls away from the very top — toggled by src/app.js's
   initNav from scroll position, not CSS-only (no scroll-triggered CSS
   exists yet that's simpler than the few lines of JS this needed). The
   transition lives here, not on .scrolled, so it eases in both
   directions. */
.site-nav {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 2010;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: clamp(12px, 2.5%, 20px) clamp(20px, 6%, 64px);
  background: rgba(0, 0, 0, 0);
  transition: background-color 0.4s ease;
}

.site-nav.scrolled {
  background: rgba(0, 0, 0, 0.75);
}

/* Shrunk down from the old standalone .hero-logo (clamp(28px,5vw,72px))
   now that it's a permanent header element sharing a slim bar with the
   nav links, not a large mark floating alone over the Hero scene. No
   position/z-index of its own anymore — it's a plain flex child of
   .site-nav, which already handles both. */
.nav-logo {
  display: block;
  height: clamp(20px, 2.5vw, 32px);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 2.5rem);
}

.site-nav a {
  font-family: var(--font-display);
  font-size: clamp(0.75rem, 1.2vw, 1rem);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-text);
  text-decoration: none;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
  transition: color 0.3s ease;
}

/* .active (the section currently in view, computed from scroll position
   by initNav — see src/app.js) reads as brand red; every other link
   stays white, eased by the transition above in both directions.
   :hover gets the same treatment as a normal link-affordance cue,
   independent of which section is actually active. */
.site-nav a.active,
.site-nav a:hover {
  color: var(--color-accent);
}

/* "Request Demo" (added 2026-06-21) — the one #nav-links item styled as
   a filled button rather than a plain text link, so it reads as a call
   to action distinct from the other 4 navigation links. Both rules here
   need to out-specificity (not just out-order) the .active/:hover rule
   above and the mobile `.site-nav a` padding/border-bottom rule below,
   since this is a sibling .site-nav a — adding the .demo-button class
   onto the same `.site-nav a` chain is what wins that fight regardless
   of source order. */
.site-nav a.demo-button {
  padding: 0.5em 1.3em;
  border: 1px solid var(--color-accent);
  background: transparent;
  color: var(--color-accent);
  text-shadow: none;
}

.site-nav a.demo-button:hover {
  background: var(--color-accent);
  color: #000;
}

/* Mobile menu (added 2026-06-21) — hidden above the 700px breakpoint
   (desktop keeps the plain inline .nav-links row above); below it,
   .nav-menu-toggle becomes the only persistent control and #nav-links
   converts from an inline row into a fixed, full-height panel that
   slides in from the right (see the @media block below for both
   halves of that swap — they have to change together). Right-anchored
   to match where this button sits, which is itself a deliberate change
   from the old site's left-side hamburger. */
.nav-menu-toggle {
  display: none;
}

@media (max-width: 700px) {
  .nav-menu-toggle {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 5px;
    width: 32px;
    height: 32px;
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
    position: relative;
    z-index: 2010; /* #nav-links is a DOM sibling inside this same .site-nav
                   stacking context (see .site-nav's own z-index note
                   above) with an explicit z-index of its own (2005) —
                   without this, the panel's explicit value would paint
                   over this button's implicit auto/0 regardless of
                   their actual DOM order, making the toggle unclickable
                   while open. Just needs to beat .nav-links' z-index
                   locally, not match the page-global numbering. */
  }

  .nav-menu-toggle span {
    display: block;
    width: 22px;
    height: 2px;
    background: var(--color-text);
    transition: transform 0.3s ease, opacity 0.3s ease;
  }

  /* Hamburger -> X morph in place (no icon swap): middle bar fades out,
     top/bottom bars rotate to meet in the middle. .open is toggled by
     src/app.js's initNav, in lockstep with #nav-links'/.nav-overlay's
     own .open below. */
  .nav-menu-toggle.open span:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
  }
  .nav-menu-toggle.open span:nth-child(2) {
    opacity: 0;
  }
  .nav-menu-toggle.open span:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
  }

  .nav-links {
    position: fixed;
    top: 0;
    right: 0;
    z-index: 2005;
    display: flex;
    flex-direction: column;
    gap: 0;
    width: min(75vw, 320px);
    height: 100vh;
    padding: clamp(80px, 14vh, 110px) clamp(24px, 6vw, 40px) 40px;
    background: rgba(10, 10, 10, 0.97);
    box-shadow: -8px 0 30px rgba(0, 0, 0, 0.5);
    transform: translateX(100%);
    transition: transform 0.35s ease;
  }

  .nav-links.open {
    transform: translateX(0);
  }

  .site-nav a {
    font-size: 1rem;
    padding: 14px 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  }

  /* Set apart from the plain link list above it (extra top margin,
     centered) rather than just another row in the same stack — the
     filled background already distinguishes it color-wise, but the
     spacing/alignment makes the "this one's different" reading land in
     the slide-in panel too, not just the desktop inline row. */
  .site-nav a.demo-button {
    margin-top: 1.5rem;
    text-align: center;
  }

  /* Click-to-close scrim behind the open panel — sits above the rest of
     the page (so it can intercept clicks) but below both the panel and
     the toggle button. */
  .nav-overlay {
    position: fixed;
    inset: 0;
    z-index: 2003;
    background: rgba(0, 0, 0, 0.6);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.35s ease;
  }

  .nav-overlay.open {
    opacity: 1;
    pointer-events: auto;
  }
}

/* Section separator (2026-06-22) — a plain-flow horizontal break
   between adjacent scenes, modeled on the Crown brochure's bottom-left
   page-1 signature: drone.band logo + a red line trailing off to the
   right, thinning out rather than ending abruptly. Here a text label
   (the name of the section that follows) stands in for the logo.
   position: static (normal flow, not absolute/fixed) — it just adds
   document height between scenes, the same way any other block element
   would; it doesn't need to be pinned or overlaid on anything. */
.section-separator {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 2rem);
  padding: clamp(1.5rem, 5vw, 3rem) clamp(20px, 6%, 64px);
  background: var(--color-bg);
}

.section-separator-label {
  flex-shrink: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.25rem, 2.6vw, 2rem);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text);
}

/* The line "reduces as it gets to the end" literally — clip-path tapers
   it from a solid bar down to a point, not just an opacity fade —
   matching the brochure's trailing line. flex: 1 (not a fixed/percentage
   width) so it genuinely fills whatever space remains after the label,
   right up to .section-separator's own right padding — same symmetric
   margin the label's left edge already gets, rather than guessing at a
   percentage that falls short or overshoots depending on label length/
   viewport width. */
.section-separator-line {
  flex: 1;
  height: clamp(2px, 0.4vw, 4px);
  background: var(--color-accent);
  clip-path: polygon(0 0, 100% 35%, 100% 65%, 0 100%);
}

/* Utility: dark radial vignette overlay, sits above media, below text */
.vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
  background: radial-gradient(circle at center, transparent 35%, rgba(0, 0, 0, 0.85) 100%);
}

/* Top/bottom fade for the document's real edges only (top of Hero,
   bottom of Logos) — see specs/heroSite.md for why this is a separate
   mechanism from a left/right edge effect rather than one shared util.
   Top stop is a vw-based clamp(), not a % (== vh here, since this
   element is itself 100vh), because it has to cover .site-nav, whose
   own height comes from width-based clamps (padding/logo), not height —
   a vh-relative stop drifts in and out of covering the nav depending on
   viewport height alone. clamp(64px, 9vw, 110px) stays above .site-nav's
   tallest (~72px, wide desktop) and shortest (~44px, narrow mobile)
   rendered height with margin at both ends. */
.scene-edge-fade {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 5;
  background:
    linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) clamp(64px, 9vw, 110px)),
    linear-gradient(to top, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 5%);
}

/* About Us / Contact / Logos (added 2026-06-21, height override added
   same day after review) are short, compact sections, not full-bleed
   media scenes like Hero/the capture scene — overriding .scene's
   default `height: 100vh` (meant for those) down to `height: auto` so
   each one is only as tall as its own content plus a normal vertical
   rhythm of padding, instead of stretching to fill the viewport and
   leaving a large empty gap around a few lines of text. */
.scene--about-us,
.scene--contact-us,
.scene--logos {
  height: auto;
  padding: clamp(3rem, 8vw, 6rem) 0;
}

/* About Us: image ahead of (before, in both DOM order and layout) the
   paragraph text — side by side on desktop, stacked (image on top) on
   narrow viewports, so "ahead of" holds in both layouts without any
   explicit ordering trick. The image itself (.about-us-image) gets its
   src/object-position set at runtime by src/app.js's initStaticImages()
   from config.js's aboutUsImage — see that file's Notes. */
.about-us-content {
  display: flex;
  align-items: center;
  gap: clamp(2rem, 5vw, 4rem);
  max-width: 1200px;
  padding: 0 clamp(20px, 6%, 64px);
}

.about-us-image {
  flex: 0 0 clamp(320px, 52vw, 600px);
  width: clamp(320px, 52vw, 600px);
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.about-us-text {
  flex: 1;
  font-family: var(--font-body);
  font-size: clamp(1.05rem, 1.9vw, 1.45rem);
  line-height: 1.7;
  letter-spacing: 0.02em;
  color: var(--color-text);
}

/* Highlights the founder's name in the site's display font (Orbitron —
   same font .site-nav/.step-title use), uppercase + accent-colored, so
   it reads as a deliberate credit line rather than disappearing into the
   surrounding paragraph. The paragraph itself stays in the body font
   (var(--font-body), same choice .step-desc already makes for its own
   longer descriptive text) rather than switching the whole thing to
   Orbitron — a multi-sentence paragraph set in a display font reads
   worse, not better, the opposite of "more legible." */
.about-us-name {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-accent);
}

@media (max-width: 700px) {
  .about-us-content {
    flex-direction: column;
  }

  .about-us-image {
    flex: none;
    width: 100%;
  }
}

/* Contact (added 2026-06-21, redesigned 2026-06-22 from a single plain
   left-aligned column into a centered grid of bordered "cards" — see
   styles.css.aii Notes for the full before/after reasoning): one card
   per platform, a circular accent-outlined icon badge + handle/email,
   each handle a real link to that platform (verified against the live
   drone.band/contact page, not guessed) — see config.js.aii/index.html.aii
   for the exact URLs and why. Icons are hand-authored inline SVGs, same
   stroke-only convention as the hero caption "key features" icons
   (config.js's FEATURE_ICON) — simplified line-art approximations of
   each platform's mark, not exact brand assets. */
.contact-list {
  display: grid;
  /* Fixed 2 columns (not auto-fit) — auto-fit happily crammed in a 3rd,
     narrower column once the viewport had room for one, which left even
     less width for the longest entry (the email address) than a single
     column did. A fixed count keeps each card's width predictable and
     wide enough for "contact@drone.band" to fit on one line down to
     this section's own max-width, with an explicit 1-column override
     below the same 700px breakpoint every other 2-column→1-column
     layout on this page already uses. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1rem, 2.5vw, 1.75rem);
  list-style: none;
  width: 100%;
  max-width: 1000px;
  margin: 0 auto;
  padding: 0 clamp(20px, 4%, 48px);
}

@media (max-width: 700px) {
  .contact-list {
    grid-template-columns: 1fr;
  }
}

.contact-item {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  padding: clamp(0.9rem, 2vw, 1.3rem) clamp(1.1rem, 2.5vw, 1.6rem);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.03);
  transition: border-color 0.3s ease, background-color 0.3s ease;
}

.contact-item:hover {
  border-color: var(--color-accent);
  background: rgba(255, 0, 0, 0.07);
}

/* Circular badge — same "outline by default, filled on hover" language
   as .demo-button/.cookie-banner-accept (see those Notes entries), just
   shaped as a circle around a single icon instead of a text pill. */
.contact-icon-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: clamp(48px, 4.4vw, 64px);
  height: clamp(48px, 4.4vw, 64px);
  border-radius: 50%;
  border: 1px solid var(--color-accent);
  transition: background-color 0.3s ease;
}

.contact-item:hover .contact-icon-wrap {
  background: var(--color-accent);
}

.contact-icon {
  width: clamp(24px, 2.2vw, 30px);
  height: clamp(24px, 2.2vw, 30px);
  color: var(--color-accent);
  flex-shrink: 0;
  transition: color 0.3s ease;
}

.contact-item:hover .contact-icon {
  color: #000;
}

.contact-item a {
  font-family: var(--font-display);
  font-size: clamp(1rem, 1.8vw, 1.3rem);
  letter-spacing: 0.04em;
  color: var(--color-text);
  text-decoration: none;
  word-break: break-word;
  transition: color 0.3s ease;
}

.contact-item a:hover {
  color: var(--color-accent);
}

/* Footer/Logos (added 2026-06-21, row layout added same day after
   review): copyright/privacy line on the left, logo on the right —
   same left/right split as .site-nav's own logo+links row, for visual
   consistency between the page's two persistent brand touchpoints.
   flex-wrap so it still reads cleanly if the text wraps to 2 lines on a
   narrow viewport rather than fighting the logo for horizontal space.
   No separate "Drone Band" text label — the logo image already IS the
   "Drone Band" wordmark (a combined icon+text lockup — see
   config.js.aii's heroLogo note). */
.footer-content {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  width: 100%;
  max-width: 1100px;
  padding: 0 clamp(20px, 6%, 64px);
}

/* Wraps the copyright/privacy line + the registered-office address
   (added 2026-06-24) as a single column — keeps .footer-content's own
   flex row at exactly 2 children (this text block, .footer-logo), same
   left/right split as before, rather than the address becoming a 3rd
   row item competing with the logo for .footer-content's space-between. */
.footer-text {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.footer-logo {
  height: clamp(24px, 3vw, 36px);
}

.footer-meta {
  font-family: var(--font-body);
  font-size: clamp(0.8rem, 1.2vw, 1rem);
  letter-spacing: 0.04em;
  color: var(--color-text-secondary);
}

.footer-address {
  font-size: clamp(0.7rem, 1vw, 0.85rem);
  opacity: 0.7;
}

.footer-meta a {
  color: var(--color-text-secondary);
  text-decoration: underline;
  transition: color 0.3s ease;
}

.footer-meta a:hover {
  color: var(--color-accent);
}

/* Capture scene scroll-scrub (one combined video for Aiming/Contact/
   Separation/Recovery, 2026-06-19): the scene is wrapped in a "track"
   taller than one viewport (--scrub-track-height). The scene itself pins
   via position:sticky while the user scrolls through that extra height;
   JS maps scroll progress within the track directly to video.currentTime
   — the video is purely a function of scroll position, never autoplays.
   Stop scrolling and it stops, scroll down and it plays forward, scroll
   up and it reverses. No wheel-event interception/preventDefault needed —
   this works identically for wheel, trackpad, touch, and keyboard
   scroll, unlike a delta-hijacking approach. */
.scrub-track {
  position: relative;
  height: var(--scrub-track-height);
}

.scene--scrub {
  position: sticky;
  top: 0;
}

.scrub-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Shown in .scrub-video's place until it's preloaded (added 2026-06-22) —
   identical positioning/sizing so the swap between the two is invisible
   beyond a frame-content change. Deliberately no `display` declared
   here (unlike .cookie-banner's earlier mistake, see that rule's own
   comment) — a plain <img> has no UA-stylesheet `display` fight to
   worry about, so the native `hidden` attribute alone is enough to
   toggle it. */
.scrub-video-backup {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Bottom-left caption carousel for the 4 capture stages — replaces 4
   separate .step-overlay boxes (one per old scene) with one clipping
   viewport (this element, same position/size .step-overlay used to have
   on its own) around a 400%-wide flex row (.stage-overlay-track) holding
   all 4 stage captions side by side. Crossing a video data-stage-changes
   threshold (see index.html, read by initScrollScrub in src/app.js)
   slides that row left by one panel-width via transform, so the outgoing
   and incoming captions visibly slide together instead of cutting
   instantly. Width is 100% (not the original 60%) specifically so the
   incoming panel slides in from the right edge of the screen rather than
   from partway across it — at 60% the next panel's content used to
   appear starting near screen-center, which read oddly. */
.stage-overlay-viewport {
  position: absolute;
  z-index: 6;
  left: 6%;
  bottom: 10%;
  width: 100%;
  overflow: hidden;
}

.stage-overlay-track {
  display: flex;
  width: 400%;
  transition: transform 0.6s ease;
}

/* Each of the 4 stage captions: large red step number, white title,
   one-line description — copy pulled directly from the Crown brochure
   (resources/other/...). flex-basis 25% of the track's own 400% width =
   100% of .stage-overlay-viewport, i.e. one full-width panel per stage —
   keep this in sync with .stage-overlay-track's child count if that ever
   changes (see src/app.js's matching -25%-per-stage transform math). */
.step-overlay {
  position: relative;
  z-index: 0; /* own stacking context, so .step-backdrop's z-index: -1 stays behind THIS panel's text but doesn't drop behind .scrub-video/.vignette */
  flex: 0 0 25%;
  box-sizing: border-box;
  color: var(--color-text);
}

/* Black-to-transparent readability backdrop behind the step number/title/
   description (added 2026-06-27), so the text stays legible over a bright
   patch of video. Sized in px and positioned behind the text by
   src/app.js (not here) — width/background are set per-stage at setup
   (and re-set on resize/font-load) from that stage's own .step-desc
   rendered text width, since CSS alone can't size a gradient stop to a
   sibling's rendered content width. Can't just give .step-overlay itself
   a content-hugging width instead of this separate absolutely-positioned
   layer: .step-loadbar (inside .step-head) is flex: 1 and needs .step-head
   to stay at its current full (definite) width to have anything to grow
   into — shrink-wrapping .step-overlay/.step-head would collapse the load
   bar to ~0 width. left-anchored (top/left/bottom, no right) so the
   explicit JS-set width is the only thing controlling its right edge. */
.step-backdrop {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: -1;
  pointer-events: none;
}

/* Row pairing the number+title block with the stage load bar (added
   2026-06-24) — see .step-loadbar below. align-items: flex-end bottom-
   aligns the (thin) bar with .step-heading's own bottom edge — i.e.
   the bottom of .step-title, not vertically centered against the
   number+title block's full height. padding-right caps the bar's max
   reach — this isn't just "mirror the 6% left inset": .step-overlay is
   exactly as wide as .stage-overlay-viewport, which itself is
   `left: 6%; width: 100%` — i.e. its OWN right edge already sits 6% of
   the screen's width PAST the screen's right edge. A padding-right of
   6% only cancels that structural overflow back out to flush with the
   screen edge, with no real margin left over.
   2026-06-21 (later same day) — changed from a flat clamp(20px, 12%,
   140px) to calc(clamp(20px, 6%, 64px) + 6%), to fix the bar's max
   reach genuinely matching .section-separator-line's own right edge
   (user: "should not reach further to the right than the section-
   separator-line"), not just approximating it. The two clamps' UPPER
   bounds (64px here, 140px there) were never derived from each other,
   so the old flat 12%-clamp version only matched at viewport widths
   where NEITHER cap had kicked in yet (e.g. exactly matched at 800px,
   but the bar reached 39px further right than the separator line at
   1920px) — clamp(20px, 6%, 64px) is .section-separator's OWN
   right-padding formula, copied here verbatim (keep both in sync by
   hand if either ever changes) so this is genuinely "that same margin,
   plus the 6% needed to cancel .stage-overlay-viewport's structural
   overflow" rather than an independently-tuned approximation — the
   two now resolve to the exact same on-screen x-coordinate at every
   viewport width, confirmed via getBoundingClientRect() at 800/1280/
   1920px (diff was 0/0.8/39.2px before this fix, 0px at all 3 after). */
.step-head {
  display: flex;
  align-items: flex-end;
  gap: clamp(1rem, 3vw, 2rem);
  padding-right: calc(clamp(20px, 6%, 64px) + 6%);
}

.step-number {
  font-size: clamp(3.5rem, 9vw, 8.75rem); /* ~140px at typical desktop widths */
  font-weight: 800;
  color: var(--color-accent);
  line-height: 1;
}

.step-title {
  font-size: clamp(1.5rem, 3vw, 2.5rem);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-top: 0.25rem;
}

/* Stage load bar (added 2026-06-24): fills from 0% to 100% as scroll
   progresses through the current stage's own range, reaching 100%
   exactly as the next stage's threshold arrives — a preview of "how
   much of this stage is left" alongside the number/title, distinct
   from the stage-to-stage carousel slide itself. .step-loadbar is the
   max-width track (flex: 1 — fills whatever space .step-head's own
   padding-right leaves after .step-heading); .step-loadbar-fill is the
   actual bar, width set per scroll frame by initScrollScrub() in
   src/app.js. Same accent color + tapered clip-path as
   .section-separator-line (the same "thins out" brochure motif), but
   here the taper rides the animated trailing edge instead of a fixed
   one. display:none is set on .step-loadbar by src/app.js when the
   NEXT stage is unreachable within the video's startFrom/endAt trim
   (see that file's initScrollScrub Notes) — showing a bar that loads
   toward a transition that can never happen would be misleading. */
.step-loadbar {
  flex: 1;
  height: clamp(2px, 0.4vw, 4px);
  margin-bottom: clamp(4px, 0.8vw, 8px);
}

.step-loadbar-fill {
  display: block;
  height: 100%;
  width: 0%;
  background: var(--color-accent);
  clip-path: polygon(0 0, 100% 35%, 100% 65%, 0 100%);
}

/* Scroller video picker (added 2026-06-27) — lets a viewer manually pick
   which scrollerVideos entry (config.js) is active, instead of being
   stuck with whichever one src/app.js's initScrollerVideo() landed on
   at random. Sits below .stage-overlay-viewport (lower `bottom%`, same
   `left%`), a deliberately non-native control (no <select>) so it can
   match this page's own look (Orbitron, sharp/tapered accents) instead
   of each browser's own unthemeable native dropdown chrome. Options are
   built at runtime by src/app.js from SCROLLER_VIDEOS, not hardcoded
   here — same data-driven convention as the hero slides/captions. */
.scroller-video-picker {
  position: absolute;
  z-index: 6;
  left: 6%;
  bottom: clamp(20px, 4%, 40px);
  font-family: var(--font-display);
}

.scroller-video-picker-toggle {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.6rem 1rem;
  background: rgba(0, 0, 0, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.25);
  color: var(--color-text);
  font-family: inherit;
  font-size: clamp(0.7rem, 1vw, 0.85rem);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
}

.scroller-video-picker-label {
  color: var(--color-text-secondary);
}

.scroller-video-picker-current {
  color: var(--color-text);
}

/* A simple CSS-triangle caret (no icon asset) — the same tapered-wedge
   accent-red motif used elsewhere (.step-loadbar-fill/.section-
   separator-line), here just a static indicator rather than an
   animated fill. Flips via rotate() when the list is open. */
.scroller-video-picker-caret {
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 6px solid var(--color-accent);
  transition: transform 0.2s ease;
}

.scroller-video-picker.open .scroller-video-picker-caret {
  transform: rotate(180deg);
}

/* Opens UPWARD (bottom: 100% of the toggle, not top) — the picker sits
   near the screen's bottom edge, so a downward-opening list would risk
   overflowing past the viewport. */
.scroller-video-picker-list {
  position: absolute;
  z-index: 7;
  left: 0;
  bottom: calc(100% + 6px);
  min-width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
  /* Fully opaque (not the 0.7-0.85 alpha used by the page's other dark
     overlays) — this sits low on screen and opens upward over the stage
     number/title/description text, so it needs to fully obscure that
     text while open, not just darken it like a backdrop does; even a
     0.9-something alpha still let a faint ghost of bright glyphs show
     through at the boundary. */
  background: #050505;
  border: 1px solid rgba(255, 255, 255, 0.25);
}

.scroller-video-picker-option {
  padding: 0.6rem 1rem;
  font-size: clamp(0.7rem, 1vw, 0.85rem);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text);
  white-space: nowrap;
  cursor: pointer;
}

.scroller-video-picker-option:hover,
.scroller-video-picker-option.active {
  color: var(--color-accent);
  background: rgba(255, 255, 255, 0.06);
}

/* padding-right (2026-06-25) fixes a real mobile overflow: .step-overlay
   is exactly as wide as .stage-overlay-viewport, which is `left: 6%;
   width: 100%` — so the box .step-desc wraps against extends 6vw PAST
   the screen's right edge. At desktop widths that's a small, mostly
   invisible margin, but at narrow phone widths (confirmed ~360-375px
   via measuring the actual rendered text glyphs' bounding rect, not
   just the block's own box) the description text was wrapping as if
   that full width were visible, then having its tail end clipped by
   .scene's overflow:hidden. Mirrors .stage-overlay-viewport's own 6%
   left inset so text wraps within the visible 88% instead. */
.step-desc {
  font-family: var(--font-body);
  font-size: clamp(1rem, 1.6vw, 1.4rem);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--color-text-secondary);
  margin-top: 0.5rem;
  padding-right: clamp(20px, 6%, 64px);
}

/* Hero crossfade slideshow — each slide (img or video) is stacked full-
   bleed and absolutely positioned; .active fades it in via opacity.
   z-index is toggled between 1 (outgoing) and 2 (incoming) per slide so
   the incoming slide visually sits above the outgoing one during the
   crossfade, instead of both fading independently over the black
   background (which would cause a brief dark dip). Slide elements
   themselves are created at runtime by src/app.js from generated
   HERO_SLIDES data — none are hardcoded in index.html. */
.hero-slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  z-index: 0;
  transition: opacity var(--hero-crossfade-ms, 2000ms) ease;
}

.hero-slide.active {
  opacity: 1;
}

/* Per-slide caption: position/content are set per slide via generated
   HERO_SLIDES[].caption (see src/app.js), not fixed center-screen — lets
   each slide's text avoid sitting on top of the product. Opacity
   transition is half the slide crossfade duration: the caption fades
   out, swaps content + position while invisible, then fades in —
   avoiding any visible overlap between two different captions. */
.hero-caption {
  position: absolute;
  z-index: 6;
  max-width: 42%;
  color: var(--color-text);
  opacity: 0;
  background: rgba(0, 0, 0, 0.7);
  padding: clamp(0.75rem, 2vw, 1.25rem) clamp(1rem, 2.5vw, 1.75rem);
  transition: opacity calc(var(--hero-crossfade-ms, 2000ms) / 2) ease;
}

/* Narrow viewports: 42% of ~390px leaves too little room for an icon +
   2-line feature label without breaking mid-word — give it more width,
   and shrink the label's own font floor further (its desktop clamp
   already maxes out at 1.6vw, which is tiny at this width, so the
   browser was just using the rem floor — that floor itself needed to
   come down here, not just the vw term) so longer labels reliably stay
   on 2 lines instead of occasionally wrapping to 3. */
@media (max-width: 600px) {
  /* The 4 nav links (Home/How It Works/About Us/Contact) used to be
     squeezed down to fit on one inline line at this width — superseded
     2026-06-21 by the hamburger/slide-in panel above (see the 700px
     block above .section-separator), which moves #nav-links off the
     header bar entirely below 700px, so there's no longer an inline
     row at 600px to keep legible. Only the bar's own edge padding is
     still tuned here. */
  .site-nav {
    padding-left: 16px;
    padding-right: 16px;
  }

  .hero-caption {
    max-width: 75%;
  }

  .caption-feature-icon {
    width: clamp(24px, 6vw, 40px);
    height: clamp(24px, 6vw, 40px);
  }

  .caption-feature-label {
    font-size: clamp(0.7rem, 3.4vw, 0.95rem);
  }
}

.hero-caption .hero-title {
  font-size: clamp(2.25rem, 5vw, 4rem);
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.05;
  margin-bottom: 0.5rem;
}

/* Scales with viewport width (clamped) rather than a fixed size — a
   single .hero-line with no accompanying .hero-title (most slides) was
   reading as too small/thin against a full-bleed image. */
.hero-caption .hero-line {
  font-family: var(--font-body);
  font-size: clamp(1.1rem, 1.8vw, 1.6rem);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  line-height: 1.5;
}

/* Red is the highlight color throughout — used on a word/phrase within
   white caption text, never as the base color of a whole line. */
.accent {
  color: var(--color-accent);
}

/* Brochure "key features" icon+label rows, stacked one per line inside
   .hero-caption (matches the brochure's own characteristics block, not
   a wrapped inline list) — one to three features per slide (not all 8
   on every slide), chosen to match that slide's existing copy/imagery.
   Each row is a thin horizontal rule below it, like the brochure. */
.caption-features {
  display: flex;
  flex-direction: column;
  margin-top: 1rem;
}

/* When .caption-features is the only content in .hero-caption (the
   features-only captions — see config.js), there's no tagline above it
   to separate from, so drop the top margin instead of leaving a gap. */
.hero-caption > .caption-features:first-child {
  margin-top: 0;
}

.caption-feature {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.15);
}

.caption-feature:last-child {
  border-bottom: none;
}

/* Sized to roughly span both lines of .caption-feature-label — these
   chips are often the only content in a caption now (features-only
   captions, see config.js), not a small addendum below a tagline, so
   the icon needs to read as a primary element, not a footnote glyph. */
.caption-feature-icon {
  width: clamp(28px, 4vw, 56px);
  height: clamp(28px, 4vw, 56px);
  color: var(--color-accent);
  flex-shrink: 0;
}

/* Always exactly 2 lines (line1<br>line2, set in config.js's
   featureChip()) — matches the brochure's own line breaks per feature
   rather than relying on the browser to wrap a single string.
   min-width: 0 overrides the flex default (content's max-content width),
   which otherwise refuses to shrink below the unbroken text width and
   pushes it past the caption box's right edge on narrow viewports —
   flex: 1 then lets it actually fill the row's remaining width.
   text-align: left is explicit (not inherited) because .hero-caption
   sets text-align: right for several slides (to anchor the caption box
   itself to that side) — without this override, that inherited
   right-align would right-align each feature's 2 lines individually,
   unlike the brochure, which always left-aligns the feature text
   regardless of which side of the page the block sits on. */
.caption-feature-label {
  flex: 1;
  min-width: 0;
  overflow-wrap: break-word;
  text-align: left;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(0.85rem, 1.6vw, 1.4rem);
  letter-spacing: 0.03em;
  line-height: 1.3;
  text-transform: uppercase;
}

/* Cookie consent banner (added 2026-06-21, later same day) — see
   specs/cookieConsent.md. Deliberately "low key": a slim bar pinned to
   the bottom (not a full-screen modal blocking the page), translucent
   dark to match .site-nav.scrolled's own treatment rather than
   introducing a new surface color. Shown/hidden via the `hidden`
   attribute, toggled by src/app.js's initCookieConsent() — no CSS
   transition on show (so the first paint isn't delayed waiting on one);
   a slide-out transition on dismiss reads better. z-index above
   everything else on the page except nothing — this needs to be
   genuinely the topmost thing a visitor can interact with.
   `.cookie-banner[hidden] { display: none }` below is NOT redundant
   with the browser's own UA-stylesheet `[hidden]` rule — author rules
   always beat UA rules regardless of specificity, so this rule's base
   `display: flex` would otherwise unconditionally win and keep the
   banner visible even while `hidden` is true. */
.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3000;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: clamp(0.75rem, 2vw, 1.5rem);
  padding: clamp(0.75rem, 2vw, 1.25rem) clamp(20px, 6%, 64px);
  background: rgba(0, 0, 0, 0.92);
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  transition: transform 0.4s ease, opacity 0.4s ease;
}

.cookie-banner[hidden] {
  display: none;
}

/* `hidden` (native attribute) fully removes it from layout/paint and
   from the tab order — no CSS needed for the hidden state itself, only
   for the dismiss transition immediately before it's set (see
   src/app.js). `.dismissing` plays a brief slide-down + fade so the
   banner doesn't just vanish instantly on click. */
.cookie-banner.dismissing {
  transform: translateY(100%);
  opacity: 0;
}

.cookie-banner-text {
  margin: 0;
  flex: 1 1 320px;
  font-family: var(--font-body);
  font-size: clamp(0.8rem, 1.3vw, 0.95rem);
  color: var(--color-text);
  line-height: 1.5;
}

.cookie-banner-text a {
  color: var(--color-accent);
  text-decoration: underline;
}

.cookie-banner-actions {
  display: flex;
  flex-shrink: 0;
  gap: 0.75rem;
}

.cookie-banner-actions button {
  font-family: var(--font-display);
  font-size: clamp(0.75rem, 1.2vw, 0.9rem);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  cursor: pointer;
  padding: 0.55em 1.3em;
  border-radius: 0;
}

/* Decline reads as the lower-emphasis choice (plain outline, neutral
   color) — Accept reuses the exact same outline-by-default/fill-on-
   hover treatment as .demo-button (see that rule's own comment for why
   filled-by-default was rejected as "too aggressive" for this page's
   nav button; the same reasoning applies here, doubly so for a banner
   every visitor sees on first load). */
.cookie-banner-decline {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.4);
  color: var(--color-text);
}

.cookie-banner-decline:hover {
  border-color: var(--color-text);
}

.cookie-banner-accept {
  background: transparent;
  border: 1px solid var(--color-accent);
  color: var(--color-accent);
}

.cookie-banner-accept:hover {
  background: var(--color-accent);
  color: #000;
}

@media (max-width: 600px) {
  .cookie-banner {
    padding: 0.75rem 16px;
  }
}
