/* ==========================================================================
   FileRanger — marketing site
   Token names follow the design handoff §10.3 (Color system).
   ========================================================================== */

:root {
  /* Brand palette — handoff §10.3 */
  --fr-night:        #071629;
  --fr-deep-blue:    #0E2A49;
  --fr-slate-blue:   #224268;
  --fr-steel:        #9DA6B0;
  --fr-silver:       #D6D9DC;
  --fr-parchment:    #F4E7CF;
  --fr-ranger-amber: #D67A22;
  --fr-sunset-gold:  #F4B44D;
  --fr-saddle:       #6E3A18;

  /* Supporting values used by this page */
  --fr-gold-hi:  #F8C972;
  --fr-border:   #12304f;
  --fr-row:      #0B2340;
  --fr-row-line: #16344f;
  --fr-abyss:    #050f1d;
  --fr-muted:    #5c6b7d;
  --fr-cta-glow: #21456e;
  --fr-ui-blue:  #4CC2FF; /* the app's own accent — filter chips, checkboxes */

  /* Color tags */
  --tag-red:    #E5484D;
  --tag-orange: #E8843C;
  --tag-yellow: #F4C93C;
  --tag-green:  #46A758;
  --tag-blue:   #4C9DE8;
  --tag-purple: #8E6AD0;
  --tag-gray:   #8B9096;

  /* Motion. Single source of truth for the Quick Look demo loop — the popup and
     the Space key both read this, so they stay locked to each other. Change it
     here, not on the individual animations, or they drift apart. */
  --ql-loop: 5s;

  /* Type */
  --font-slab: 'Roboto Slab', serif;
  --font-sans: 'Noto Sans', system-ui, sans-serif;
  --font-mono: ui-monospace, monospace;

  /* ---------- Responsive scale ----------
     Three tokens carry almost all of this site's responsiveness, which is why
     there are so few media queries below. Each interpolates continuously from
     the 320px floor to the design's authored desktop value and then stops, so
     every width in between is composed rather than being a breakpoint's
     nearest neighbour — and every width at or above 1280px is pixel-identical
     to the original desktop build.

       --gutter     16px @320  ->  32px @1280+   (the authored page gutter)
       --section-y  56px @320  -> 120px @1280+   (the authored section rhythm)
       --shell      the 1240px content shell, unchanged

     env(safe-area-inset-*) is folded in at the edges rather than here, so a
     notched phone in landscape widens the gutter without also inflating the
     vertical rhythm. */
  --gutter:     clamp(1rem,   0.5rem + 2.5vw, 2rem);
  --gutter-l:   max(var(--gutter), env(safe-area-inset-left));
  --gutter-r:   max(var(--gutter), env(safe-area-inset-right));
  --section-y:  clamp(3.5rem, 2rem   + 7.5vw, 7.5rem);
  --shell:      1240px;
  --nav-h:      68px;
}

/* ---------- Base ---------- */

html {
  /* scroll-padding keeps an anchored heading clear of the sticky bar rather
     than landing underneath it. Tracks --nav-h so it stays correct when the
     bar shortens on small screens. */
  scroll-padding-top: calc(var(--nav-h) + 1rem);
}

/* Smooth scrolling is motion, and it is motion the visitor did not ask for on
   every in-page jump. Gated rather than global. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

body {
  margin: 0;
  background: var(--fr-night);
  color: var(--fr-silver);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;

  /* clip, not hidden. `overflow-x: hidden` forces the block axis to `auto`,
     which makes <body> a scroll container and breaks position:sticky on the
     nav in some engines; `clip` clips without creating one. This is a
     backstop, not the fix — every element that deliberately breaks its
     bounds (the characters, the roadrunner's run, the CTA artwork) is
     clipped by its own section, so nothing relies on this rule. */
  overflow-x: clip;
}

a { color: var(--fr-sunset-gold); text-decoration: none; }
a:hover { color: var(--fr-gold-hi); }
img { max-width: 100%; }
::selection { background: var(--fr-ranger-amber); color: var(--fr-night); }

/* ---------- Shared type ---------- */

.eyebrow {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fr-sunset-gold);
}

/* Heading scale. The vw terms were re-anchored so each size reaches its
   authored desktop maximum at exactly 1280px and holds there — the previous
   `Xvw` forms kept growing to 1440px+ and, more importantly, bottomed out at
   minimums (30/34/38px) that overflowed a 320px screen. The hierarchy between
   the three steps is preserved at every width, not just at the extremes. */
.h2 {
  font-family: var(--font-slab);
  font-weight: 700;
  font-size: clamp(1.625rem, 1.19rem + 2.17vw, 2.625rem);
  line-height: 1.1;
  margin: 0;
  color: var(--fr-parchment);
}
.h2--lg  { font-size: clamp(1.75rem, 1.19rem + 2.8vw,  3rem); }
.h2--cta { font-weight: 800; font-size: clamp(1.9rem, 1.24rem + 3.3vw, 3.5rem); line-height: 1.05; }

.balance { text-wrap: balance; }
.hl { color: var(--fr-parchment); }

.code {
  font-family: var(--font-mono);
  background: var(--fr-deep-blue);
  padding: 1px 6px;
  border-radius: 4px;
  color: var(--fr-sunset-gold);
}

/* ---------- Layout ---------- */

.container {
  max-width: var(--shell);
  margin: 0 auto;
  padding: var(--section-y) var(--gutter-r) var(--section-y) var(--gutter-l);
}
.container--narrow     { max-width: 860px; }
.container--mid        { max-width: 1000px; }
.container--flush-top  { padding-top: 0; }

/* Every section clips its own overflow. The characters, the roadrunner's run
   and the CTA artwork are all authored to break their bounds, and each needs
   the section it lives in to be the thing that contains it — relying on a
   document-level clip would mean any one of them could reintroduce a
   page-wide horizontal scrollbar. `clip` over `hidden` so none of these
   become scroll containers (which would break the sticky nav's ancestor
   chain and swallow scroll anchoring). */
.section,
.section--deep,
.section--privacy,
.section--brand,
.cta,
.trust,
.hero { overflow-x: clip; }

.section         { background: var(--fr-night); overflow: hidden; }
.section--deep   { background: var(--fr-deep-blue); border-top: 1px solid var(--fr-border); border-bottom: 1px solid var(--fr-border); }
.section--privacy{ background: linear-gradient(180deg, var(--fr-deep-blue) 0%, var(--fr-night) 100%); border-top: 1px solid var(--fr-border); }

/* ---------- Buttons & links ---------- */

/* An angle has to be a registered property to be animatable — a plain custom
   property is just a token to the interpolator, so the gradient would jump
   rather than sweep. */
@property --gold-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

/* The gradient's centre point drifts as well as rotating. Registered for the
   same reason as the angle: unregistered properties don't interpolate. */
@property --gold-x {
  syntax: '<percentage>';
  initial-value: 50%;
  inherits: false;
}
@property --gold-y {
  syntax: '<percentage>';
  initial-value: 50%;
  inherits: false;
}

/* Flat amber text button — the original style, now used only by the nav, where
   the artwork button's detail doesn't survive at 68px bar height.

   The gold stroke is a two-layer background rather than a bordered pseudo-
   element: the fill is painted to padding-box and the conic gradient to
   border-box, so the gradient shows only in the 2px border ring. Rotating
   --gold-angle sweeps the highlights round the edge.
   The `0deg` fallback inside var() matters — without @property support the
   declaration would be invalid at computed-value time and the button would
   lose its background entirely, not just its animation. */
.btn {
  position: relative; /* anchors the hover shine — see the card-hover block */
  --btn-fill: var(--fr-ranger-amber);
  border: var(--gold-stroke, 2px) solid transparent;
  background:
    linear-gradient(var(--btn-fill) 0 0) padding-box,
    /* Four highlights instead of two — the dark/mid/bright/mid/dark run now
       repeats every 90deg rather than every 180deg. The bright and mid tones
       alternate slightly between cycles so the ring doesn't read as a
       mechanical repeat as it turns. */
    conic-gradient(from var(--gold-angle, 0deg)
                   at var(--gold-x, 50%) var(--gold-y, 50%),
      #7A4E14   0deg,
      #D9A24A  19deg,
      #FFF3D0  34deg,
      #D9A24A  49deg,
      #7A4E14  70deg,
      #7A4E14  90deg,
      #C9922F 109deg,
      #FFE9A8 124deg,
      #C9922F 139deg,
      #7A4E14 160deg,
      #7A4E14 180deg,
      #D9A24A 199deg,
      #FFF3D0 214deg,
      #D9A24A 229deg,
      #7A4E14 250deg,
      #7A4E14 270deg,
      #C9922F 289deg,
      #FFE9A8 304deg,
      #C9922F 319deg,
      #7A4E14 340deg,
      #7A4E14 360deg) border-box;
  color: var(--fr-night);
  font-weight: 700;
  border-radius: 10px;
  white-space: nowrap;
}

/* Only the fill changes on hover; swapping `background` wholesale here would
   drop the gradient layer and take the stroke with it. */
.btn:hover { --btn-fill: var(--fr-sunset-gold); color: var(--fr-night); }

@keyframes gold-spin { to { --gold-angle: 360deg; } }

/* Each axis sweeps back and forth on its own clock. */
@keyframes gold-drift-x {
  0%, 100% { --gold-x: 20%; }
  50%      { --gold-x: 80%; }
}
@keyframes gold-drift-y {
  0%, 100% { --gold-y: 12%; }
  50%      { --gold-y: 88%; }
}

/* The three durations are deliberately incommensurate — 6s / 7.3s / 11.1s.
   CSS has no random(), but periods that share no common multiple produce a
   combined path that doesn't visibly repeat (these realign only after ~40
   minutes), which reads as random rather than as a loop. Rounding them to
   6/8/12 would resync every 24s and the pattern would become obvious. */
@media (prefers-reduced-motion: no-preference) {
  .btn {
    animation:
      gold-spin    var(--gold-spin, 6s)      linear      infinite,
      gold-drift-x var(--gold-drift-x, 7.15s) ease-in-out infinite,
      gold-drift-y var(--gold-drift-y, 10.04s) ease-in-out infinite;
  }
}

.btn--sm { font-size: 15px; padding: 10px 20px; border-radius: 8px; }

/* Image-based download button, used by the hero and final CTA. The label is
   baked into the artwork, so the real text stays in the DOM but visually
   hidden: that keeps the accessible name, keeps the link findable in a screen
   reader's link list, and leaves something readable if the image fails. */
.btn-dl {
  position: relative; /* anchors the hover shine — see the shine block */
  display: block;
  width: var(--btn-dl-w, 260px);
  aspect-ratio: 260 / 81;
  flex-shrink: 0;
  background: url('assets/images/btn-download.png') center / 100% 100% no-repeat;
}
.btn-dl:hover { background-image: url('assets/images/btn-download-hover.png'); }

/* The shine band gets masked to the button artwork's own alpha. This button's
   rounded corners are painted into the PNG rather than set in CSS, so there's
   no border-radius for the band to inherit — unmasked it would spill square
   corners into the transparent ones and show shine outside the button edge.
   Masking with the same image clips it to the exact silhouette, no radius to
   guess and nothing to re-tune if the artwork changes shape. */
.btn-dl::after {
  -webkit-mask: url('assets/images/btn-download.png') center / 100% 100% no-repeat;
          mask: url('assets/images/btn-download.png') center / 100% 100% no-repeat;
}

.btn-dl--md { --btn-dl-w: 232px; }
.btn-dl--lg { --btn-dl-w: 260px; margin-top: 6px; }

/* Off-screen but still announced. clip-path over display:none/visibility so
   screen readers keep reading it. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.link-arrow { color: var(--fr-sunset-gold); font-weight: 600; font-size: 16px; padding: 15px 12px; }
.link-arrow:hover { color: var(--fr-gold-hi); }

/* ---------- Sticky nav ---------- */

.nav {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(7, 22, 41, 0.88);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--fr-slate-blue);
}
.nav__inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding-inline: var(--gutter-l) var(--gutter-r);
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
.nav__brand { display: flex; align-items: center; gap: 12px; color: var(--fr-silver); min-width: 0; }
.nav__logo { border-radius: 8px; display: block; flex-shrink: 0; }
.nav__wordmark {
  font-family: var(--font-slab);
  font-weight: 700;
  font-size: 20px;
  letter-spacing: 0.01em;
  color: var(--fr-parchment);
}
.nav__links { display: flex; align-items: center; gap: 28px; font-size: 15px; font-weight: 500; }
.nav__link { color: var(--fr-steel); }
.nav__link:hover { color: var(--fr-parchment); }

