/* ───── Self-hosted Inter (SIL OFL 1.1, see assets/fonts/Inter-LICENSE.txt) ───── */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url('assets/fonts/inter-latin-var.woff2') format('woff2');
}

/* ───── Tokens ───── */
:root {
  --bg: #000000;
  --ink: #FFFFFF;
  --ink-soft: #D1D5DB;
  --line: rgba(255, 255, 255, 0.2);
  --green: #6EE7A8;
  /* 0.66, not 0.62: the glass elements that sit at the TOP of the viewport (the
     navbar and the use-case tablist) are above the bottom-anchored gradient's
     last stop, so their only backdrop is raw film. Measured over every frame of
     all three films (2509 frames), the brightest pixel in the tablist band is
     rgb(223,245,253) on the driving film at t=2.708. At 0.62 that put
     --ink-soft type (unselected tabs) at 4.49:1 in the
     browser, just under AA. 0.66 lifts the same worst frame to 5.3:1. Every
     other --glass surface only gets darker, so nothing regresses. */
  --glass: rgba(0, 0, 0, 0.66);
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --gutter: clamp(1.5rem, 5vw, 4rem);
  --measure: 34rem;
}

/* ───── Base ───── */
* { box-sizing: border-box; margin: 0; }
/* scrollbar-gutter keeps the gutter reserved at all times, so locking the scroll
   while the opening film loads does not shift the layout sideways as the scrollbar
   disappears and comes back. */
html { scroll-behavior: smooth; background: var(--bg); scrollbar-gutter: stable; }
/* The page is held on the hero until the opening film can actually be scrubbed.
   Scrolling into the stage before then shows a poster that jumps between conditions
   rather than a film that moves, which reads as broken. Only scrolling is held: the
   hero stays fully readable, focusable and tabbable throughout, and main.js releases
   this on ready, on error, and on a hard timeout, so it can never strand anyone. */
html.film-gating, html.film-gating body { overflow: hidden; }
body {
  /* html carries the opaque --bg base so the canvas is black before the film paints.
     If body also painted an opaque background here, it would sit as its own box over
     .bg-world (a z-index: -1 descendant of body), hiding the film entirely. */
  background: transparent;
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.6;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { font-weight: 400; line-height: 1.05; letter-spacing: -0.04em; text-wrap: balance; }
/* The headline is three authored lines, not a phrase, so it is sized to let each
   line hold together on one visual line beside the example card rather than to be
   as large as possible. The vw term is what keeps that true between 1024 and 1440:
   the lead column shrinks with the viewport, so the type has to shrink with it. */
h1 { font-size: clamp(1.75rem, 3.4vw, 3.25rem); }
h2 { font-size: clamp(1.75rem, 4vw, 3rem); }
h3 { font-size: 1rem; font-weight: 500; letter-spacing: -0.01em; margin-bottom: 0.75rem; }
p  { max-width: var(--measure); }
ul { padding-left: 1.1rem; }

/* ───── Liquid glass (recipe from the reference; darker base so white text
   clears AA even over the film's brightest snow frames) ───── */
.liquid-glass {
  background: var(--glass);
  background-blend-mode: luminosity;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  border: none;
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
  position: relative;
  overflow: hidden;
}
.liquid-glass::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1.4px;
  background: linear-gradient(180deg,
    rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.1) 20%,
    rgba(255,255,255,0) 40%, rgba(255,255,255,0) 60%,
    rgba(255,255,255,0.1) 80%, rgba(255,255,255,0.3) 100%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}