/* ---------- Hamburger navigation (everything below 1024px) ----------

   One DOM for both modes. The same .nav__menu holds the same links and the
   same CTA at every width; only its box changes. Above 1024px it is
   `display: contents`, so .nav__links and the button are laid out as direct
   flex children of .nav__inner exactly as they were before this drawer
   existed — which is why the desktop bar is byte-for-byte the original.
   Below 1024px the same element becomes a drawer.

   A duplicated mobile menu would have been simpler to style and worse in
   every other way: two copies of every link in the accessibility tree, two
   sets of ids to keep unique, and an open/closed state to keep in sync. */

/* No-JS baseline. The toggle is a control we cannot operate without the
   script, so it stays hidden until the script announces itself; the menu is
   meanwhile a plain wrapping row of links, which is usable at every width
   even if it is not pretty. `.has-js-nav` is set by assets/js/nav.js. */
.nav__toggle,
.nav__scrim { display: none; }

@media (max-width: 63.99rem) {
  .nav__inner { flex-wrap: wrap; height: auto; padding-block: 10px; row-gap: 10px; }
  .nav__menu  { display: flex; align-items: center; gap: 12px 24px; flex-wrap: wrap; }
}

.has-js-nav .nav__toggle {
  /* 44x44 hit area around a 20px glyph — the target is the button, not the
     bars. Negative margin keeps the enlarged box from pushing the bar taller
     than its 68px. */
  width: 44px;
  height: 44px;
  margin-right: -10px;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 10px;
  background: none;
  color: var(--fr-parchment);
  cursor: pointer;
}
.nav__toggle:hover { border-color: var(--fr-slate-blue); }

.nav__toggle-box {
  width: 20px;
  height: 14px;
  position: relative;
  display: block;
}
.nav__bar {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
}
.nav__bar:nth-child(1) { top: 0; }
.nav__bar:nth-child(2) { top: 6px; }
.nav__bar:nth-child(3) { top: 12px; }

/* Bars fold into a cross. The outer two rotate about the middle bar's line,
   so they converge on the centre rather than pivoting in place. */
.nav__toggle[aria-expanded='true'] .nav__bar:nth-child(1) { top: 6px; transform: rotate(45deg); }
.nav__toggle[aria-expanded='true'] .nav__bar:nth-child(2) { opacity: 0; }
.nav__toggle[aria-expanded='true'] .nav__bar:nth-child(3) { top: 6px; transform: rotate(-45deg); }

@media (prefers-reduced-motion: no-preference) {
  .nav__bar { transition: transform 220ms ease, top 220ms ease, opacity 140ms linear; }
}

/* The drawer itself. Only below the desktop navigation breakpoint, and only
   once the script is present to open it. */
@media (max-width: 63.99rem) {
  .has-js-nav .nav__inner { flex-wrap: nowrap; height: var(--nav-h); padding-block: 0; }

  .has-js-nav .nav__menu {
    position: fixed;
    /* Positioned against .nav, NOT the viewport. .nav carries
       backdrop-filter: blur(12px), and any backdrop-filter other than `none`
       makes an element the containing block for its fixed-position
       descendants — so `inset: 68px 0 0 0` resolved against a 68px-tall bar
       and collapsed this panel to 44px. Anchoring to `top: 100%` (the bar's
       own bottom edge) and giving it an explicit height sizes it correctly
       without touching the bar's blur, which is part of the design.

       .nav is sticky at top:0, so the panel stays pinned under it at any
       scroll position. */
    top: 100%;
    right: 0;
    left: auto;
    width: min(22rem, 100%);
    /* dvh so the panel ends at the true bottom edge rather than under the
       mobile browser's collapsing toolbar. */
    height: calc(100dvh - var(--nav-h));
    z-index: 99;

    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;

    padding:
      1.25rem
      max(1.25rem, env(safe-area-inset-right))
      max(1.5rem, env(safe-area-inset-bottom))
      1.25rem;

    /* This stylesheet has no global box-sizing reset, and the desktop build
       is authored against content-box — so it is set here rather than
       globally, where it would silently re-measure every padded box on the
       page. Without it the drawer's 20px padding and 1px border land OUTSIDE
       its 22rem width and push 41px past the viewport. */
    box-sizing: border-box;

    background: var(--fr-night);
    border-left: 1px solid var(--fr-slate-blue);
    box-shadow: -24px 0 60px rgba(0, 0, 0, 0.5);

    overflow-y: auto;
    overscroll-behavior: contain;

    /* Closed state. The reveal is a clip-path wipe rather than a
       translateX(100%) park, because the panel is position:fixed and a fixed
       element sitting outside the viewport still counts toward the
       document's scrollable width — measured, it added 388px of horizontal
       overflow at every width below 1024px, which is exactly the page-level
       scrollbar this whole exercise is meant to eliminate. Clipping reveals
       the same left-to-right slide without the box ever leaving the screen.

       visibility (not display:none) so the panel remains a transition target;
       it is out of the tab order and the accessibility tree either way. */
    visibility: hidden;
    opacity: 0;
    clip-path: inset(0 0 0 100%);
  }

  .has-js-nav .nav__menu[data-open] {
    visibility: visible;
    opacity: 1;
    clip-path: inset(0);
  }

  @media (prefers-reduced-motion: no-preference) {
    .has-js-nav .nav__menu {
      transition:
        clip-path  260ms cubic-bezier(0.32, 0.72, 0, 1),
        opacity    180ms linear,
        visibility 0s linear 260ms;
    }
    .has-js-nav .nav__menu[data-open] { transition-delay: 0s; }
  }

  .has-js-nav .nav__links {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    font-size: 1rem;
  }

  /* Full-width rows at 48px tall. The link is the target, not the word. */
  .has-js-nav .nav__link {
    display: flex;
    align-items: center;
    min-height: 48px;
    padding: 0 0.75rem;
    border-radius: 10px;
    color: var(--fr-silver);
  }
  .has-js-nav .nav__link:hover { background: var(--fr-deep-blue); color: var(--fr-parchment); }

  .has-js-nav .nav__cta {
    margin-top: 0.85rem;
    text-align: center;
    padding: 14px 20px;
    font-size: 1rem;
  }

  /* Scrim. Sits under the drawer and over the page, and is what makes an
     outside tap close the menu. */
  .has-js-nav .nav__scrim {
    display: block;
    position: fixed;
    /* Same containing-block caveat as the panel above. */
    top: 100%;
    left: 0;
    right: 0;
    height: 100dvh;
    z-index: 98;
    background: rgba(5, 15, 29, 0.6);
    backdrop-filter: blur(2px);
    border: 0;
    padding: 0;
    opacity: 0;
    visibility: hidden;
  }
  .has-js-nav .nav__scrim[data-open] { opacity: 1; visibility: visible; }

  @media (prefers-reduced-motion: no-preference) {
    .has-js-nav .nav__scrim { transition: opacity 200ms linear, visibility 0s linear 200ms; }
    .has-js-nav .nav__scrim[data-open] { transition-delay: 0s; }
  }
}

/* Desktop navigation returns at 1024px. This boundary is set by the project,
   not by the content: the bar itself fits comfortably from about 780px up
   (wordmark + five links + button measure ~700px against a 1024px shell), so
   1024 is a deliberate choice to keep portrait tablets and phones on the
   drawer rather than the width at which the row would break.

   Note this is the only breakpoint in the stylesheet at 64rem that is about
   navigation; the feature and hero splits share the value by coincidence, not
   because they are related. */
@media (min-width: 64rem) {
  /* Matched on .has-js-nav so this outranks the drawer rules, which are
     themselves scoped to it — a media query adds no specificity of its own. */
  .has-js-nav .nav__toggle,
  .has-js-nav .nav__scrim,
  .nav__toggle,
  .nav__scrim { display: none; }

  /* display:contents dissolves the wrapper, restoring the original
     brand | links | button flex row exactly. */
  .nav__menu { display: contents; }
}

/* Locked while the drawer is open. Applied to <html> by the nav script;
   scroll position is preserved because the element keeps its box and only
   stops scrolling. */
.has-nav-open { overflow: hidden; }

/* ---------- Parallax header ----------
   Ten registered 1728x1013 plates: six scene plates plus four lens-flare
   ghosts. Depth is expressed as --drift: deeper
   planes lag further behind the scroll, near planes outrun it. Driven by a
   scroll-progress timeline, so no JavaScript. Browsers without scroll-driven
   animation support simply render the composed scene, static. */

.hdr {
  position: relative;
  width: 100%;
  aspect-ratio: 1728 / 1013;
  /* min-height: 420px; */
  max-height: 92svh;
  overflow: hidden;
  background: var(--fr-night);

  /* Required, not cosmetic. The flare plates use mix-blend-mode: screen, and
     an un-isolated blend group blends against everything behind it — which
     promotes this whole section above the sticky nav's stacking context and
     paints the mobile drawer underneath the artwork. Isolating confines the
     blend to this section, which is where every plate the flares are meant to
     composite against already lives, so the header is pixel-identical. */
  isolation: isolate;
}

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

  /* --- Per-plate composition knobs. Neutral here; override per layer below. ---
     --x / --y   nudge the plate. % resolves against the plate's own box
                 (--x against width, --y against height); px also works.
     --scale     per-plate zoom. Values >1 also buy headroom for --drift:
                 a plate needs roughly (scale - 1) / 2 of slack per edge.
     --origin    the point --scale grows from, and --drift pivots around.
     --pos       object-fit crop anchor — reframes inside the box without
                 moving the box, so it never exposes an edge.
     --drift     travel across the scroll range. Positive lags behind the
                 scroll (reads as distant), negative outruns it (reads near). */
  --x: 0%;
  --y: 0%;
  --scale: 1;
  --origin: 50% 50%;
  --pos: center 30%;
  --drift: 0%;

  object-position: var(--pos);
  transform-origin: var(--origin);
  transform: translate3d(var(--x), var(--y), 0) scale(var(--scale));
  pointer-events: none;
}

/* Each plate's full state on one line, near-to-far. */
.hdr__layer--sky   { --x: 0%; --y: 0%; --scale: 1; --drift:  50%; }
.hdr__layer--far   { --x: 0%; --y: 10%; --scale: 1; --drift: 40%; }
.hdr__layer--mesa  { --x: 0%; --y: 10%; --scale: 1; --drift: 30%; }
.hdr__layer--scene { --x: 0%; --y: 10%; --scale: 1; --drift: -12%; }
.hdr__layer--mark  { --x: 0%; --y: 13%; --scale: 0.95; --drift: -50%; }
.hdr__layer--sign  { --x: 0%; --y: 12%; --scale: 1; --drift: -20%; }

/* Lens-flare ghosts. These plates are black-backed JPEGs with no alpha, so they
   composite additively: `screen` drops the black to nothing and keeps only the
   ghost. They sit above the scene because a flare is an artefact of the lens,
   in front of everything it photographs. Same knobs as any other plate, plus
   --alpha to dial each ghost back independently.
   Ordered by distance from the sun along the flare axis: 1 nearest, 4 furthest. */
.hdr__flare {
  mix-blend-mode: screen;
  opacity: var(--alpha, 1);
}

.hdr__flare--1 { --x: 0%; --y: 0%; --scale: 1; --alpha: 0.9; --drift: 55%; }
.hdr__flare--2 { --x: 0%; --y: 0%; --scale: 1; --alpha: 0.7; --drift: 65%; }
.hdr__flare--3 { --x: 0%; --y: 0%; --scale: 1; --alpha: 0.8; --drift: 70%; }
.hdr__flare--4 { --x: 0%; --y: 0%; --scale: 1; --alpha: 0.8; --drift: 80%; }

/* The drift is added to --y, so a plate's authored position is its start
   point rather than being overwritten when the animation takes over. */
@keyframes hdr-drift {
  to {
    transform: translate3d(var(--x), calc(var(--y) + var(--drift)), 0) scale(var(--scale));
  }
}

@media (prefers-reduced-motion: no-preference) {
  .hdr__layer {
    will-change: transform;
    animation: hdr-drift linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 100vh;
  }
}

/* three.js dust particle canvas — rendered above the stampede scene layer
   but below the foreground mark/sign layers in DOM order. Fills the header
   band; pointer-events disabled so it never blocks clicks. */
.hdr__dust {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  display: block;
}

/* Melts the bottom of the artwork into the hero below. */
.hdr__veil {
  position: absolute;
  inset: auto 0 0;
  height: 22%;
  background: linear-gradient(180deg, rgba(7, 22, 41, 0) 0%, rgba(7, 22, 41, 0.55) 62%, var(--fr-night) 100%);
  pointer-events: none;
}

/* The plate is a 1.71:1 landscape composition. Left alone at 375px it would
   be 220px tall — a letterbox strip. min-height forces it back to a usable
   band and object-fit crops into the artwork instead of shrinking it, with
   --pos walking the crop toward the rider so the subject survives the trim.
   svh, not vh: with vh the band is sized for the *collapsed* browser chrome
   and jumps on first scroll as the toolbar retracts. */
@media (max-width: 47.99rem) {
  .hdr { min-height: 21.25rem; max-height: 70svh; }
  .hdr__layer { --pos: 32% 32%; }
}
@media (max-width: 30rem) {
  .hdr { min-height: 17.5rem; }
  .hdr__layer { --pos: 34% 34%; }
}

/* ---------- Hero ---------- */

.hero {
  /* Glow centred on the screenshot's bottom edge. .hero__media is align-self:end
     and .hero__shot drops its bottom border/radius, so the image's bottom edge
     and the section's bottom edge are the same line — hence --glow-y: 100%. */
  --glow-x: 75%;
  --glow-y: 100%;
  --glow-w: 1100px;
  --glow-h: 600px;
  background: radial-gradient(var(--glow-w) var(--glow-h) at var(--glow-x) var(--glow-y), var(--fr-deep-blue) 0%, var(--fr-night) 65%);
  border-bottom: 1px solid var(--fr-border);
}
/* Single column by default; the 5fr/7fr split returns at 64rem, which is the
   first width where the copy column is still wide enough to hold a readable
   measure beside the screenshot. Below that the two would each be starved. */
.hero__inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: clamp(2.5rem, 1.333rem + 5.833vw, 6rem) var(--gutter-r) 0 var(--gutter-l);
  display: grid;
  gap: clamp(2rem, 1.5rem + 2.5vw, 3.5rem);
  align-items: center;
}
.hero__copy {
  display: flex;
  flex-direction: column;
  gap: 22px;
  padding-bottom: clamp(2.5rem, 1.333rem + 5.833vw, 6rem);
}
.hero__title {
  font-family: var(--font-slab);
  font-weight: 800;
  /* 34px at 320 rather than the old 44px floor, which pushed "exists." past
     the edge on a 320px screen. Still reaches the authored 68px at 1280. */
  font-size: clamp(2.125rem, 1.28rem + 4.2vw, 4.25rem);
  line-height: 1.03;
  margin: 0;
  color: var(--fr-parchment);
  text-wrap: balance;
}
.hero__sub {
  font-size: clamp(1.0625rem, 1rem + 0.3125vw, 1.25rem);
  line-height: 1.55;
  color: var(--fr-steel);
  margin: 0;
  max-width: 46ch;
  text-wrap: pretty;
}

@media (min-width: 64rem) {
  .hero__inner { grid-template-columns: 5fr 7fr; }
}
.hero__actions { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
.hero__trust { font-size: 14px; color: var(--fr-steel); display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.hero__sep { color: var(--fr-slate-blue); }
.hero__media { align-self: end; position: relative; }
.hero__shot {
  display: block;
  width: 100%;
  /* Same trap .shot documents below: the width/height attributes that reserve
     this image's box (and keep it out of the CLS budget) also supply a
     presentational height hint, which wins over `width: 100%` alone and
     renders the shot at its literal pixel height. */
  height: auto;
  border-radius: 12px 12px 0 0;
  border: 1px solid var(--fr-slate-blue);
  border-bottom: none;
  box-shadow: 0 -12px 80px rgba(244, 180, 77, 0.12), 0 24px 64px rgba(0, 0, 0, 0.5);
}

/* ---------- Trust strip ---------- */

.trust { background: var(--fr-deep-blue); border-bottom: 1px solid var(--fr-border); }
.trust__inner {
  max-width: 1240px;
  margin: 0 auto;
  padding: 22px var(--gutter-r) 22px var(--gutter-l);
  display: flex;
  justify-content: center;
  gap: 12px clamp(1.25rem, 0.833rem + 2.083vw, 2.5rem);
  flex-wrap: wrap;
  font-size: 14px;
  font-weight: 600;
  color: var(--fr-silver);
}
.trust__item { display: flex; align-items: center; gap: 8px; }
.trust__dot { width: 7px; height: 7px; border-radius: 50%; background: var(--fr-sunset-gold); display: inline-block; }

/* ---------- Problem ---------- */

.problem { text-align: center; display: flex; flex-direction: column; gap: 24px; align-items: center; /* force GPU compositing so the magnifier doesn't flicker */ }

/* Two-line lockup: an exasperated amber "Windows!" over the complaint itself.
   The heading shrinks to the width of its long line and centres as a block,
   which is what lets the kicker sit flush against that line's left edge —
   centring the kicker as text instead would strand it over the middle of the
   line and break the read. On narrow viewports max-width hands wrapping back
   to the normal centred flow. */
.problem__head {
  width: fit-content;
  max-width: 100%;
  text-align: left;
  transform: rotate(1.25deg)
}
.problem__kicker {
  display: block;
  font-size: 0.8em;
  line-height: 1.05;
  color: var(--fr-ranger-amber);
  transform: rotate(-3deg) translateY(-0.5em);
}
/* The lockup is a rotated fit-content block, so its corners swing wider than
   its box — at 320px that is enough to push a page-level scrollbar. Below
   48rem it returns to ordinary centred flow and the tilts come off: the
   kicker's -3deg over a wrapped two-line heading reads as a mistake rather
   than a flourish. */
@media (max-width: 47.99rem) {
  .problem__head   { text-align: center; width: auto; transform: none; }
  .problem__kicker { transform: translateY(-0.15em); }
}
.problem__lede {
  font-size: 19px;
  line-height: 1.6;
  color: var(--fr-steel);
  margin: 0;
  max-width: 62ch;
  text-wrap: pretty;
}
/* Intrinsic, not counted. `auto-fit` + `minmax(min(100%, X), 1fr)` reflows on
   the space actually available instead of on a device guess, so there is no
   breakpoint here at all: two columns wherever two fit, one where they don't.
   min(100%, X) is what stops the track floor from overflowing a viewport
   narrower than X — a bare minmax(18rem, 1fr) blows out at 320px.
   Verified to still resolve to exactly 2 columns in the 796px narrow shell. */
.problem__grid {
  position: relative; /* anchor for the magnifier below */
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: 20px;
  margin-top: 16px;
  text-align: left;
  width: 100%;
}

/* Magnifier resting on its mound, straddling the grid's bottom edge. Absolutely
   positioned, so it drops out of grid flow rather than becoming a fifth cell.
   top:100% puts its top on that edge; the translate then lifts it back up by a
   share of its own height, which is what makes it overlap rather than hang.
   The PNG is padded (T33/B33 of 300), so the mound sits higher than the box. */
.problem__lens {
  position: absolute;
  left: 50%;
  top: 100%;
  width: var(--lens-width, 23%);
  height: auto;
  transform: translate(-50%, var(--lens-lift, -32%));
  pointer-events: none;
  /* Above the cards even when one of them is raised on hover — without this
     the lens loses the overlap the moment card 3 or 4 is picked up. */
  z-index: 3;
}
/* Thumbs-down marker sits in the card's left gutter. The icon is absolutely
   positioned and the text is inset by padding, rather than the icon sitting
   inline — that way a card wrapping to two lines aligns its second line under
   the text instead of sliding back under the icon. */
.problem__card {
  position: relative;
  background: var(--fr-deep-blue);
  border: 1px solid var(--fr-slate-blue);
  border-radius: 10px;
  padding: 18px 20px 18px
           calc(20px + var(--thumb-size, 36px) + var(--thumb-gap, 18px));
  font-size: 15px;
  line-height: 1.5;
  color: var(--fr-silver);
}

/* top: 18px card padding + 0.15em, which centres an 18px icon in the 22.5px
   line box (15px x 1.5) so it holds the first line when the text wraps. */
.problem__card::before {
  content: '';
  position: absolute;
  left: 20px;
  top: calc(24px + 0.15em);
  width: var(--thumb-size, 32px);
  height: var(--thumb-size, 32px);
  background: url('assets/thumb_down.svg') center / contain no-repeat;
}

/* ---------- Mechanism / three steps ---------- */

.how { display: flex; flex-direction: column; gap: 56px; align-items: center; }
.how__head { text-align: center; max-width: 760px; display: flex; flex-direction: column; gap: 20px; }
.how__lede { font-size: 18px; line-height: 1.6; color: var(--fr-steel); margin: 0; text-wrap: pretty; }

/* margin-top buys the room the icons overhang into — they're lifted out of the
   cards, so without it they'd collide with the lede above. */
/* Three cards, but expressed intrinsically — with only three items, auto-fit
   collapses any surplus track to zero and the 1fr floors let the three stretch
   to fill the row, so the desktop result is identical to `1fr 1fr 1fr`.
   The icons overhang the card tops, so the clearance has to shrink with the
   cards or it leaves a hole above a stacked column. */
.steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  gap: 24px;
  width: 100%;
  margin-top: var(--step-icon-clearance, clamp(3.25rem, 1.5rem + 5vw, 5rem));
}
.step {
  position: relative; /* containing block for the overhanging icon */
  background: var(--fr-night);
  border: 1px solid var(--fr-slate-blue);
  border-radius: 12px;
  padding: 32px 28px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  user-select: none; 
}  
/* Icons straddle the card's top edge — centred, sized against the card so they
   scale with it, and lifted so most of each sits above the card. Positioned
   rather than in flow, which keeps them out of the text stack: the number sits
   at the left, clear of the icon's centred footprint. */
.step__icon {
  /* Local alias so the shared hover rule can rebuild this transform without
     knowing which card type it is. */
  --lift: var(--step-icon-lift, -58%);
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--step-icon, 45%);
  height: auto;
  transform: translate(-50%, var(--lift));
  pointer-events: none;

  /* drop-shadow, not box-shadow — these are cutout PNGs, so the shadow has to
     follow each icon's alpha silhouette rather than its rectangular box.
     Two passes: a faint close one for contact, a wide diffuse one for the soft
     falloff. Stacking them reads softer than a single large blur. */
  filter:
    drop-shadow(0 var(--icon-shadow-near-y, 6px) var(--icon-shadow-near-blur, 10px)
                rgba(0, 0, 0, var(--icon-shadow-near-alpha, 0.28)))
    drop-shadow(0 var(--icon-shadow-far-y, 22px) var(--icon-shadow-far-blur, 30px)
                rgba(0, 0, 0, var(--icon-shadow-far-alpha, 0.4)));
}