/* ───── Accessibility ───── */
.skip-link {
  position: absolute; left: -999px; top: 0;
  background: var(--ink); color: #000; padding: 0.75rem 1.25rem; z-index: 50;
}
.skip-link:focus { left: 0; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ───── CTAs ───── */
.cta {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; min-width: 44px; padding: 0.75rem 2rem;
  font-size: 0.9375rem; font-weight: 500; letter-spacing: -0.01em;
  text-decoration: none; border-radius: 10px;
  transition: opacity 0.25s ease, background-color 0.25s ease, color 0.25s ease;
}
.cta-primary { background: var(--ink); color: #000; }
.cta-primary:hover { opacity: 0.88; }
.cta-ghost { color: var(--ink); border: 1px solid var(--line); }
.cta-ghost:hover { background: var(--ink); color: #000; }
.cta-sm { padding: 0.6rem 1.25rem; font-size: 0.875rem; }

/* ───── Navbar ───── */
.nav-wrap { position: absolute; top: 0; left: 0; right: 0; z-index: 20; padding: 1.5rem var(--gutter) 0; }
.site-nav {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  border-radius: 14px; padding: 0.5rem 0.6rem 0.5rem 1.1rem;
}
.wordmark {
  display: inline-flex; align-items: center; min-height: 44px;
  font-size: 1.1rem; font-weight: 600; letter-spacing: -0.03em;
  color: var(--ink); text-decoration: none;
}

/* ───── Hero (bottom-anchored) ───── */
.hero {
  min-height: 100vh; min-height: 100svh;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 0 var(--gutter) clamp(3rem, 7vh, 4.5rem);
  max-width: none; margin: 0;
}
/* The motto is a kicker inside the lead block, directly above the headline: no
   card, no glass, it sits on the same bottom-anchored gradient (and, below 1024,
   the same dark bed) that already carries the headline and subs. Sized one tier
   below the h1 so the two never read as competing headlines. */
.hero-motto {
  max-width: 52ch;
  margin-bottom: 1.1rem;
  color: var(--ink-soft);
  font-size: clamp(1rem, 1.7vw, 1.5rem);
  font-weight: 300; line-height: 1.35; letter-spacing: -0.02em;
  text-wrap: balance;
}
/* Colour, not opacity, carries the hierarchy between the two sentences: the
   [data-fade] system animates opacity and would reset a dimmed value to 1. */
.hero-motto-law {
  display: block; color: var(--ink); font-weight: 400;
  font-size: clamp(1.25rem, 2.2vw, 2.1rem);
  line-height: 1.15; letter-spacing: -0.03em;
  margin-bottom: 0.2em;
}
.hero-inner { display: grid; gap: 2rem; align-items: end; }
/* minmax(0, 1fr) rather than 1fr: the lead column must be allowed to be narrower
   than its longest headline line, otherwise the auto-sized card is squeezed off
   the right edge between 1024 and 1280 instead of the heading reflowing. */
@media (min-width: 1024px) { .hero-inner { grid-template-columns: minmax(0, 1fr) auto; } }
/* Wide enough for the longest authored line ("into 1,000,000 hours of data") plus
   slack; the lead column, not this cap, is what binds below ~1280. */
.hero-heading { max-width: 34ch; margin-bottom: 1.1rem; }
/* White on the primary line, --ink-soft on the secondary one: the hierarchy between
   the two paragraphs is carried by colour and size, not by opacity, because the
   [data-fade] system animates opacity and would reset a dimmed value to 1. */
.hero-sub { color: var(--ink); font-size: 1.0625rem; max-width: 46ch; margin-bottom: 0.6rem; }
.hero-sub-2 {
  color: var(--ink-soft); font-size: 0.9375rem; line-height: 1.55;
  max-width: 52ch; margin-bottom: 1.75rem;
}
.hero-actions { display: flex; flex-wrap: wrap; gap: 1rem; }
/* The card now holds a sentence, so it is set at reading size with a ~40ch measure
   instead of the display size the old three-word phrase used. */
.hero-tag {
  border-radius: 14px; padding: 1rem 1.25rem;
  font-size: 0.875rem; line-height: 1.55; font-weight: 300; letter-spacing: -0.01em;
  max-width: 40ch; justify-self: start;
}
.hero-tag p { max-width: none; }
/* Between 1024 and ~1340 a flat 40ch card would eat the width the headline needs,
   so the card's measure is capped against the viewport as well. */
@media (min-width: 1024px) { .hero-tag { justify-self: end; max-width: min(40ch, 30vw); } }

/* Below 1024 the card stacks under the lead column, so the whole hero block is ~630px
   tall and its top edge lands around 85% of the viewport height, above the point
   where the bottom-anchored gradient has faded out entirely. Measured at 375x812 the
   headline sat on raw sky. Same remedy the static condition lines and use-case notes
   already use: a dark bed under the column itself (rgba(0,0,0,0.78) over a white frame
   => rgb(56,56,56), 11.7:1 for white, 7.9:1 for --ink-soft). fit-content so the bed
   hugs the copy instead of ruling a slab across a 1000px-wide viewport. Static bed,
   nothing here transitions. */
@media (max-width: 1023px) {
  .hero-lead {
    width: fit-content; max-width: 100%;
    border-radius: 16px; padding: 1.1rem 1.15rem 1.2rem;
    background: rgba(0, 0, 0, 0.78);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
  }
  /* The bed costs vertical room the stacked hero does not have at 375x812, and the
     navbar is absolutely positioned, so the column has to reserve its own clearance:
     padding-top holds the block below the nav, and the rhythm is tightened so the
     example card still lands above the fold. */
  .hero { padding-top: 6.75rem; padding-bottom: clamp(1.5rem, 4vh, 3rem); }
  .hero-motto { margin-bottom: 0.9rem; }
  .hero-inner { gap: 1.25rem; }
  .hero-heading { margin-bottom: 0.8rem; }
  .hero-sub { margin-bottom: 0.5rem; }
  .hero-sub-2 { margin-bottom: 1.15rem; }
  .hero-actions { gap: 0.75rem; }
  .hero-tag { padding: 0.9rem 1.1rem; }
}

/* ───── Load-in fades (JS-gated; visible by default) ───── */
html.js [data-fade] { opacity: 0; }
html.js [data-fade].is-shown { opacity: 1; transition: opacity 1s ease; }
@media (prefers-reduced-motion: reduce) {
  html.js [data-fade] { opacity: 1; transition: none; }
}

/* ───── 3. Tech ───── */
/* Full-bleed black band with a --band-wide reading area inside it. The section cannot
   be a centred max-width box: the film is a fixed layer behind the whole page, so a
   narrower box would let the film show in the side gutters beside the copy. The
   padding shorthand keeps the reading area at --band while the background (and the
   rule above it) covers the viewport edge to edge.

   The band is sized backwards from the rail: --rail (12rem) plus --rail-gap (2.5rem)
   plus a 34rem main column, which is the site --measure. At 1440 that main column
   renders 544px wide and puts the longest body line at 66 characters, inside the
   62 to 68 target. The band used to be 68rem, which left every block but one ending
   somewhere different in a 1080px field with full-width rules under it. */
.tech {
  --rail: 12rem;
  --rail-gap: 2.5rem;
  --band: 48.5rem;
  position: relative; z-index: 2;
  background: var(--bg);
  border-top: 1px solid var(--line);
  padding: clamp(4rem, 12vh, 8rem) max(var(--gutter), calc((100% - var(--band)) / 2));
}
/* One grid governs the whole section: a fixed rail holding labels, a main column
   holding every sentence. Unlabelled prose defaults into the main column, so there is
   one label edge and one reading edge from the heading to the closing guardrail. */
.tech-grid { display: grid; row-gap: 1.75rem; }
.tech h2 { margin-bottom: 0.25rem; }
/* The grid column, not a per-element cap, is what governs the measure now: every
   block ends at the same right edge because they all share one column. */
.tech-grid p, .tech-grid ul { max-width: none; }
@media (min-width: 800px) {
  .tech-grid { grid-template-columns: var(--rail) minmax(0, 1fr); column-gap: var(--rail-gap); }
  .tech-grid > * { grid-column: 2; }
  .tech-grid > h2, .tech-grid > .rail-row, .tech-grid > .facts { grid-column: 1 / -1; }
}
/* The one-sentence answer sits directly under the question, so it is set larger and in
   --ink while everything under it is --ink-soft body copy. */
.lede {
  color: var(--ink); font-size: clamp(1.1875rem, 2.2vw, 1.5rem);
  line-height: 1.35; letter-spacing: -0.02em;
  margin-bottom: 0;
}
.tech-body { color: var(--ink-soft); font-size: 1.0625rem; margin-bottom: 0; }
/* A spec table, not a bullet list. Each row is a category label plus the sentence that
   answers it, so a reader scanning for "does it handle LiDAR" reads four labels instead
   of four sentences. The hairline rules span the rail and the main column together,
   which is exactly what the rail makes honest: the rule now covers something. */
.facts {
  list-style: none; padding: 0; margin: 1rem 0;
  display: grid; gap: 0; border-bottom: 1px solid rgba(255,255,255,0.1);
}
/* .fact and .rail-row are the same row: label in the rail, content in the main column.
   The fact rows add the hairline and the padding that make them read as a table. */
.fact, .rail-row { display: grid; gap: 0.3rem; }
.fact {
  padding: 1.1rem 0;
  border-top: 1px solid rgba(255,255,255,0.1); letter-spacing: -0.01em;
}
/* Beside the content on wide viewports, above it on narrow ones, where a fixed label
   column would leave the sentence too little room to set. */
@media (min-width: 800px) {
  .fact, .rail-row {
    grid-template-columns: var(--rail) minmax(0, 1fr);
    gap: 0 var(--rail-gap); align-items: baseline;
  }
}
/* One label style for the whole rail: the fact labels set the pattern, the section
   now follows it everywhere a block needs naming. */
.rail-label, .fact-label {
  color: var(--ink-soft); font-size: 0.8125rem; font-weight: 500;
  letter-spacing: 0.06em; line-height: 1.5; text-transform: uppercase;
  margin-bottom: 0;
}
.rail-anchored { color: var(--green); }
.fact-body { color: var(--ink); font-size: 1.0625rem; }
/* Technical footnote: smaller and set on a slightly tighter leading than the body
   paragraphs so it reads as an aside without dropping below AA. */
.tech-note { color: var(--ink-soft); font-size: 0.875rem; line-height: 1.55; }
.invariant-list { color: var(--ink-soft); }
.invariant-list li { padding: 0.2rem 0; break-inside: avoid; }
/* Two columns inside the main column, because these are short enumerated phrases: a
   single column of them would leave two thirds of the reading column empty, which is
   the exact rag the rail was introduced to remove. */
@media (min-width: 800px) {
  /* 2rem, not --rail-gap: at 2rem each of the six "what can vary" phrases holds one
     line, which is what keeps the pair of lists from looking like wrapped prose. */
  .invariant-list { columns: 2; column-gap: 2rem; }
  /* The two invariant lists are a pair, so the air before the pair is larger than the
     air inside it. */
  .rail-row-anchored { margin-top: 1.75rem; }
  .rail-row-varies { margin-top: 0.75rem; }
}
.closing { margin-top: 4rem; text-align: center; }
/* Balanced because the band is narrower than the sentence now: the greedy break left
   "own." alone on a second line under a centred statement. */
.closing p {
  max-width: none; font-size: clamp(1.5rem, 3.5vw, 2.25rem);
  letter-spacing: -0.03em; margin-bottom: 1.75rem;
  text-wrap: balance;
}
.closing .cta { margin: 0 auto; }

/* ───── Motion states (JS adds html.js; without JS everything is visible) ───── */
html.js .reveal { opacity: 0; transform: translateY(14px); }
html.js .reveal.is-visible { opacity: 1; transform: none; transition: opacity 0.6s ease, transform 0.6s ease; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  html.js .reveal { opacity: 1; transform: none; transition: none; }
  * { transition: none !important; }
}

/* ───── Scroll-scrubbed background film ───── */
/* --poster is the still shown before/behind the film. It is declared here so the
   token always resolves, and re-set on this element by bg-scrub.js when a use case
   with a different film is activated. */
.bg-world { --poster: url("assets/bg/poster.webp"); position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; }
.bg-world video, .bg-world::before {
  position: absolute; inset: 0; width: 100%; height: 100%;
}
.bg-world video { object-fit: cover; }
/* While a cold film swap is in flight the previous use case's film is still the
   decoded one, so it is hidden and the repointed ::after poster shows through. That
   is what makes a tab click change the background immediately instead of after a
   3 to 15 MB download. Opacity only, and instant: a fade would read as a transition
   between two scenes rather than as the new scene arriving. */
.bg-world.is-swapping video { opacity: 0; }

/* ───── Opening film loading indicator ───── */
.film-gate {
  position: fixed; left: 50%; bottom: 1.5rem; z-index: 30;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 0.6rem;
  pointer-events: none;
}
.film-gate[hidden] { display: none; }
.film-gate-label {
  font-size: 0.75rem; font-weight: 500; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-soft); white-space: nowrap;
}
.film-gate-bar {
  width: 180px; height: 2px; border-radius: 2px;
  background: rgba(255, 255, 255, 0.22); overflow: hidden;
}
/* scaleX rather than width: the bar updates on every streamed chunk, and transform
   is the only thing that can be driven that often without touching layout. */
.film-gate-fill {
  display: block; height: 100%; background: var(--ink);
  transform: scaleX(0); transform-origin: left center;
  transition: transform 0.2s ease;
}
/* Bottom-anchored gradient, not a flat veil: the top ~25% of the film stays
   completely raw, exactly as the reference intends, while bottom-anchored text
   sits on a dark bed that survives the snow leg's near-white frames.
   The mid stops (32–58%) track the top edge of the hero copy: when the headline
   grew to three lines (top edge ~50% of viewport) the old 0.30 stop left white
   type at a measured 3.62:1 over the source frame's sky, and the motto kicker now
   extends the lead block to ~45% of the viewport, so the 46–58% band is deepened
   again (0.62→0.70, 0.30→0.55). The band the copy occupies is the band that has
   to be dark; the top quarter is untouched. */
.bg-world::before {
  content: ""; z-index: 1;
  background: linear-gradient(to top,
    rgba(0,0,0,0.93) 0%,
    rgba(0,0,0,0.88) 16%,
    rgba(0,0,0,0.78) 32%,
    rgba(0,0,0,0.70) 46%,
    rgba(0,0,0,0.55) 58%,
    rgba(0,0,0,0.18) 68%,
    rgba(0,0,0,0) 76%);
}
@media (prefers-reduced-motion: reduce) {
  .bg-world video { display: none; }
}
.bg-world::after {
  content: ""; position: absolute; inset: 0;
  background: var(--poster, url("assets/bg/poster.webp")) center / cover no-repeat;
}
.bg-world video { z-index: 0; }
.bg-world::after { z-index: -1; }
/* JS-off Chromium shows default media controls on unattributed <video> elements as a
   scripting fallback; the background layer is decorative and aria-hidden, so suppress them. */
.bg-world video::-webkit-media-controls,
.bg-world video::-webkit-media-controls-enclosure { display: none !important; }

/* ───── Character-by-character heading ───── */
.heading-line { display: inline-block; }
.heading-word { display: inline-block; white-space: nowrap; }
html.js .heading-char {
  display: inline-block;
  opacity: 0; transform: translateX(-18px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}
html.js .is-typed .heading-char { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  html.js .heading-char { opacity: 1; transform: none; transition: none; }
}

/* ───── Use-case stage ───── */
.stage { position: relative; padding: var(--gutter); max-width: none; margin: 0; }
/* The tabs are the film-ui affordance for swapping the background film. Every static
   path (JS-off, reduced motion, coarse/narrow) stacks all three use cases with their
   titles and notes instead, so a tablist there would be dead controls sitting above
   content that is already fully visible, hence display: none until film-ui exists. */
.stage-tabs { display: none; gap: 0.25rem; border-radius: 999px; padding: 0.3rem; margin-bottom: 2rem; }
.stage-tab {
  appearance: none; background: none; border: 0; cursor: pointer;
  color: var(--ink-soft); font: inherit; font-size: 0.875rem; letter-spacing: -0.01em;
  white-space: nowrap;
  min-height: 44px; padding: 0.5rem 1.1rem; border-radius: 999px;
  transition: color 0.25s ease, background-color 0.25s ease;
}
.stage-tab:hover { color: var(--ink); }
.stage-tab[aria-selected="true"] { background: var(--ink); color: #000; }
/* The global :focus-visible ring is white; offset it outward so it reads against the
   selected tab's white pill as well as against the glass. */
.stage-tab:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }
.usecase-title { font-size: 1rem; font-weight: 500; color: var(--ink); margin-bottom: 0.4rem; }
.usecase-note { color: var(--ink-soft); font-size: 0.9375rem; margin-bottom: 1.25rem; max-width: 42ch; }
/* Titles and notes only exist on the static paths (film-ui hides them), where they
   scroll through the top of the viewport. The fixed .bg-world gradient is at ~0
   alpha up there, so they land on the poster's bright sky at a measured 2.3:1. Same
   remedy as .condition-line, one step darker than --glass because the note is
   --ink-soft rather than white: rgba(0,0,0,0.78) over a white frame => rgb(56,56,56),
   which is 7.9:1 for #D1D5DB and 11.7:1 for white. Static bed, nothing transitions. */
.usecase-title, .usecase-note {
  width: fit-content; border-radius: 10px; padding: 0.3rem 0.7rem;
  background: rgba(0, 0, 0, 0.78);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
}
.usecase + .usecase { margin-top: 3rem; }
/* The chip is fixed and centred with left: 50%, so like the tablist it resolves to
   min-content against half the viewport. Without nowrap "Power grid" broke in two and
   made the chip a two-line box at 390. Neither half of the chip is prose. */
.chip-usecase { color: var(--ink-soft); white-space: nowrap; }
.chip-usecase::after { content: "·"; margin: 0 0.4rem; }

.condition-lines { list-style: none; padding: 0; display: grid; gap: 1rem; }
/* The default (non-film-ui) state is what mobile/coarse, reduced-motion and JS-off
   readers get: the lines sit in normal flow near the top of the film section, where
   the bottom-anchored gradient is at ~0 alpha and the poster's bright sky is the
   backdrop. White measured 2.3:1 there. So the default carries the same glass bed
   the navbar and hero tag use (--glass over a white frame => rgb(87,87,87), 7.2:1).
   The properties are inlined rather than adding a `liquid-glass` class to the markup
   so the list items stay plain semantic content. The bed is static, nothing here is
   transitioned. html.film-ui strips it again below: on that path the lines are sticky
   inside the gradient band, where raw text on film is the intended look. */
.condition-line {
  font-size: clamp(1.5rem, 4vw, 2.75rem); font-weight: 400; letter-spacing: -0.04em;
  color: var(--ink); max-width: 20ch;
  display: block; width: fit-content;
  border-radius: 12px; padding: 0.35rem 0.85rem;
  background: var(--glass);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
}
/* Under film-ui the stage shows one use case at a time and the tabs float over the film. */
html.film-ui .stage-tabs {
  display: inline-flex;
  position: fixed; top: 6.5rem; left: 50%; z-index: 15; margin: 0;
  opacity: 0; transform: translateX(-50%) translateY(-8px);
  transition: opacity 0.4s ease, transform 0.4s ease;
}
html.film-ui .stage-tabs.is-visible { opacity: 1; transform: translateX(-50%); }
/* Hidden tabs must not swallow clicks meant for the page underneath; main.js takes
   them out of the tab order at the same moment (see syncTabIndex). */
html.film-ui .stage-tabs:not(.is-visible) { pointer-events: none; }
html.film-ui .usecase-title, html.film-ui .usecase-note { display: none; }
html.film-ui .usecase + .usecase { margin-top: 0; }

/* Sits low in the viewport so the gradient scrim guarantees AA over every film frame.
   padding-bottom is zeroed under film-ui so the sticky container runs out of travel
   exactly at the section's bottom edge, i.e. exactly where the opaque tech section
   begins. Any bottom padding leaves the lines that many pixels of extra travel, in
   which they ride up above the tech section over raw film (measured 1.43:1).
   The 480vh of scroll travel lives on the panel, not on .stage: .condition-lines is
   sticky inside its panel, so the panel, not the section, is the element whose
   height becomes the sticky travel. A tall .stage wrapping an auto-height panel
   would give the lines zero travel. .stage's own height follows the panel's. */
html.film-ui .stage { padding-bottom: 0; }
html.film-ui .usecase { display: none; min-height: 480vh; }
html.film-ui .usecase.is-active { display: block; }
html.film-ui .condition-lines { position: sticky; top: 68vh; height: 0; display: block; }
html.film-ui .condition-line {
  position: absolute; top: 0; left: 0; right: 0;
  /* Drop the default glass bed: here the lines ride at 68vh inside the gradient's
     dark end, and raw text on the film is the intended look. */
  width: auto; padding: 0; border-radius: 0;
  background: none; backdrop-filter: none; -webkit-backdrop-filter: none;
  box-shadow: none;
  opacity: 0; transform: translateY(12px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}
html.film-ui .condition-line.is-current { opacity: 1; transform: none; }

.chip { display: none; }
html.film-ui .chip {
  display: flex; align-items: center; gap: 0.75rem;
  position: fixed; left: 50%; bottom: 1.75rem; z-index: 15;
  border-radius: 999px; padding: 0.55rem 1.1rem;
  font-size: 0.8125rem; letter-spacing: 0.01em;
  opacity: 0; transform: translateX(-50%) translateY(8px);
  transition: opacity 0.4s ease, transform 0.4s ease;
}
html.film-ui .chip.is-visible { opacity: 1; transform: translateX(-50%); }
.chip-label { font-weight: 500; min-width: 14ch; color: var(--ink); white-space: nowrap; }
.chip-track { list-style: none; padding: 0; margin: 0; display: flex; gap: 0.4rem; }
.chip-dot {
  width: 6px; height: 6px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.45); opacity: 0.5;
  transition: opacity 0.3s ease, transform 0.3s ease, background-color 0.3s ease;
}
.chip-dot.is-active { background: var(--ink); border-color: var(--ink); opacity: 1; transform: scale(1.3); }

/* ───── Narrow viewports: the film UI now runs here too ───── */
/* The chrome was sized for a desktop viewport and all of it is fixed, so at 390 and
   375 it was over budget rather than merely tight. Measured at 390 before this block:
   the navbar CTA had 181px of room for ~186px of label and wrapped, which took the
   navbar from 60px to 80px and left its bottom edge at 104px, exactly where the fixed
   tablist sits; and the three tabs each ran to two lines. Tighter type and padding buy
   the room back. The 44px tap floor is untouched: it lives on min-height, and only the
   horizontal padding and the type size move. */
@media (max-width: 480px) {
  .site-nav { gap: 0.6rem; padding: 0.5rem 0.5rem 0.5rem 0.9rem; }
  .cta-sm { font-size: 0.8125rem; padding: 0.6rem 0.9rem; }
  .stage-tab { font-size: 0.8125rem; padding: 0.5rem 0.8rem; }
  /* The chip is fixed to the bottom edge and the condition lines are sticky at 68vh,
     so on a short viewport the two are the closest pair on screen. 1.25rem of inset
     plus the tighter chip padding measures a 114px gap between them at 375x812. */
  html.film-ui .chip { bottom: 1.25rem; gap: 0.6rem; padding: 0.5rem 0.9rem; }
}