.step__num { font-family: var(--font-slab); font-weight: 800; font-size: 40px; color: var(--fr-ranger-amber); line-height: 1; }
.step__title { font-weight: 700; font-size: 18px; color: var(--fr-parchment); }
.step__body { margin: 0; font-size: 15px; line-height: 1.55; color: var(--fr-steel); }

/* ---------- Hover shine + icon pop (step cards, format cards, nav button) ----------
   One definition serving both. Each icon declares a --lift alias for its own
   offset, which is what lets the hover rule rebuild the transform without
   caring which card it is. Whole effect is gated on prefers-reduced-motion, so
   a visitor who asked for less movement gets a static card, not a damped one. */
@media (prefers-reduced-motion: no-preference) {

  /* Card pop. 5% on the box itself, compounding with the icon's own scale so
     the icon still leads and the card follows underneath it. Deliberately no
     z-index: a 5% grow adds ~9px per side to a step card against a 24px
     gutter, and ~6px to a format card against a 20px one, so neither ever
     reaches its neighbour — raising one would only buy a paint-order flicker
     on the way back down. */
  .step,
  .format {
    transition: transform var(--card-pop-dur, 220ms) var(--pop-ease, cubic-bezier(0.34, 1.56, 0.64, 1));
  }
  .step:hover,
  .format:hover { transform: scale(var(--card-pop, 1.05)); }

  /* Download button. Same 5% and same curve as the cards, so the page has one
     hover vocabulary. The shine ::after is a child, so it scales with the
     button and stays registered to the artwork it's masked against. */
  .btn-dl {
    transition: transform var(--card-pop-dur, 220ms) var(--pop-ease, cubic-bezier(0.34, 1.56, 0.64, 1));
  }
  .btn-dl:hover { transform: scale(var(--btn-dl-pop, 1.05)); }

  /* Problem cards get the same pop plus a tilt. CSS has no random(), so the
     scatter is authored: a fixed angle per card, mixed in sign and magnitude
     across the ±3deg range so the four don't read as a mechanical alternation.
     Tune any one of them on its own.

     These do need a z-index, unlike the step and format cards. Two columns at
     the narrow measure leave a 14px gutter, and rotating a wide short box
     swings its corners out far enough to close most of that — so a lifted card
     can meet its neighbour and has to be the one on top. The z-index change is
     delayed by the pop duration on the way back down, which keeps the card
     above its neighbour until it has finished settling instead of dropping
     behind mid-animation. */
  .problem__card {
    --card-tilt: 0deg;
    transition:
      transform var(--card-pop-dur, 220ms) var(--pop-ease, cubic-bezier(0.34, 1.56, 0.64, 1)),
      z-index    0s linear var(--card-pop-dur, 220ms);
  }
  .problem__card:nth-child(1) { --card-tilt: -2.2deg; }
  .problem__card:nth-child(2) { --card-tilt:  3deg;   }
  .problem__card:nth-child(3) { --card-tilt: -3deg;   }
  .problem__card:nth-child(4) { --card-tilt:  1.7deg; }

  .problem__card:hover {
    transform: rotate(var(--card-tilt)) scale(var(--card-pop, 1.05));
    z-index: 2;
    transition:
      transform var(--card-pop-dur, 220ms) var(--pop-ease, cubic-bezier(0.34, 1.56, 0.64, 1)),
      z-index    0s;
  }

  /* Thumb pop, on the same timing as the step and format icons so every card
     on the page snaps the same way. It compounds with the card's own scale,
     which is the point — the icon leads and the card follows. Grown from its
     centre, which walks the left edge in from 20px to ~17px: still clear of
     the card border, and the text is inset past the icon by padding so it
     cannot be reached. */
  .problem__card::before {
    transition: transform var(--pop-dur, 180ms) var(--pop-ease, cubic-bezier(0.34, 1.56, 0.64, 1));
  }
  .problem__card:hover::before { transform: scale(var(--thumb-pop, 1.5)); }

  /* Toolbox cards press in rather than out — 10% down. Same overshooting curve
     as everything else on the page, which on a shrink dips a little past 0.9
     and settles back: the squash that makes it read as a press instead of a
     fade. Shrinking can't reach a neighbour, so no z-index here. */
  .tool {
    transition: transform var(--card-pop-dur, 220ms) var(--pop-ease, cubic-bezier(0.34, 1.56, 0.64, 1));
    user-select: none; /* prevents a text selection highlight from appearing on the card when the user clicks it */
  }
  .tool:hover { transform: scale(var(--tool-press, 0.95)); }

  /* Icon pop. The transition sits on the base rule, not on :hover, so it eases
     back out as well as in. The curve overshoots slightly past 1 — that tiny
     bounce is what makes it read as a snap rather than a slide. */
  .step__icon,
  .format__icon {
    transition: transform var(--pop-dur, 180ms) var(--pop-ease, cubic-bezier(0.34, 1.56, 0.64, 1));
    z-index: 5; /* above the card border when it pops, and above the shine */
  }

  .step:hover .step__icon,
  .format:hover .format__icon {
    transform:
      translate(-50%, var(--lift))
      scale(var(--icon-pop, 1.2));
  }

  /* Diagonal shine. Hard stops — the color repeats at the same position rather
     than ramping — so the band has crisp edges instead of a soft feather.

     It sweeps by animating background-position, not transform: neither card can
     use overflow:hidden to contain a moving child, because the icons overhang
     the top edge on purpose and would be clipped. A background is clipped to
     the element's own rounded box for free, so the band stays inside the card. */
  .step::after,
  .format::after,
  .btn::after,
  .btn-dl::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    pointer-events: none;
    background-image: linear-gradient(115deg,
      rgba(255, 255, 255, 0)                          40%,
      rgba(255, 255, 255, var(--shine-alpha, 0.15))   40%,
      rgba(255, 255, 255, var(--shine-alpha, 0.15))   47%,
      rgba(255, 255, 255, 0)                          47%);
    background-size: 300% 100%;
    background-repeat: no-repeat;
    /* Band parked off the left edge. */
    background-position: 100% 0;
    opacity: 0;
  }

  /* Transition lives only on :hover, so leaving the card snaps the band back
     instantly instead of reversing it — the next hover always starts clean. */
  .step:hover::after,
  .format:hover::after,
  .btn:hover::after,
  .btn-dl:hover::after {
    opacity: 1;
    background-position: -50% 0;
    transition:
      background-position var(--shine-dur, 1s) ease-out,
      opacity 100ms linear;
  }
}

/* ---------- Feature splits ---------- */

/* Stacked below 64rem. The asymmetric 5/7 split can't be expressed
   intrinsically the way the card grids can, and squeezing it into a portrait
   tablet leaves the copy at roughly 300px — narrower than the screenshot's own
   UI text. So the split waits until there is genuinely room for both.

   Source order already puts the copy first in half these blocks and the media
   first in the other half. When stacked that alternation is what the reader
   gets, which is the correct reading order in both cases: `--media-first`
   sections lead with the screenshot the copy then explains. No CSS reordering,
   so DOM order and visual order stay identical for keyboard users. */
.feature { display: grid; gap: clamp(1.75rem, 1rem + 2.5vw, 2.125rem); align-items: center; }

@media (min-width: 64rem) {
  .feature--text-first  { grid-template-columns: 5fr 7fr; }
  .feature--media-first { grid-template-columns: 7fr 5fr; }
}

.feature__copy { display: flex; flex-direction: column; gap: 18px; }
.feature__body { font-size: 17px; line-height: 1.6; color: var(--fr-steel); margin: 0; text-wrap: pretty; }
.feature__note { font-size: 15px; line-height: 1.6; color: var(--fr-steel); margin: 0; }
.feature__fineprint { font-size: 14px; line-height: 1.5; color: var(--fr-steel); margin: 0; }
/* Checkbox markers instead of bullets. Drawn as a ::before rather than
   list-style-image, which gives no control over size or vertical alignment —
   the icon needs both to sit on the text baseline properly.
   The left padding moves from the <ul> to each <li>, so the marker can be
   absolutely positioned against the item it belongs to and wrapped lines align
   under the text rather than under the icon. */
.feature__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--fr-silver);
}

.feature__list li {
  position: relative;
  padding-left: var(--check-gap, 28px);
}

/* top: 0.15em centres an 18px icon in the 22.5px line box (15px x 1.5), so it
   sits on the first line even when the item wraps. */
.feature__list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.15em;
  width: var(--check-size, 18px);
  height: var(--check-size, 18px);
  background: url('assets/checkbox.svg') center / contain no-repeat;
}

.feature__list--spaced { margin: 8px 0 0; }

/* Positioning context for props that overlap a feature screenshot. .shot has no
   overflow clip, so a prop can hang past the screenshot's edges freely. */
.feature__media { position: relative; }

/* Saguaro standing at the screenshot's right edge — offsets are proportional to
   the screenshot, so it holds its placement as the shot resizes. Negative right
   and bottom let the arm and the rocky base break out past the frame. */
/* Now a VP9 WebM with an alpha channel, so it still composites as a cutout
   rather than a rectangle. display:block matters here in a way it didn't for
   the <img>: a video is inline by default and would pick up descender space
   under its baseline. The width/height attributes supply the aspect ratio so
   the box is reserved before metadata arrives. */
.feature__cactus {
  position: absolute;
  right: var(--cactus-right, -35%);
  bottom: var(--cactus-bottom, -13%);
  width: var(--cactus-width, 50%);
  height: auto;
  display: block;
  overflow: hidden;
  pointer-events: none;
}

/* Hare and tortoise flanking the indexing screenshot. Both sit on the same
   ground line, so they share a --creature-bottom rather than each carrying its
   own — nudging one keeps them standing on the same plane as the other. */
.feature__rabbit,
.feature__tortoise {
  position: absolute;
  bottom: var(--creature-bottom, -23%);
  height: auto;
  pointer-events: none;
}
/* The rabbit webm is authored unlike every other character. Rather than a tight
   cutout it's a 736x736 STAGE: the hare runs across it toward the camera, so it
   changes scale dramatically through the loop — roughly 150px to 500px on
   screen at these settings. That means it can't be size-matched to the still
   the way the other swaps were; matching any one frame guarantees mismatching
   the rest. These values frame the stage over the screenshot's lower left so
   the run reads well across the whole cycle, rather than pinning one pose.
   The trim comes off --creature-bottom rather than replacing it, so nudging the
   shared value still moves the hare and the tortoise together. */
.feature__rabbit.is-motion {
  left: var(--rabbit-left, -30%);
  width: var(--rabbit-width, 55%);
  bottom: calc(var(--creature-bottom, -20%) - var(--rabbit-pad-trim, 7%));
  display: block; /* a <video> is inline by default; avoids a baseline gap */
}

/* The still keeps the png's own numbers — it's a tight crop, so it needs none
   of the above compensation. */
.feature__rabbit.is-still { left: var(--rabbit-left, -23%); width: var(--rabbit-width, 50%); }

/* The tortoise is now a VP9 WebM with alpha. Its box carries 8.5% bottom
   padding where the png had 0.6%, so left at the shared offset its feet would
   float above the ground line the hare stands on. The correction is expressed
   as a trim off --creature-bottom rather than a replacement value, so nudging
   the shared variable still moves both animals together — which is the whole
   point of them sharing it. */
.feature__tortoise {
  right: var(--tortoise-right, -11.9%);
  width: var(--tortoise-width, 30.2%);
  bottom: calc(var(--creature-bottom, -23%) - var(--tortoise-pad-trim, 3.6%));
  display: block; /* a <video> is inline by default; avoids a baseline gap */
}

/* Quick Look popup, overlaid on the app-window screenshot and looped so the
   preview appears to open on its own. Offsets are proportional to the shot, so
   the popup stays registered to the window as the screenshot resizes.
   drop-shadow rather than box-shadow: the PNG is a rounded cutout, so the
   shadow needs to follow its silhouette, not its rectangular box. */
.ql-popup {
  position: absolute;
  left: var(--ql-left, 13%);
  top: var(--ql-top, 23.4%);
  width: var(--ql-width, 77%);
  height: auto;
  pointer-events: none;
  filter:
    drop-shadow(0 8px 14px rgba(0, 0, 0, 0.34))
    drop-shadow(0 28px 46px rgba(0, 0, 0, 0.46));
}

/* Timing lives per-keyframe: the pop uses the same overshoot curve as the card
   icons, then it holds, then eases out. Most of the 3s is hold — a loop that
   spends its time animating reads as a distraction rather than a demo. */
@keyframes ql-pop {
  0%   { opacity: 0; transform: scale(0.94); animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); }
  8%  { opacity: 1; transform: scale(1); animation-timing-function: linear; }
  90%  { opacity: 1; transform: scale(1); animation-timing-function: cubic-bezier(0.4, 0, 1, 1); }
  95%  { opacity: 0; transform: scale(0.97); }
  100% { opacity: 0; transform: scale(0.94); }
}

/* Gated: with reduced motion the popup simply sits there, fully visible, since
   the base rule already has it at rest. */
@media (prefers-reduced-motion: no-preference) {
  .ql-popup { animation: ql-pop var(--ql-loop) infinite; }
}

/* Prairie dog popping out of its burrow at the Quick Look screenshot's lower
   left. The PNG carries heavy transparent padding (T75/B53 of 500), so these
   offsets are the image box, not the silhouette — the visible animal sits
   higher and narrower inside it than the numbers suggest. */
/* VP9 WebM with alpha. The offsets changed when this stopped being the PNG:
   both files hold the same artwork (visible aspect 1.231 vs 1.229) but the
   video is cropped far tighter — bottom padding is 2.2% of its box against the
   PNG's 10.6%. Reusing the old numbers dropped the animal about 29px too low.
   These are re-solved so the visible silhouette lands exactly where it did. */
.feature__prairie {
  position: absolute;
  left: var(--prairie-left, -24.6%);
  bottom: var(--prairie-bottom, -13%);
  width: var(--prairie-width, 49.2%);
  height: auto;
  display: block; /* a <video> is inline by default; avoids baseline gap */
  pointer-events: none;
}

/* Roadrunner sprinting across the Roundup screenshot — wider than the shot
   itself, so it breaks out on the right and drops its claws below the frame. */
/* Now a VP9 WebM with alpha. Unlike the other swaps this is NOT the same
   framing: the png's bird had a visible aspect of 1.763, the video's is 1.574,
   so width and height cannot both be preserved. These values match the visible
   WIDTH and the ground line, which makes the bird about 12% taller than before
   — it grows upward, since the bottom is what's anchored.
   The large bottom shift (-14% to -36.7%) is padding, not repositioning: the
   video carries 125px of transparent space below the bird, 19.5% of its box. */
.feature__roadrunner {
  position: absolute;
  /* right: var(--roadrunner-right, -27.1%); */
  bottom: var(--roadrunner-bottom, -36.7%);
  width: var(--roadrunner-width, 60%);
  max-width: none;
  height: auto;
  display: block; /* a <video> is inline by default; avoids a baseline gap */
  pointer-events: none;
}

/* Runs right-to-left on a timer, looping every --rr-loop. Time-based rather
   than scroll-linked: it crosses on its own cadence regardless of whether the
   reader is scrolling, so a stationary visitor still sees the bird go past.

   --rr-from / --rr-to are the editable start and end positions. Percentages
   resolve against the ROADRUNNER'S own width (~705px at a 1600px viewport),
   not the screenshot's. px, vw and rem work here too.

   Both defaults sit fully outside the viewport, which is what makes the loop
   seamless — the jump back to the start happens while the bird is off-screen.
   At 1600px wide it enters around 30% through the cycle and has fully left by
   94%, so roughly two seconds of each six is an empty gap before it returns.
   Shrink the values and the reset becomes visible as a snap. */
@keyframes roadrunner-run {
  0% { transform: translate3d(var(--rr-from, 250%), 0, 0); }
  25% { transform: translate3d(var(--rr-to, -330%), 0, 0); }
  100% { transform: translate3d(var(--rr-to, -330%), 0, 0); }
}

@media (prefers-reduced-motion: no-preference) {
  /* Parked at the run's start until the section arrives. Without this the bird
     would sit at its authored position on the screenshot and visibly jump
     off-screen the moment the trigger fired. Removing .is-inview on exit
     returns it here, which is the reset. */
  .feature__roadrunner.is-motion {
    transform: translate3d(var(--rr-from, 250%), 0, 0);
    will-change: transform;
  }

  /* The crossing is gated on the same class the video playback uses, so the
     legs and the travel start together instead of drifting apart. */
  .feature__roadrunner.is-motion.is-inview {
    animation: roadrunner-run var(--rr-loop, 6s) linear 1;
  }
}

.shot {
  display: block;
  width: 100%;
  /* Required, not cosmetic: an <img> carrying width/height attributes gets a
     presentational height hint, and with only width:100% set here that hint
     wins — the shot renders at its literal pixel height and stretches. */
  height: auto;
  border-radius: 12px;
  border: 1px solid var(--fr-slate-blue);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
}

/* ---------- Filter chips ---------- */

.chips { display: flex; gap: 8px; flex-wrap: wrap; }

/* Styled to match the filter chips in the app UI: solid accent blue, dark
   label, dismiss cross on the right. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  background: var(--fr-ui-blue);
  border: none;
  color: var(--fr-night);
  font-size: 14px;
  font-weight: 600;
  padding: 6px 12px 6px 15px;
  border-radius: 999px;
}

/* The cross is a background image, not text content. Generated text lands in
   the accessibility tree, and these chips are a static illustration of the
   app — a screen reader announcing "multiplication sign" after each filter
   would be noise on something nobody can actually dismiss. */
.chip::after {
  content: '';
  width: 11px;
  height: 11px;
  flex-shrink: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 3 9 9 M9 3 3 9' stroke='%23071629' stroke-width='1.7' stroke-linecap='round' fill='none'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* ---------- Keyboard keys ---------- */

.keys { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.key {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 600;
  color: var(--fr-parchment);
  background: var(--fr-night);
  border: 1px solid var(--fr-slate-blue);
  border-bottom-width: 3px;
  border-radius: 8px;
  padding: 8px 13px;
}
.key--wide { padding: 8px 26px; }

/* The Space key lights gold on the popup's timeline, so the keypress and the
   preview opening read as cause and effect rather than two loops that happen to
   share a duration. Percentages mirror @keyframes ql-pop exactly, and both pull
   their duration from --ql-loop, so they cannot drift.
   Only the wide key animates — the arrows are for browsing, not opening. */
@keyframes key-press {
  0% {
    background-color: var(--fr-night);
    color: var(--fr-parchment);
    border-color: var(--fr-slate-blue);
    box-shadow: none;
    animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
  }
  4% {
    background-color: var(--fr-sunset-gold);
    color: var(--fr-night);
    border-color: var(--fr-ranger-amber);
    box-shadow: 0 0 18px rgba(244, 180, 77, 0.45);
    animation-timing-function: linear;
  }
  10% {
    background-color: var(--fr-sunset-gold);
    color: var(--fr-night);
    border-color: var(--fr-ranger-amber);
    box-shadow: 0 0 18px rgba(244, 180, 77, 0.45);
    animation-timing-function: cubic-bezier(0.4, 0, 1, 1);
  }
  14%, 100% {
    background-color: var(--fr-night);
    color: var(--fr-parchment);
    border-color: var(--fr-slate-blue);
    box-shadow: none;
  }
}

@media (prefers-reduced-motion: no-preference) {
  .key--wide { animation: key-press var(--ql-loop) infinite; }
}
.keys__note { font-size: 13px; color: var(--fr-steel); margin-left: 4px; }

/* ---------- Formats & color tags ---------- */

.formats-wrap { display: flex; flex-direction: column; gap: 48px; }
.formats__intro {
  max-width: 720px;
  margin-inline: auto; /* centred header */
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.formats__lede { font-size: 17px; line-height: 1.6; color: var(--fr-steel); margin: 0; text-wrap: pretty; }

/* margin-top buys the room the icons overhang into, same as .steps. */
/* Five across on desktop, resolved intrinsically: 12rem floors + 20px gaps fit
   exactly five in the 1176px shell and cannot fit six, so the desktop grid is
   unchanged while narrower viewports step down to 4 / 3 / 2 / 1 on their own. */
.formats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
  gap: 20px;
  margin-top: var(--format-icon-clearance, 36px);
  user-select: none; /* prevents a text selection highlight from appearing on the card when the user clicks it */
}

/* Icons straddle each card's top edge, centred and sized against the card.
   Positioned rather than in flow so they don't push the text down. */
.format__icon {
  /* Same alias as .step__icon — see the shared card-hover block. */
  --lift: var(--format-icon-lift, -72%);
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--format-icon, 45%);
  height: auto;
  transform: translate(-50%, var(--lift));
  pointer-events: none;
  filter:
    drop-shadow(0 4px 7px rgba(0, 0, 0, 0.28))
    drop-shadow(0 14px 20px rgba(0, 0, 0, 0.4));
}

.format {
  position: relative; /* containing block for the overhanging icon */
  background: var(--fr-deep-blue);
  border: 1px solid var(--fr-slate-blue);
  border-radius: 12px;
  /* Extra top padding keeps the title clear of the icon's lower edge. */
  padding: 40px 20px 24px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* Centred to sit under the centred icon above it; the body copy stays left
   aligned, since centring multi-line text hurts readability. */
.format__title { font-weight: 700; font-size: 16px; color: var(--fr-sunset-gold); text-align: center; }
.format__body { margin: 0; font-size: 14px; line-height: 1.55; color: var(--fr-steel); }

.tags {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  background: var(--fr-deep-blue);
  border: 1px solid var(--fr-slate-blue);
  border-radius: 12px;
  padding: 20px 24px;
}
.tags__dots { display: flex; gap: 7px; flex-shrink: 0; }
.tags__text { margin: 0; font-size: 15px; line-height: 1.5; color: var(--fr-silver); }

.tag { width: 14px; height: 14px; border-radius: 50%; }
.tag--red    { background: var(--tag-red); }
.tag--orange { background: var(--tag-orange); }
.tag--yellow { background: var(--tag-yellow); }
.tag--green  { background: var(--tag-green); }
.tag--blue   { background: var(--tag-blue); }
.tag--purple { background: var(--tag-purple); }
.tag--gray   { background: var(--tag-gray); }

/* ---------- Privacy ---------- */

.privacy { text-align: center; display: flex; flex-direction: column; gap: 24px; align-items: center; }
.privacy__icon { border-radius: 16px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); }
.privacy__body {
  font-size: 18px;
  line-height: 1.6;
  color: var(--fr-steel);
  margin: 0;
  max-width: 60ch;
  text-wrap: pretty;
}

.pills { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; margin-top: 8px; }
.pill {
  background: var(--fr-deep-blue);
  border: 1px solid var(--fr-slate-blue);
  border-radius: 999px;
  padding: 8px 18px;
  font-size: 14px;
  font-weight: 600;
  color: var(--fr-parchment);
}

/* ---------- Toolbox grid ---------- */

.toolbox-wrap { display: flex; flex-direction: column; gap: 40px; }
/* Chest on the left, cards on the right. minmax(0, …) on both tracks stops the
   cards' own content from forcing the grid wider than its share — fr tracks
   have an automatic min-content floor otherwise. */
/* Chest beside the cards on desktop, above them below 48rem. The chest is
   decorative (alt=""), so it stacks first rather than being dropped — it sets
   the section's tone and costs one screen-width of height once capped. */
.toolbox-layout {
  display: grid;
  gap: var(--toolbox-gap, 16px);
  align-items: center;
  justify-items: center;
}
/* Capped only while stacked — above 48rem the 20fr track governs it, and a
   max-width here would shrink the chest on desktop too. */
@media (max-width: 47.99rem) {
  .toolbox__icon { max-width: 11rem; }
}
@media (min-width: 48rem) {
  .toolbox-layout {
    grid-template-columns: minmax(0, var(--toolbox-icon-col, 20fr)) minmax(0, 80fr);
    justify-items: stretch;
  }
}
.toolbox__icon {
  width: 100%;
  height: auto;
  filter:
    drop-shadow(0 6px 10px rgba(0, 0, 0, 0.3))
    drop-shadow(0 20px 30px rgba(0, 0, 0, 0.42));
}

/* Four across beside the chest on desktop; intrinsic, so it steps down to
   2 and then 1 as the column narrows without a breakpoint of its own. */
.toolbox {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
  gap: 10px;
}
.tool {
  background: var(--fr-deep-blue);
  border: 1px solid var(--fr-slate-blue);
  border-radius: 12px;
  padding: 22px 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: background-color var(--tool-darken-dur, 200ms) ease;
}
/* Hover fill: 20% black mixed into the card color. Deliberately outside the
   prefers-reduced-motion gate that holds the press — a color change isn't
   motion, and a visitor who asked for less movement should still get the
   hover feedback. Text sits above the background, so only the box darkens;
   body copy still measures 6.6:1 on the mixed fill. */
.tool:hover {
  background-color: color-mix(in srgb, #000 var(--tool-darken, 20%), var(--fr-deep-blue));
}
.tool__title { font-weight: 700; font-size: 15px; color: var(--fr-sunset-gold); }
.tool__body { margin: 0; font-size: 14px; line-height: 1.5; color: var(--fr-steel); }

/* ---------- Comparison ---------- */

.cmp-wrap { display: flex; flex-direction: column; gap: 40px; }
.cmp { border: 1px solid var(--fr-slate-blue); border-radius: 12px; overflow: hidden; }

.cmp__head,
.cmp__row { display: grid; grid-template-columns: 2.4fr 1fr 1fr 1fr; }

/* A four-column comparison cannot be read at 320px, and none of its cells are
   droppable — every row is a claim about three products. Of the options in
   play (hide columns, stack into cards, scroll) only scrolling keeps all
   twelve header/row relationships intact, so the table gets its own scroll
   container below 60rem.

   The container is the table's, not the page's: the surrounding section still
   has no horizontal scrollbar. tabindex + role + a label make the region
   reachable and announced for keyboard and screen-reader users, which a bare
   overflow container is not. */
.cmp-scroll { overflow-x: auto; overscroll-behavior-x: contain; }
.cmp-scroll:focus-visible { outline: 2px solid var(--fr-sunset-gold); outline-offset: 3px; }

@media (max-width: 59.99rem) {
  .cmp { min-width: 34rem; }
  /* Capability column carries the longest strings, so it keeps the slack. */
  .cmp__head,
  .cmp__row { grid-template-columns: 2fr 1fr 1fr 1fr; }
  .cmp__head-cap, .cmp__cap { padding-inline: 14px; }
  .cmp__head-fr, .cmp__head-cell { padding-inline: 8px; }
  .cmp__fr, .cmp__cell { padding-inline: 8px; }

  /* Fades at both ends signal that there is more table off-screen. Pinned to
     the scroll container's own edges, and inert to pointer input so they
     never intercept the swipe they are advertising. */
  .cmp-stage { position: relative; }
  .cmp-stage::after {
    content: '';
    position: absolute;
    inset-block: 0;
    right: 0;
    width: 2.5rem;
    pointer-events: none;
    background: linear-gradient(90deg, rgba(14, 42, 73, 0), rgba(14, 42, 73, 0.9));
    border-radius: 0 12px 12px 0;
  }
}

.cmp__head {
  background: var(--fr-night);
  border-bottom: 1px solid var(--fr-slate-blue);
  font-size: 14px;
  font-weight: 700;
}
.cmp__head-cap  { padding: 14px 20px; color: var(--fr-steel); }
.cmp__head-fr   { padding: 14px 12px; color: var(--fr-sunset-gold); text-align: center; }
.cmp__head-cell { padding: 14px 12px; color: var(--fr-steel); text-align: center; }

.cmp__row { border-bottom: 1px solid var(--fr-row-line); font-size: 14px; background: var(--fr-row); }
.cmp__cap  { padding: 12px 20px; color: var(--fr-silver); }
.cmp__fr   { padding: 12px; color: var(--fr-parchment); font-weight: 600; text-align: center; }
.cmp__cell { padding: 12px; color: var(--fr-steel); text-align: center; }

/* Section decorations. .cmp-stage wraps the table and matches its box exactly,
   so both props anchor to the table's own corners rather than the section —
   .cmp itself can't host them because it clips (overflow: hidden) for its
   rounded corners. Both deliberately overflow the stage: the vulture crosses
   the section boundary above, the pile spills past the table below. */
.cmp-stage { position: relative; }

.cmp__perch,
.cmp__pile {
  position: absolute;
  height: auto;
  pointer-events: none;
  user-select: none;
}

/* Perched on the table's top-right corner. bottom: 98% drops the talons 2% of
   the table's height below its top edge, so they grip the header bar. */
/* Now a VP9 WebM with alpha. These three numbers look like a resize but aren't:
   the png was tight-cropped, while the video's 400x400 box carries L95 T40 R83
   B17 of padding, so the bird fills only ~55% of its box width. The box has to
   grow and shift to put the same-sized vulture in the same place — visible
   height lands at 293.6px against the png's 295.6px.
   If the webm is ever re-exported tight, these revert to roughly 19% / 5% / 93%. */
.cmp__perch {
  right: var(--perch-right, -2.1%);
  bottom: var(--perch-bottom, 89.75%);
  width: var(--perch-width, 34.2%);
  display: block; /* a <video> is inline by default; avoids a baseline gap */
}

/* Heaped against the bottom-right corner, spilling below the table. */
.cmp__pile { right: 2%; bottom: -20%; width: 30%; }

/* ---------- Brand / creator ---------- */

.brand { display: flex; flex-direction: column; gap: 40px; align-items: center; text-align: center; }
.brand__head { max-width: 720px; display: flex; flex-direction: column; gap: 16px; }
.brand__body { font-size: 17px; line-height: 1.6; color: var(--fr-steel); margin: 0; text-wrap: pretty; }
/* Full-bleed closing band with the copy sitting on it. The banner is a
   background rather than an <img> so the text can flow normally on top instead
   of being absolutely positioned over it — the band can never be shorter than
   its own copy that way. min-height tracks the banner's natural 3:1 height so
   `cover` barely has to crop at common widths, capped so it doesn't become a
   giant band on ultra-wide displays. */
.section--brand {
  position: relative;
  min-height: clamp(360px, 33vw, 560px);
  display: flex;
  align-items: center;
  /* image-set is how a CSS background gets the srcset treatment: the UA picks
     the tier, so a phone never downloads the 2172px plate for a 390px band.
     The plain url() stays as the last entry for anything that cannot parse
     image-set at all. */
  background: var(--fr-slate-blue) center / cover no-repeat;
  background-image: image-set(
    url('assets/images/banner-900.webp')  type('image/webp') 1x,
    url('assets/images/banner-1440.webp') type('image/webp') 1.6x,
    url('assets/images/banner-2172.webp') type('image/webp') 2.4x,
    url('assets/images/banner.jpg')       type('image/jpeg') 1x);
}

/* Legibility scrim. The banner is a bright sunset and the copy is parchment on
   steel — measured, the heading lands near 2:1 against bare artwork. Darkest
   behind the centred text and easing off toward the edges so the canyon walls
   and the sun keep their color. */
.section--brand::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    /* 1. Bottom fade into the section below. Listed first so it paints over the
       radial too — the radial still carries ~0.36 alpha at the band's lower
       edge, so a fade underneath it would come out slate-tinted and leave a
       seam. Lands on --fr-deep-blue #0E2A49, which is what .cta's radial
       resolves to at its top edge. */
    linear-gradient(180deg,
      rgba(14, 42, 73, 0) 62%,
      rgba(14, 42, 73, 0.70) 86%,
      rgba(14, 42, 73, 1) 100%),
    /* 2. Legibility pool behind the copy. */
    radial-gradient(35% 50% at 50% 50%,
      rgba(34, 66, 104, 0.97) 0%,
      rgba(34, 66, 104, 0.90) 25%,
      rgba(34, 66, 104, 0.42) 78%,
      rgba(34, 66, 104, 0) 100%);
    /* 3. Overall slate tint. */
    /* linear-gradient(180deg,
      rgba(34, 66, 104, 0.34) 0%,
      rgba(34, 66, 104, 0.10) 45%,
      rgba(34, 66, 104, 0.38) 100%); */
  pointer-events: none;
}

.section--brand .brand {
  position: relative;
  z-index: 1; /* above the scrim */
  width: 100%;
}

/* Steel is tuned for flat dark panels; over artwork it measured 4.25:1, just
   under AA. Lifting to silver clears it at 7.3:1 without darkening the scrim
   any further, so the banner keeps its color. */
.section--brand .brand__body { color: var(--fr-silver); }

/* ---------- Final CTA ---------- */

/* No border-top: the brand band above now fades to this section's own color,
   so a divider line would be the only thing marking a join that's meant to be
   invisible. It read as a 1px hairline against the fade. */
.cta {
  position: relative;
  background: radial-gradient(800px 400px at 50% 120%, var(--fr-cta-glow) 0%, var(--fr-deep-blue) 60%);
  text-shadow: black 0 0 4px, black 0 0 20px;
}

/* Brand art behind the closing pitch. Purely decorative, so it lives in CSS
   rather than the DOM — no alt text to caption, nothing for a screen reader to
   announce. A pseudo-element rather than an extra background layer on .cta,
   because a background-image can't carry its own opacity, and the art has to
   sit well below the text it backs. --art-alpha / --art-width tune it. */
.cta::before {
  content: '';
  position: absolute;
  inset: 0;
  background: no-repeat center / min(var(--art-width, 1500px), 100%) auto;
  background-image: image-set(
    url('assets/images/footer2-900.webp')  type('image/webp') 1x,
    url('assets/images/footer2-1500.webp') type('image/webp') 1.7x,
    url('assets/images/footer2.png')       type('image/png')  1x);
  left: -18%;
  pointer-events: none;
}

/* The artwork is a wide landscape composition sitting behind a centred text
   column. Below 64rem it is scaled to the viewport and pushed to the lower
   half, so the ranger reads as a horizon behind the CTA rather than a crop of
   a horse behind the headline. */
@media (max-width: 63.99rem) {
  .cta::before {
    --art-width: 46rem;
    left: 0;
    background-position: center bottom;
  }
}

/* Scrim that keeps the copy readable over the art: soft, centred on the text
   column, sitting above the artwork but below the text. Measured — without it
   the sub-line falls to 2.98:1 against the brightest part of the horse, under
   the 4.5:1 AA floor it clears everywhere else on the page. Fades to nothing
   at the edges so the ranger and the roped document stay bright. */
/* .cta::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background: radial-gradient(58% 62% at 50% 48%,
              rgba(14, 42, 73, 1) 0%,
              rgba(14, 42, 73, 0.78) 50%,
              rgba(14, 42, 73, 0) 75%,
              rgba(14, 42, 73, 0) 100%);
  pointer-events: none;
} */

.cta__inner {
  position: relative;
  z-index: 1; /* above both ::before art and ::after scrim */
  max-width: 760px;
  margin: 0 auto;
  padding: clamp(4rem, 2rem + 8vw, 8.125rem) var(--gutter-r) clamp(4rem, 2rem + 8vw, 8.125rem) var(--gutter-l);
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 22px;
  align-items: center;
}
.cta__lede {
  font-size: 18px;
  line-height: 1.6;
  color: var(--fr-steel);
  margin: 0;
  max-width: 52ch;
  text-wrap: pretty;
}
.cta__meta { font-size: 13px; color: var(--fr-steel); }
/* This line stopped being a throwaway note when it started carrying the
   price and refund disclosure. --fr-muted measures 2.74:1 on the CTA
   backdrop; --fr-steel measures 6.05:1. */
.cta__note { font-size: 12px; color: var(--fr-steel); }

/* ---------- Footer ---------- */

.footer { background: var(--fr-abyss); border-top: 1px solid var(--fr-border); }
.footer__inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: clamp(2.5rem, 1.5rem + 3vw, 3.5rem) var(--gutter-r)
           max(clamp(2.5rem, 1.5rem + 3vw, 3.5rem), env(safe-area-inset-bottom)) var(--gutter-l);
  display: flex;
  justify-content: space-between;
  gap: clamp(1.75rem, 1rem + 2.5vw, 2rem) 2rem;
  flex-wrap: wrap;
  align-items: flex-start;
}
.footer__brand { display: flex; flex-direction: column; gap: 10px; }
.footer__lockup { display: flex; align-items: center; gap: 10px; }
.footer__logo { border-radius: 6px; }
.footer__name { font-family: var(--font-slab); font-weight: 700; font-size: 17px; color: var(--fr-parchment); }
.footer__tagline { font-size: 14px; color: var(--fr-steel); }
.footer__copy { font-size: 13px; color: var(--fr-muted); }

/* CTRL Colab studio mark. alt="" because the column already names the studio
   twice in text — announcing it a third time would just be noise.
   The mark is pure #fff, which would otherwise be the brightest thing in the
   footer and outrank the FileRanger wordmark above it; --studio-alpha softens
   it into the hierarchy. Set it to 1 if the brand needs it unmodified. */
.footer__studio {
  display: block;
  width: var(--studio-logo, 64px);
  height: auto;
  margin-top: 6px;
  opacity: var(--studio-alpha, 0.85);
}

.footer__cols { display: flex; gap: clamp(1.75rem, 0.5rem + 4vw, 3.5rem); flex-wrap: wrap; }
.footer__col { display: flex; flex-direction: column; gap: 10px; font-size: 14px; }
.footer__heading {
  font-weight: 700;
  color: var(--fr-steel);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.footer__link { color: var(--fr-silver); }
.footer__link:hover { color: var(--fr-sunset-gold); }
.footer__soon { color: var(--fr-muted); }

/* ---------- Reduced-motion stills ----------
   Every animated character has a still PNG twin in the DOM; CSS picks which one
   renders. No JavaScript, and it follows the OS setting live — including a
   change made mid-session.

   The stills need their OWN offsets. The webm and png crops carry different
   amounts of transparent padding, which is exactly what each video swap had to
   re-solve; reusing the video's numbers would misplace the png by as much as
   20% of its box. These restore each png's original verified values.

   Selectors are element-qualified (img./video.) so they outrank the component
   rules, which set display themselves — no !important needed. */
img.is-still { display: none; }

@media (prefers-reduced-motion: reduce) {
  video.is-motion { display: none; }
  img.is-still    { display: block; }

  /* png-era offsets, restored per character */
  .feature__prairie.is-still {
    --prairie-left: -24%;
    --prairie-bottom: -18%;
    --prairie-width: 48%;
  }
  .feature__roadrunner.is-still {
    --roadrunner-right: -25%;
    --roadrunner-bottom: -14%;
    --roadrunner-width: 100%;
  }
  .feature__tortoise.is-still {
    --tortoise-right: -12%;
    --tortoise-width: 30%;
    --tortoise-pad-trim: 0%; /* png sits on the shared ground line unaided */
  }
  .cmp__perch.is-still {
    --perch-right: 5%;
    --perch-bottom: 93%;
    --perch-width: 19%;
  }
  /* cactus omitted deliberately: its png and webm crops match closely enough
     (visible aspect 0.6437 vs 0.6408) to share the same offsets. */
}

/* ---------- Indexing image sequence ----------
   Four steps in a loop. Frames stack rather than crossfade: each switches on
   and stays, with later frames occluding earlier ones, so opacity only ever
   goes 0 -> 1 and only the "switch on" mark in each block matters. There are
   only three stills, so frame 3 backs both step 3 and step 4 — step 4 is that
   same frame with the drive-scan overlay on top.

   ADJUSTING STEP TIMES
   CSS keyframe selectors must be literal percentages — they can't read a custom
   property — so per-step seconds can't live in variables. Each step is a window
   given as a percentage of --seq-total. To retime:
     1. change --seq-total to scale the whole loop, and/or
     2. move a switch-on mark, computed as cumulative_seconds / total x 100

   Current: --seq-total 17.5s
     step 1   0.00s -  4.29s   (0%    - 24.5%)  no locations added
     step 2   4.29s -  6.83s   (24.5% - 39%)    drive being added
     step 3   6.83s -  9.28s   (39%   - 53%)    drive added, idle
     step 4   9.28s - 17.50s   (53%   - 100%)   same frame + scan overlay
   Steps 2 and 3 run 2.5s each — deliberately brisk, so the loop moves through
   setup and dwells on the scan. Step 4 is 8.2s, long enough for the ~5s
   drive.webm to loop once and start again rather than cutting mid-cycle.
   Each window opens 0.5% after the previous one's mark; that gap is the fade. */

.app-seq {
  position: relative;
  aspect-ratio: 1276 / 850;
  border-radius: 12px;
  border: 1px solid var(--fr-slate-blue);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
  overflow: hidden;
}

.app-seq__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}

/* Drive scan, centred. Only visible during step 3 — it shares that step's
   keyframes, so retiming the step moves the overlay with it automatically. */
.app-seq__drive {
  position: absolute;
  left: var(--drive-x, 50%);
  top: var(--drive-y, 50%);
  width: var(--drive-width, 27%);
  height: auto;
  transform: translate(-50%, -50%);
  display: block;
  pointer-events: none;
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  .app-seq__frame--1 { animation: seq-1 var(--seq-total, 17.5s) linear infinite; }
  .app-seq__frame--2 { animation: seq-2 var(--seq-total, 17.5s) linear infinite; }
  .app-seq__frame--3 { animation: seq-3 var(--seq-total, 17.5s) linear infinite; }
  .app-seq__drive    { animation: seq-4 var(--seq-total, 17.5s) linear infinite; }
}

/* step 1 — also fades back in over the wrap, so the loop has no hard cut */
@keyframes seq-1 {
  0%, 30.5%   { opacity: 1; }
  31%, 99.5%  { opacity: 1; }
  100%      { opacity: 0; }
}
/* step 2 */
@keyframes seq-2 {
  0%, 12%     { opacity: 0; }
  12.5%, 59.5%  { opacity: 1; }
  60%, 100% { opacity: 1; }
}
/* step 3 (+ the drive overlay) */
/* frame 3 — held across BOTH step 3 and step 4; there are only three stills, so
   step 4 is this same frame with the scan overlay on top of it. */
@keyframes seq-3 {
  0%, 38.5%   { opacity: 0; }
  39%, 99.5%  { opacity: 1; }
  100%      { opacity: 0; }
}
/* step 4 — the drive-scan overlay on its own window, so its length is
   adjustable independently of step 3's. */
@keyframes seq-4 {
  0%, 52.5%   { opacity: 0; }
  53%, 99.5%  { opacity: 1; }
  100%      { opacity: 0; }
}

/* Reduced motion: no cycling. Rest on the end state — a drive indexed and
   ready — which is the frame that actually makes the section's point. The
   scan overlay is hidden rather than frozen, since a paused progress
   animation reads as broken. */
@media (prefers-reduced-motion: reduce) {
  .app-seq__frame--3 { opacity: 1; }
  .app-seq__drive    { display: none; }
}

/* ==========================================================================
   LEGAL PAGES — /privacy, /terms, /refund, /eula
   A single shared skin for all four documents. Long-form reading, so the
   rules here are about measure and rhythm rather than art direction: a
   narrower column than the marketing page, looser leading, and section
   numbers generated by a counter so renumbering never desyncs the prose
   from the table of contents.
   ========================================================================== */

.legal { background: var(--fr-night); }
/* The marketing page's 120px top gutter is tuned for full-bleed sections
   arriving one after another. Here the masthead has already done that job,
   so the body starts closer. */
.legal .container { padding-top: 76px; }

/* ---------- Masthead ---------- */

.legal-hero {
  background: linear-gradient(180deg, var(--fr-deep-blue) 0%, var(--fr-night) 100%);
  border-bottom: 1px solid var(--fr-border);
}
.legal-hero__inner {
  max-width: 820px;
  margin: 0 auto;
  padding: clamp(5.25rem, 4.25rem + 5vw, 8.25rem) var(--gutter-r) clamp(2.5rem, 1.5rem + 3vw, 3.5rem) var(--gutter-l);
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.legal-hero__title {
  font-family: var(--font-slab);
  font-weight: 800;
  font-size: clamp(34px, 4.4vw, 52px);
  line-height: 1.05;
  margin: 0;
  color: var(--fr-parchment);
}
.legal-hero__lede {
  font-size: 18px;
  line-height: 1.6;
  color: var(--fr-silver);
  margin: 0;
  text-wrap: pretty;
}
.legal-hero__meta {
  font-size: 13px;
  color: var(--fr-steel);
  letter-spacing: 0.02em;
  padding-top: 4px;
  border-top: 1px solid var(--fr-border);
  margin-top: 6px;
}

/* Pre-publish only. Counsel has to review these documents before they go
   live (see the handoff's §8 checklist), so the draft state is stated on
   the page itself rather than tracked somewhere a reader can't see.
   Delete the four .legal-draft blocks at publish time. */
.legal-draft {
  max-width: 820px;
  margin: 28px auto 0;
  padding: 16px 20px;
  margin-inline: max(var(--gutter-l), calc(50% - 410px)) max(var(--gutter-r), calc(50% - 410px));
  border: 1px dashed var(--fr-sunset-gold);
  border-radius: 10px;
  background: rgba(244, 180, 77, 0.07);
  font-size: 14px;
  line-height: 1.6;
  color: var(--fr-parchment);
}
.legal-draft strong { color: var(--fr-sunset-gold); }

/* ---------- Contents ---------- */

.legal__toc {
  margin: 0 0 64px;
  padding: 24px 28px;
  border: 1px solid var(--fr-border);
  border-radius: 12px;
  background: var(--fr-row);
}
.legal__toc-head {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fr-sunset-gold);
  margin-bottom: 14px;
}
.legal__toc ol {
  margin: 0;
  padding-left: 22px;
  columns: 2;
  column-gap: 40px;
  font-size: 15px;
  line-height: 1.9;
  color: var(--fr-steel);
}
.legal__toc li { break-inside: avoid; }
.legal__toc a { color: var(--fr-silver); }
.legal__toc a:hover { color: var(--fr-sunset-gold); }

/* ---------- Document body ---------- */

.legal__body {
  counter-reset: legal-sec;
  font-size: 16.5px;
  line-height: 1.72;
  color: var(--fr-silver);
}
.legal__body h2 {
  counter-increment: legal-sec;
  font-family: var(--font-slab);
  font-weight: 700;
  font-size: clamp(22px, 2.4vw, 28px);
  line-height: 1.25;
  color: var(--fr-parchment);
  margin: 64px 0 18px;
  padding-top: 26px;
  border-top: 1px solid var(--fr-border);
}
.legal__body h2::before {
  content: counter(legal-sec) ".";
  color: var(--fr-ranger-amber);
  margin-right: 12px;
  font-weight: 800;
}
.legal__body h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.legal__body h3 {
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 17px;
  color: var(--fr-sunset-gold);
  margin: 32px 0 10px;
}
.legal__body p { margin: 0 0 18px; text-wrap: pretty; }
.legal__body strong { color: var(--fr-parchment); font-weight: 600; }
.legal__body em { color: var(--fr-silver); }

.legal__body ul,
.legal__body ol { margin: 0 0 18px; padding-left: 24px; }
.legal__body li { margin-bottom: 9px; }
.legal__body li::marker { color: var(--fr-ranger-amber); }

/* Underline links inside prose only. Scoped to the elements that hold running
   text rather than applied to every descendant anchor, because the related-
   document cards at the foot of each page are also anchors and a card rendered
   as three underlined lines reads as broken. */
.legal__body p a,
.legal__body li a,
.legal__body td a { text-decoration: underline; text-underline-offset: 3px; }

/* The one-sentence answer that opens a section — the house pattern is
   "plain answer first, detail after", so it gets its own weight. */
.legal__lead {
  color: var(--fr-parchment);
  font-size: 18px;
  line-height: 1.6;
}

/* ---------- Tables ---------- */

.legal__table-wrap { overflow-x: auto; margin: 0 0 22px; }
.legal__table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14.5px;
  border: 1px solid var(--fr-slate-blue);
  border-radius: 12px;
  overflow: hidden;
}
.legal__table th {
  text-align: left;
  padding: 12px 16px;
  background: var(--fr-deep-blue);
  color: var(--fr-sunset-gold);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--fr-slate-blue);
}
.legal__table td {
  padding: 12px 16px;
  border-bottom: 1px solid var(--fr-row-line);
  background: var(--fr-row);
  color: var(--fr-steel);
  vertical-align: top;
  line-height: 1.55;
}
.legal__table tr:last-child td { border-bottom: 0; }
.legal__table td:first-child { color: var(--fr-silver); font-weight: 600; }

/* ---------- Conspicuous blocks ---------- */

/* Warranty disclaimers and liability caps are conventionally set apart so a
   court can see they were conspicuous. That convention is the reason for the
   uppercase here — it is not a stylistic choice, and it is the only place on
   the site where uppercase body copy appears. */
.legal__conspicuous {
  border: 1px solid var(--fr-slate-blue);
  border-left: 3px solid var(--fr-ranger-amber);
  border-radius: 10px;
  background: var(--fr-row);
  padding: 20px 24px;
  margin: 0 0 20px;
  font-size: 14px;
  line-height: 1.7;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--fr-silver);
}
.legal__conspicuous p:last-child { margin-bottom: 0; }

/* A plain aside — quoted in-app copy, cross-references, worked examples. */
.legal__aside {
  border-left: 3px solid var(--fr-slate-blue);
  padding: 4px 0 4px 20px;
  margin: 0 0 22px;
  color: var(--fr-steel);
  font-size: 15.5px;
}
.legal__aside p:last-child { margin-bottom: 0; }

/* ---------- Unresolved placeholders ---------- */

/* Every fact the handoff could not verify is marked rather than guessed.
   Styled loudly on purpose: a [TK] that ships is a factual error in a legal
   document, so it should be impossible to miss in review. */
.tk {
  display: inline;
  padding: 1px 7px;
  border: 1px dashed var(--fr-sunset-gold);
  border-radius: 4px;
  background: rgba(244, 180, 77, 0.1);
  color: var(--fr-sunset-gold);
  font-family: var(--font-mono);
  font-size: 0.86em;
  font-style: normal;
}

/* ---------- Cross-links to the sibling documents ---------- */

.legal__related {
  margin-top: 80px;
  padding-top: 32px;
  border-top: 1px solid var(--fr-border);
}
.legal__related-head {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fr-steel);
  margin-bottom: 18px;
}
.legal__related-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
}
.legal__related-card {
  display: block;
  padding: 16px 18px;
  border: 1px solid var(--fr-border);
  border-radius: 10px;
  background: var(--fr-row);
  color: var(--fr-silver);
  transition: border-color 160ms ease, background 160ms ease;
  /* These are cards, not inline citations — the body rule underlines every
     link, which turns the whole card into a wall of rules. */
  text-decoration: none;
}
.legal__related-card:hover { border-color: var(--fr-slate-blue); background: var(--fr-deep-blue); color: var(--fr-silver); }
.legal__related-title { font-weight: 700; font-size: 15px; color: var(--fr-sunset-gold); margin-bottom: 4px; }
.legal__related-body { font-size: 13.5px; line-height: 1.5; color: var(--fr-steel); }

@media (max-width: 47.99rem) {
  .legal__toc    { padding: 20px; }
  .legal__toc ol { columns: 1; }
  .legal__body   { font-size: 1rem; }
  /* Section numbers on their own line rather than hanging beside a heading
     that now wraps to three lines. */
  .legal__body h2 { margin-top: 44px; }
  .legal__conspicuous { padding: 16px 18px; }
  .legal__related { margin-top: 56px; }
}

/* ==========================================================================
   RESPONSIVE LAYER
   Everything above is written to be fluid on its own — clamp()ed type and
   spacing, intrinsic grids, and per-component breakpoints stated next to the
   component they govern. What is left here is the small set of rules that are
   genuinely cross-cutting: input affordances, and the recomposition the
   decorative artwork needs once the layout stacks.

   The ladder, for reference. Nothing is added just because a device category
   exists; a breakpoint appears only where the content demanded one.

     base            mobile-first
     30rem  / 480px  large mobile
     48rem  / 768px  tablet portrait  — feature/toolbox composition
     64rem  / 1024px tablet landscape — DESKTOP NAVIGATION RETURNS, and the
                                        hero + feature splits return with it
     80rem  / 1280px desktop          — every clamp() has reached its cap
     90rem  / 1440px wide             — the two full-bleed bands stop growing
   ========================================================================== */

/* ---------- Focus ----------
   Nothing removes an outline anywhere in this stylesheet; this replaces the
   UA default with something that survives the site's dark palette. Gold on
   night measures 9.9:1, and the offset keeps it clear of the amber borders
   it sits next to. :focus-visible so a mouse click on a card doesn't ring it. */
:where(a, button, [tabindex]):focus-visible {
  outline: 3px solid var(--fr-sunset-gold);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ---------- Touch targets ----------
   Scoped to coarse pointers rather than to a width: a small window on a
   desktop is still driven by a mouse and does not need 44px rows, while a
   1024px tablet does. The visible ink is unchanged in every case — only the
   hit area grows, via padding or a min-height. */
@media (pointer: coarse) {
  .footer__link,
  .legal__toc a { min-height: 44px; display: flex; align-items: center; }
  .legal__related-card { min-height: 44px; }
  .link-arrow { padding: 15px 12px; }
}

/* ---------- Decorative characters ----------
   The cast is positioned in percentages of each screenshot, so it rescales
   correctly on its own — but percentage offsets that read as "peeking in from
   the edge" beside a 700px screenshot read as "half an animal" beside a 340px
   one. These pull each character back toward its screenshot and shrink the
   ones that would otherwise dominate a stacked column. Nobody is removed:
   they are the brand, and every one of them is still on the page at 320px. */
@media (max-width: 63.99rem) {
  /* Stacked: the media column is now full width, so the characters have the
     whole viewport behind them and can sit closer to the frame.

     The bottom offsets matter as much as the widths. Side by side, a
     character hanging below the screenshot hangs into empty column; stacked,
     the copy is directly underneath it — measured, the prairie dog landed on
     top of the "Preview" eyebrow and the tortoise on "Speed and control".
     These lift each one back onto its own screenshot's baseline. */
  .feature__cactus     { --cactus-right: -18%;  --cactus-width: 34%;  --cactus-bottom: -4%; }
  .feature__prairie    { --prairie-left: -14%;  --prairie-width: 34%; --prairie-bottom: -2%; }
  .feature__tortoise   { --tortoise-right: -6%; --tortoise-width: 22%; }
  .feature__rabbit.is-motion { --rabbit-left: -18%; --rabbit-width: 40%; }
  .feature__rabbit,
  .feature__tortoise   { --creature-bottom: -6%; }
  .feature__roadrunner { --roadrunner-width: 44%; --roadrunner-bottom: -22%; }
  .cmp__perch          { --perch-right: 1%;     --perch-width: 24%; }
  .cmp__pile           { right: 1%; width: 22%; }
  .problem__lens       { --lens-width: 18%; }

  /* Extra room between a screenshot and the copy that now sits under it,
     so the lifted characters still have somewhere to stand. */
  .feature { gap: clamp(2.5rem, 1.5rem + 3vw, 3.25rem); }
}

@media (max-width: 47.99rem) {
  .feature__cactus     { --cactus-right: -12%;  --cactus-width: 30%; --cactus-bottom: -2%; }
  .feature__prairie    { --prairie-left: -10%;  --prairie-width: 30%; --prairie-bottom: 0%; }
  .feature__tortoise   { --tortoise-right: -4%; --tortoise-width: 20%; }
  .feature__rabbit.is-motion { --rabbit-left: -12%; --rabbit-width: 36%; }
  .feature__rabbit,
  .feature__tortoise   { --creature-bottom: -3%; }
  .feature__roadrunner { --roadrunner-width: 38%; --roadrunner-bottom: -18%; }
  .cmp__perch          { --perch-right: 0%;     --perch-width: 22%; }
  .cmp__pile           { width: 20%; bottom: -12%; }

  /* The magnifier straddles the grid's bottom edge. Once the cards stack it
     has a single column under it instead of two, so it is pulled in tighter
     and lifted less — otherwise it hangs into the next section. */
  .problem__lens { --lens-width: 26%; --lens-lift: -26%; }
}

/* ---------- Section-level composition ---------- */

@media (max-width: 47.99rem) {
  /* Icons overhang each card's top edge by a percentage of the card's own
     width. A stacked card is much wider than a gridded one, so the same
     percentage would throw the icon far above the card — these hold the
     overhang to roughly the size it has on desktop. */
  .step__icon   { --step-icon: 22%;   --step-icon-lift: -46%; }
  .format__icon { --format-icon: 22%; --format-icon-lift: -58%; }
  .format       { padding-top: 30px; }

  .step  { padding: 26px 22px; }
  .steps { gap: 18px; }

  /* Bottom padding only: the CTA artwork is anchored low, and symmetric
     padding on a short screen leaves the horse cropped through the middle. */
  .cta__inner { gap: 18px; }

  .trust__inner { font-size: 13px; }
  .pills { gap: 8px; }
  .pill  { padding: 7px 14px; }
}

@media (max-width: 30rem) {
  /* Six words of "Download for Windows" plus the artwork's own bevel: the
     button is a fixed-ratio image, so it is capped rather than reflowed. */
  .btn-dl--md, .btn-dl--lg { --btn-dl-w: min(14.5rem, 100%); }
  .hero__actions { gap: 8px; }

  .cmp__pile, .problem__lens { display: none; } /* no room to read them */
}

/* ---------- Landscape phones ----------
   Wide enough to look like a tablet and roughly 380px tall. The rules that
   matter here are vertical: a full-height hero band and a 120px section
   rhythm together fill the screen with padding. Keyed on orientation AND a
   short viewport so a genuine tablet in landscape is untouched. */
@media (max-height: 30rem) and (orientation: landscape) {
  .hdr { min-height: 14rem; max-height: 78svh; }
  .container { padding-block: clamp(2.5rem, 6vh, 4rem); }
  .hero__inner { padding-top: 2.5rem; }
  .hero__copy  { padding-bottom: 2.5rem; }
  .nav__menu   { padding-block: 0.75rem; }
  .nav__link   { min-height: 42px; }
}

/* ---------- Wide desktop ----------
   The shell already caps at 1240px and every clamp() has topped out by
   1280px, so 90rem needs only to stop the two full-bleed bands from growing
   without limit — both are sized in vw and would otherwise keep climbing. */
@media (min-width: 90rem) {
  .section--brand { min-height: 560px; }
  .hero { --glow-w: 1100px; --glow-h: 600px; }
}

/* ---------- Motion ----------
   The site's decorative motion is already gated component by component. What
   is left is the scroll-linked parallax and the character clips, which have
   a cost as well as a preference attached: ten compositing layers driven by a
   scroll timeline is the most expensive thing on the page, and it is doing
   that work on the device least able to afford it.

   Below 48rem the plates are composed statically. The artwork is unchanged —
   only the per-plate drift stops, so the header still reads as the same
   illustration. */
@media (max-width: 47.99rem) {
  .hdr__layer { animation: none; will-change: auto; }
}

@media (prefers-reduced-motion: reduce) {
  /* Belt and braces over the per-component gates: anything that slipped
     through is reduced to a single frame rather than merely slowed. */
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
