/* ═══════════════════════════════════════════════════════════════════════
   DRIFT — SHARED DESIGN SYSTEM
   Per DRIFT-Kinetic-Immersive-Design-Spec.md, Part A.

   Everything in this file is used by EVERY page. Page-specific CSS stays
   in that page's own {% block css %} — do not add section styles here.

   Load order on every page:  site.css  →  page block
   ═══════════════════════════════════════════════════════════════════════ */

/* ── FONTS ────────────────────────────────────────────────────────────────
   Poppins, served locally. No external requests, so the site works offline
   and there is no third-party font call on an investor-facing page. */
@font-face{font-family:Poppins;font-weight:200;font-style:normal;font-display:swap;
  src:url(/assets/fonts/poppins-200.woff2) format("woff2")}
@font-face{font-family:Poppins;font-weight:300;font-style:normal;font-display:swap;
  src:url(/assets/fonts/poppins-300.woff2) format("woff2")}
@font-face{font-family:Poppins;font-weight:400;font-style:normal;font-display:swap;
  src:url(/assets/fonts/poppins-400.woff2) format("woff2")}
@font-face{font-family:Poppins;font-weight:500;font-style:normal;font-display:swap;
  src:url(/assets/fonts/poppins-500.woff2) format("woff2")}
@font-face{font-family:Poppins;font-weight:600;font-style:normal;font-display:swap;
  src:url(/assets/fonts/poppins-600.woff2) format("woff2")}

/* ── TOKENS ─────────────────────────────────────────────────────────────── */
:root{
  /* GROUND — v1.3 lifts the whole ground a further two stops and pushes it
     hard toward ocean blue. The v1.2 set was still a near-black navy: on a
     projector or a bright office screen it read as "dark tech site", not as
     open water, and every photograph on the page had to fight it. These are
     the colours of deep sea rather than of night.

     --abyss-rgb is the same colour as --abyss in channels, because a dozen
     scrims across the site are rgba(abyss, α) and CANNOT reference a hex
     token. Change one, change both — or the overlays go back to being the
     old near-black over a blue page, which shows up as grey haloes around
     every full-bleed image. */
  --abyss:#082740; --abyss-rgb:8,39,64;
  --hull:#0E3A5C; --deck:#17527E; --schematic:#0A2F4D;
  /* INK */
  --white:#fff;
  --mist:rgba(255,255,255,.78);
  --fog:rgba(255,255,255,.54);
  --ghost:rgba(255,255,255,.26);
  /* SIGNAL — lifted to match the brighter ground. A signal blue only reads
     as a signal if it is clearly above the surface it sits on, and the old
     --current was barely a stop off the new --deck.
     --ocean and --foam are new: the mid and top of the water column, for
     the gradients that used to be mixed by hand out of raw hex. */
  --current:#3E8CFF; --current-lift:#93C6FF; --wake:#CFE6FA;
  --sky:#B6DCFB;
  --ocean:#1A6DA6; --foam:#7FD4F5;
  --charge:#4EF0C4; --warn:#FFB454;
  /* LINES */
  --line:rgba(255,255,255,.18);
  --line-strong:rgba(255,255,255,.34);
  --line-signal:rgba(88,164,255,.55);
  /* TYPE — brand font, served locally.
     --mono resolves to the BRAND face. Everything that was set in a
     monospace for "deep-tech texture" (eyebrows, tags, labels, stat digits)
     now sets in Poppins, which is what makes the page read as DRIFT.
     --instrument is the only true monospace, reserved for the globe's own
     output — its corner tick, its legend and its coordinates — and for the
     machine-output notes beside it. Real machine output, small doses. */
  --sans:Poppins,-apple-system,BlinkMacSystemFont,"Helvetica Neue","Segoe UI",Roboto,Arial,sans-serif;
  --mono:Poppins,-apple-system,BlinkMacSystemFont,"Helvetica Neue",Arial,sans-serif;
  --instrument:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
  /* MOTION */
  --ease-out:cubic-bezier(.22,.61,.36,1);
  --ease-inout:cubic-bezier(.65,.05,.36,1);
  --ease-snap:cubic-bezier(.2,.9,.3,1.2);
  --t-fast:160ms; --t-base:420ms; --t-slow:900ms;
  /* RADIUS */
  --r-sm:8px; --r-md:16px; --r-lg:24px;
  /* TEXTURE */
  --grid-schematic:
    repeating-linear-gradient(0deg,rgba(126,196,255,.07) 0 1px,transparent 1px 48px),
    repeating-linear-gradient(90deg,rgba(126,196,255,.07) 0 1px,transparent 1px 48px);
}

/* ── BASE ─────────────────────────────────────────────────────────────── */
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{
  font-family:var(--sans);background:var(--abyss);color:var(--white);
  line-height:1.62;font-weight:300;-webkit-font-smoothing:antialiased;
  overflow-x:hidden;font-variant-numeric:tabular-nums;
}
img,video{max-width:100%;display:block}
a{color:inherit;text-decoration:none}
.wrap{width:min(1240px,90vw);margin-inline:auto}
::selection{background:var(--current);color:#fff}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}

/* ── TYPE SCALE ───────────────────────────────────────────────────────────
   Poppins is a geometric with a large x-height; the hairline weights that
   suited a grotesk go weak and wide here. drift.energy sets headings at 500
   and body at 300, so we follow the brand: 500 display, 300 body. Tracking
   is tightened harder than a grotesk needs, because Poppins sets loose.

   This is the CINEMATIC scale — the homepage and any page whose job is
   vision. Evidence pages step down; see .page-dense below. */
.d1{font-size:clamp(2.9rem,10.5vw,9.5rem);font-weight:500;line-height:.92;letter-spacing:-.045em}
.d2{font-size:clamp(2.2rem,5.8vw,4.9rem);font-weight:500;line-height:1.02;letter-spacing:-.035em}
.d3{font-size:clamp(1.7rem,3.8vw,3rem);font-weight:500;line-height:1.12;letter-spacing:-.03em}
.h4{font-size:clamp(1.2rem,2vw,1.7rem);font-weight:500;line-height:1.25;letter-spacing:-.02em}
.lede{font-size:clamp(1rem,1.6vw,1.28rem);font-weight:300;line-height:1.62}
.meta{font-size:.8125rem;line-height:1.5;font-weight:400}
em{font-style:normal;color:var(--current-lift)}
p{max-width:46ch}

/* THE DENSE SCALE — opt in with class="page-dense" on <body>.
   Spec Part C: evidence pages (Team, and the interior pages that follow)
   are "deliberately less cinematic than the homepage… denser,
   document-like". That is a real design decision, so it gets a real
   switch. Previously this scale existed only as divergent copy-pasted
   values in team.html, which made it indistinguishable from a mistake. */
.page-dense .d1{font-size:clamp(2.4rem,6.4vw,5.2rem);line-height:1.02;letter-spacing:-.04em}
.page-dense .d2{font-size:clamp(2rem,4.6vw,3.6rem);line-height:1.06}
.page-dense .d3{font-size:clamp(1.5rem,3vw,2.4rem);line-height:1.14}
.page-dense .h4{font-size:clamp(1.15rem,1.7vw,1.5rem)}
.page-dense .lede{font-size:clamp(1rem,1.5vw,1.2rem)}
.page-dense p{max-width:52ch}

/* ── EYEBROWS ─────────────────────────────────────────────────────────────
   Eyebrows use the brand face, not a mono — the mono read as generic
   deep-tech and pulled away from DRIFT. Mono is reserved strictly for
   live instrument data (the globe and its readings), where it earns its
   place.

   No leading rule (James, 27 Aug 2026): the label carries itself on the
   letter-spacing alone. The flex row stays — the gap is what separates the
   pulse dot from "Investment open" on the homepage hero. */
.eyebrow{
  font-family:var(--sans);font-size:.72rem;font-weight:600;letter-spacing:.26em;
  text-transform:uppercase;color:var(--current-lift);
  display:flex;align-items:center;gap:14px;margin-bottom:26px;
}
.eyebrow.charge{color:var(--charge)}
.eyebrow.center{justify-content:center}
.page-dense .eyebrow{margin-bottom:24px}

/* ── MOTION PRIMITIVES ────────────────────────────────────────────────────
   Reveal is OPT-IN via .js-reveal on <html>, set by the inline script in
   the base layout only when IntersectionObserver exists. Hiding by default
   meant any failure — JS error, unsupported IO, an element tagged after
   reveal.js had already snapshotted the DOM — left content invisible
   forever. Content defaults to VISIBLE; animation is the enhancement.
   Spec Part D: "Scroll-reveal must be fail-safe." Do not invert this. */
.js-reveal [data-reveal]{opacity:0;transform:translateY(34px);
  transition:opacity var(--t-slow) var(--ease-out),transform 1s var(--ease-out)}
.js-reveal [data-reveal].is-in{opacity:1;transform:none}

/* ── BUTTONS ──────────────────────────────────────────────────────────── */
.btn{
  display:inline-flex;align-items:center;gap:12px;padding:19px 40px;border-radius:999px;
  font-size:1.02rem;font-weight:500;letter-spacing:-.005em;border:1px solid transparent;
  cursor:pointer;position:relative;overflow:hidden;
  transition:transform var(--t-fast) var(--ease-snap),background var(--t-fast),
             box-shadow var(--t-fast),border-color var(--t-fast);
}
.btn .arw{transition:transform var(--t-fast) var(--ease-out)}
.btn:hover .arw{transform:translateX(5px)}
.btn:focus-visible{outline:2px solid var(--current-lift);outline-offset:3px}

.btn-primary{background:var(--current);color:#fff}
.btn-primary:hover{background:var(--current-lift);transform:translateY(-2px);
  box-shadow:0 10px 30px rgba(62,140,255,.42)}
.btn-primary:active{transform:translateY(0) scale(.985)}

.btn-secondary{border-color:var(--line-strong);color:#fff;background:transparent;z-index:0}
.btn-secondary::before{
  content:"";position:absolute;inset:0;background:var(--current);z-index:-1;
  transform:scaleX(0);transform-origin:left;transition:transform 240ms var(--ease-out);
}
.btn-secondary:hover{border-color:var(--current)}
.btn-secondary:hover::before{transform:scaleX(1)}

.tlink{display:inline-flex;align-items:center;gap:8px;position:relative;padding-bottom:3px;font-size:.95rem}
.tlink::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:var(--ghost)}
.tlink::before{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:var(--current-lift);
  transform:scaleX(0);transform-origin:left;transition:transform 220ms var(--ease-out);z-index:1}
.tlink:hover::before{transform:scaleX(1)}

/* ── CHIPS ────────────────────────────────────────────────────────────────
   Claim governance, spec D3: every forward-looking figure carries one of
   these. VALIDATED = third-party verified, MODELLED = DRIFT's own model,
   TARGET = intent. Never state a modelled figure without the chip. */
.chip{
  display:inline-block;font-family:var(--mono);font-size:.625rem;font-weight:600;
  letter-spacing:.16em;text-transform:uppercase;padding:4px 9px;border-radius:var(--r-sm);
  border:1px solid currentColor;line-height:1.3;
}
.chip.validated{color:var(--charge)}
.chip.target{color:var(--warn)}
.chip.modelled{color:var(--current-lift)}

/* ── NAV ──────────────────────────────────────────────────────────────────
   Two current-state mechanisms, deliberately:
   · [aria-current="page"] — which PAGE you are on. Set server-side by the
     header partial. Works with no JS.
   · .nav-ind — a sliding rule under the nav. Homepage only; driven by
     site.js §5. Purely decorative. It was built to track which SECTION of
     the homepage was in view, but no nav item points at an in-page anchor
     any more, so it only follows the cursor. Point one at "/#…" again and
     the scroll tracking comes back on its own. Rests at width:0, so with
     nothing hovered it is invisible rather than parked at the far left. */
header[data-header]{
  position:fixed;inset:0 0 auto 0;z-index:80;display:flex;align-items:center;
  justify-content:space-between;padding:20px clamp(20px,5vw,50px);
  transition:background var(--t-base),padding var(--t-base),backdrop-filter var(--t-base);
}
/* rgba(var(--abyss-rgb),…) IS --abyss. The homepage previously used rgba(3,10,18,.82),
   left over from the pre-v1.2 darker palette — so the sticky nav was a
   different colour on the homepage than on the team page. Unified here. */
header.is-solid{background:rgba(var(--abyss-rgb),.88);backdrop-filter:blur(14px);padding-block:13px}
.brand{display:flex;align-items:center}
.brand img{height:30px;width:auto;transition:opacity var(--t-fast)}
.brand:hover img{opacity:.82}
@media(max-width:600px){.brand img{height:24px}}

nav.mainnav{display:flex;gap:28px;font-size:.8rem;letter-spacing:.07em;position:relative}
nav.mainnav a{opacity:.8;transition:opacity var(--t-fast);padding-bottom:5px}
nav.mainnav a:hover,nav.mainnav a.active,nav.mainnav a[aria-current]{opacity:1}
nav.mainnav a[aria-current]{border-bottom:2px solid var(--current)}
.nav-ind{position:absolute;bottom:0;height:2px;background:var(--current);
  transition:left var(--t-base) var(--ease-out),width var(--t-base) var(--ease-out);
  left:0;width:0}

.nav-cta{border:1px solid var(--line-strong);padding:9px 19px;border-radius:999px;
  font-size:.75rem;letter-spacing:.06em;transition:background var(--t-fast),border-color var(--t-fast)}
.nav-cta:hover{background:var(--current);border-color:var(--current)}

/* The sheet's wrapper. Above the breakpoint it must not exist as a box at
   all, or it
   would collapse the nav and the CTA into one flex item and the nav would
   stop being centred — display:contents promotes its two children back to
   header flex children. The flex fallback is for a renderer without it;
   it groups the two on the right rather than breaking the header. */
.navsheet{display:flex;align-items:center;gap:clamp(24px,5vw,54px);margin-left:auto}
@supports(display:contents){.navsheet{display:contents}}

/* ── BURGER + MOBILE SHEET ────────────────────────────────────────────────
   Below the breakpoint the nav was `display:none` with nothing in its place,
   so on a phone most pages were reachable only through the footer.

   Spec §00: "Mobile: full-screen overlay, items animate in on a 60ms
   stagger, investor CTA pinned to the bottom of the sheet." That is what
   .navsheet becomes here — the same links, restyled, never a second copy.
   The burger toggles .nav-open on the header (see §3b of site.js). */
.navburger{display:none;flex-direction:column;justify-content:center;gap:5px;
  width:42px;height:42px;padding:0 9px;cursor:pointer;
  background:transparent;border:1px solid var(--line-strong);border-radius:999px;
  transition:background var(--t-fast),border-color var(--t-fast)}
.navburger:hover{border-color:var(--current)}
.navburger:focus-visible{outline:2px solid var(--current-lift);outline-offset:3px}
.navburger span{display:block;width:100%;height:1.5px;border-radius:2px;background:var(--white);
  transition:transform var(--t-fast) var(--ease-out),opacity var(--t-fast)}
/* to an X — 6.5px is one bar (1.5) plus one gap (5) */
header.nav-open .navburger{border-color:var(--current)}
header.nav-open .navburger span:nth-child(1){transform:translateY(6.5px) rotate(45deg)}
header.nav-open .navburger span:nth-child(2){opacity:0}
header.nav-open .navburger span:nth-child(3){transform:translateY(-6.5px) rotate(-45deg)}

/* Must follow the @supports above: same specificity, so source order is what
   makes the sheet a flex column here rather than display:contents.

   1145px, not the 1040px it was, and not the 940px this was built at. The bar
   is brand | nav | CTA under space-between, so every nav item eats the slack
   on both sides of it. Seven items exhausted it: at 950px the brand touched
   "The Fleet" and "Contact" ran under the Investor Relations pill — both gaps
   measured 0. The slack grows about 1px per 2px of viewport, so 1040 was
   where seven items cleared 45px.

   Goldilocks made it eight (site.js, 28 Aug 2026) and the number moved with
   it, exactly as the last line of this note said it would. Measured on
   /technology/ with the bar rendered: 1100px gave 23.7px gaps, 1145px gives
   46.2px, 1190px gives 68.7px. 1145 is where eight items clear 45 again.

   The cost is the 1041–1145 band, which now gets the burger instead of the
   bar — small laptop windows. Landscape tablets are unaffected: a 1024px iPad
   was already under the old 1040. The alternative was a fluid nav gap
   (gap:clamp(…) on nav.mainnav, currently a flat 28px), which would hold the
   bar down to about 1040 by tightening the spacing between items at every
   width below the clamp ceiling. That changes the bar's rhythm everywhere to
   save one band; raising the breakpoint changes nothing above it.

   Trim the nav and this can come back down — the number is a function of how
   many items are in site.js, not a constant. */
@media(max-width:1145px){
  .navburger{display:flex}
  /* the sheet is a positioned child of the header and so paints over the
     brand and the burger; lift both back above it, so the top bar stays
     readable and the burger stays tappable while the sheet is open */
  .brand,.navburger{position:relative;z-index:1}
  .navsheet{
    position:fixed;inset:0;margin:0;
    display:flex;flex-direction:column;align-items:stretch;gap:0;
    padding:clamp(76px,11vh,104px) clamp(22px,7vw,54px)
            calc(clamp(22px,3vh,34px) + env(safe-area-inset-bottom,0px));
    background:rgba(var(--abyss-rgb),.97);backdrop-filter:blur(18px);
    overflow-y:auto;overscroll-behavior:contain;
    /* visibility, not display, so the closed sheet is out of the tab order
       and the a11y tree AND still has something to transition */
    opacity:0;visibility:hidden;
    transition:opacity var(--t-base) var(--ease-out),visibility var(--t-base);
  }
  header.nav-open .navsheet{opacity:1;visibility:visible}
  /* THE HEADER MUST CARRY NO backdrop-filter WHILE THE SHEET IS OPEN. An
     element with one becomes the containing block for its fixed descendants,
     so `inset:0` on the sheet would resolve against the header's own box
     rather than the viewport — scroll down far enough for .is-solid, open the
     menu, and the "full-screen" overlay is a bar 82px tall. It needs no
     background of its own either: the sheet spans the whole viewport, the
     header strip included, and the brand and burger are lifted above it. */
  header.nav-open{background:transparent;backdrop-filter:none}

  /* SIZED BY HEIGHT, NOT WIDTH. The sheet is full-screen and its rows stack,
     so what runs out is vertical space — but 6.2vw read the wrong axis: it
     hits its 2rem cap at any viewport wider than ~516px, so a 1024x768 iPad
     in landscape got 91px rows and pushed the investor CTA 44px below the
     fold. min(6.2vw,2.6vh) keeps the phone sizing it was tuned for and lets
     short-and-wide viewports shrink instead of overflowing. The CTA must
     stay reachable without a scroll (§00: the door that never scrolls away),
     and eight rows plus that CTA now fit every viewport this sheet covers,
     from 360x640 up to the nav breakpoint. Eight is measured, not assumed:
     at 360x640 the CTA's bottom edge sits 22px clear of the fold. That is
     the margin a ninth row would have to come out of — it will not fit, so
     a ninth item means re-tuning the row padding here, not just adding a
     --stagger step below. */
  nav.mainnav{display:flex;flex-direction:column;gap:0;
    font-size:clamp(1.05rem,min(6.2vw,2.6vh),2rem);letter-spacing:-.02em}
  nav.mainnav a{opacity:0;transform:translateY(16px);padding:clamp(9px,1.6vh,19px) 2px;
    border-bottom:1px solid var(--line);
    transition:opacity 340ms var(--ease-out) var(--stagger,0ms),
               transform 340ms var(--ease-out) var(--stagger,0ms)}
  /* 60ms stagger, spec §00. One step per nav item (site.js data), with the
     CTA riding in one beat after the last of them — so adding a nav item
     means adding a step here and pushing the CTA back by 60ms. An item past
     the last step still fades in, just on the 0ms fallback, out of step
     with the rest. */
  nav.mainnav a:nth-child(1){--stagger:60ms}
  nav.mainnav a:nth-child(2){--stagger:120ms}
  nav.mainnav a:nth-child(3){--stagger:180ms}
  nav.mainnav a:nth-child(4){--stagger:240ms}
  nav.mainnav a:nth-child(5){--stagger:300ms}
  nav.mainnav a:nth-child(6){--stagger:360ms}
  nav.mainnav a:nth-child(7){--stagger:420ms}
  nav.mainnav a:nth-child(8){--stagger:480ms}
  .navsheet .nav-cta{--stagger:540ms;opacity:0;transform:translateY(16px);
    margin-top:auto;padding:clamp(13px,2vh,17px) 24px;text-align:center;font-size:.82rem;
    transition:opacity 340ms var(--ease-out) var(--stagger),
               transform 340ms var(--ease-out) var(--stagger),
               background var(--t-fast),border-color var(--t-fast)}
  header.nav-open nav.mainnav a,
  header.nav-open .navsheet .nav-cta{opacity:1;transform:none}
  /* closing is not staggered — the sheet fades as one */
  header:not(.nav-open) nav.mainnav a,
  header:not(.nav-open) .navsheet .nav-cta{transition-delay:0ms}
  /* The global reduced-motion block collapses transition DURATIONS, not
     delays, so without this the sheet's own items would still trickle in
     over 360ms for a reader who asked for no motion. */
  @media(prefers-reduced-motion:reduce){
    nav.mainnav a,.navsheet .nav-cta{transition-delay:0ms!important}
  }
  /* :last-of-type, not :last-child — on the homepage the last child of the
     nav is the (hidden) indicator, so :last-child matches no link at all */
  nav.mainnav a:last-of-type{border-bottom:0}
  /* the desktop rule marks the current page with a 2px underline, which on a
     row this size reads as a heavier divider rather than as a state */
  nav.mainnav a[aria-current]{border-bottom-width:1px;color:var(--current-lift)}
  nav.mainnav a[aria-current]:last-of-type{border-bottom:0}
  nav.mainnav a:focus-visible{outline:2px solid var(--current-lift);outline-offset:-2px}
  .nav-ind{display:none}
  /* the page behind the sheet must not scroll under it */
  html.nav-locked,html.nav-locked body{overflow:hidden}

}
@media(max-width:600px){.nav-cta{font-size:.68rem;padding:8px 14px}
  .navburger{width:38px;height:38px;padding:0 8px}}

/* ── PHONE RHYTHM ─────────────────────────────────────────────────────────
   James, 28 Aug 2026: "spacing between the modules" on mobile — too much,
   tighten it. Every interior section pads with a vh-based clamp, which is
   right on a laptop and absurd on a phone: at 812px of height, 13vh is
   105px, so two adjacent sections put 210px — a quarter of the screen —
   of nothing between them, and the page reads as a series of gaps.

   56px each side, so a module change costs ~112px. That is still clearly a
   break; it is no longer a pause.

   `body .sec`, not `.sec`, and that is deliberate rather than sloppy: .sec
   is declared identically in interior.css, tech.css and team.css, all of
   which load AFTER this file, so a bare .sec here would lose on source
   order. One extra element in the selector wins without an !important and
   without touching three stylesheets. Anything with its own id-scoped
   padding (#our-story, #team-says, #where, #media, #leadership) still wins
   over this, which is correct — those were tuned for their own contents. */
@media(max-width:620px){
  body .sec{padding-block:56px}
  /* The closing bands keep more than a section does — they are a full-bleed
     photograph with a statement on it, and squeezing one to 56px makes it
     look like a section that failed to load its picture. 80px is the same
     figure the homepage's .ask keeps, for the same reason. Both are
     declared in later stylesheets (interior.css, team.css), hence `body`. */
  body .xclose,body .careers{padding-block:80px}
}

/* ── LIGHTBOX ─────────────────────────────────────────────────────────────
   A linked image, full screen. Built and driven by lightbox.js, which
   exists on any page carrying an <a data-lightbox> and nowhere else — see
   the note at the top of that file for why the trigger is a real anchor.

   THE ROTATION IS THE POINT, on a phone. The chart this was written for is
   1006x590 — landscape, on a device that is not. Fitted to a 375px portrait
   screen it draws 375px wide, which is the size it already was in the page
   and no help at all. Turned 90° it is limited by the screen's LONG edge
   instead: ~640px of chart, and the reader tilts the phone to read it. Turn
   the phone for real and the orientation query stops applying, the picture
   comes back upright and fits the landscape screen — so both ways up give a
   readable chart, and the hint says so.

   Above 620px, or on a landscape phone, it is the ordinary thing: contained
   within the viewport, centred. */
.lbx{position:fixed;inset:0;z-index:200;
  background:rgba(var(--abyss-rgb),.94);backdrop-filter:blur(6px);
  display:flex;align-items:center;justify-content:center}
.lbx[hidden]{display:none}
.lbx-fig{position:relative;width:100%;height:100%;
  display:flex;align-items:center;justify-content:center;padding:clamp(12px,3vw,40px)}
.lbx-fig img{max-width:100%;max-height:100%;width:auto;height:auto;display:block;
  background:#fff;border-radius:2px}
/* Two hints, because the useful thing to say depends on what the reader is
   holding: a phone is told how to get a bigger look, a mouse is told how to
   get out. Switched on pointer type rather than width — a touchscreen
   laptop gets the touch line and deserves it. */
.lbx-hint{position:absolute;left:0;right:0;bottom:clamp(10px,2vh,18px);margin:0;
  text-align:center;font-family:var(--mono);font-size:.6rem;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ghost);max-width:none;pointer-events:none}
.lbx-hint .hint-t{display:none}
@media(hover:none){
  .lbx-hint .hint-t{display:inline}
  .lbx-hint .hint-c{display:none}
}
.lbx-x{position:absolute;top:clamp(10px,2vh,18px);right:clamp(10px,2vw,18px);
  width:44px;height:44px;display:grid;place-items:center;cursor:pointer;
  background:rgba(var(--abyss-rgb),.7);border:1px solid var(--line-strong);border-radius:999px}
.lbx-x svg{width:18px;height:18px;fill:none;stroke:var(--white);stroke-width:1.6}
.lbx-x:hover{border-color:var(--current)}
.lbx-x:focus-visible{outline:2px solid var(--current-lift);outline-offset:3px}
/* The page behind must not scroll while this is up. */
.lbx-open,.lbx-open body{overflow:hidden}
@media(max-width:620px) and (orientation:portrait){
  .lbx-fig img{position:absolute;left:50%;top:50%;max-width:none;max-height:none;
    width:min(calc(100svh - 88px),calc((100vw - 20px) * 1006 / 590));
    transform:translate(-50%,-50%) rotate(90deg)}
}
@media(prefers-reduced-motion:reduce){
  .lbx{backdrop-filter:none}
}

/* ── FOOTER ───────────────────────────────────────────────────────────── */
footer{background:var(--abyss);border-top:1px solid var(--line);
  padding:clamp(60px,10vh,110px) 0 40px}
.irpanel{background:var(--deck);border:1px solid var(--line-signal);border-radius:var(--r-lg);
  padding:clamp(26px,4vw,44px);display:flex;justify-content:space-between;align-items:center;
  gap:30px;flex-wrap:wrap;margin-bottom:clamp(50px,8vh,80px)}
.irpanel .t{font-family:var(--mono);font-size:.66rem;letter-spacing:.24em;text-transform:uppercase;
  color:var(--current-lift);font-weight:600;margin-bottom:12px}
.irpanel .h4{font-weight:200;margin-bottom:8px}
.irpanel .mail{font-family:var(--mono);font-size:.8rem;color:var(--fog)}
.fgrid{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:40px 30px}
.fgrid .about p{color:var(--fog);font-size:.85rem;margin-top:22px;max-width:30ch}
.brand.flogo img{height:auto;width:min(230px,72%)}
.fcol h3{font-family:var(--mono);font-size:.62rem;letter-spacing:.22em;text-transform:uppercase;
  color:var(--fog);margin-bottom:18px;font-weight:600}
.fcol ul{list-style:none;display:grid;gap:11px}
.fcol a{font-size:.87rem;color:var(--mist);position:relative;padding-bottom:2px}
.fcol a::before{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;
  background:var(--current);transform:scaleX(0);transform-origin:left;
  transition:transform 220ms var(--ease-out)}
.fcol a:hover{color:var(--white)}
.fcol a:hover::before{transform:scaleX(1)}
/* A designed-but-unbuilt entry (site.js `soon: true`). The template gives it
   no href at all, so all this has to do is say "not yet" — --ghost, one stop
   below the live links beside it. */
[data-soon]{color:var(--ghost)}

/* Social row — icons only, sat under the Connect column's addresses.
   36px targets: comfortably over the 24px minimum on a phone without the
   row growing wide enough to wrap in a 1fr footer column. The marks inherit
   currentColor, so the hover is one colour change on the <a>. */
/* `.fcol ul` above is display:grid and out-specifies a bare `.fsocial`, which
   stacked the four icons in a column — hence `.fcol .fsocial`. */
.fcol .fsocial{list-style:none;display:flex;flex-wrap:wrap;gap:6px;margin-top:22px;
  margin-left:-9px}
.fsocial a{display:grid;place-items:center;width:36px;height:36px;border-radius:50%;
  color:var(--fog);transition:color var(--t-fast),background var(--t-fast)}
.fsocial svg{width:17px;height:17px;fill:currentColor;display:block}
.fsocial a:hover{color:var(--white);background:rgba(255,255,255,.07)}
.fsocial a:focus-visible{outline:2px solid var(--current-lift);outline-offset:2px}
/* The column-link underline sweep is on .fcol a and would otherwise draw a
   1px line across each icon. */
.fsocial a::before{content:none}

.fbase{margin-top:clamp(50px,8vh,80px);padding-top:28px;border-top:1px solid var(--line);
  display:flex;justify-content:space-between;gap:24px;flex-wrap:wrap;
  font-size:.75rem;color:var(--fog);font-family:var(--mono);letter-spacing:.06em;line-height:1.9}
.fbase .flogos{display:flex;gap:22px;align-items:center}
.fbase .flogos img{height:17px;opacity:.35;transition:opacity var(--t-fast)}
.fbase .flogos img:hover{opacity:1}
/* Policy links. The middot is a ::before on every item but the first, so the
   separators come and go with the items when the row wraps — typing them
   into the copy leaves a dangling "·" at the end of a wrapped line. */
.flegal{list-style:none;display:flex;flex-wrap:wrap;align-items:baseline;
  column-gap:9px;row-gap:2px;margin-top:2px}
.flegal li+li::before{content:"·";color:var(--ghost);margin-right:9px}
/* "Cookie settings" is a <button> — it opens a bar, it does not navigate —
   so it is carried by the same selectors as its neighbours and stripped of
   the UA button styling. It has to be indistinguishable from the policy
   links beside it: the row reads as one list of four small-print items. */
.flegal a,.flegal .consent-open{color:var(--fog);position:relative;padding-bottom:2px}
.flegal .consent-open{
  appearance:none;background:none;border:0;padding:0 0 2px;margin:0;cursor:pointer;
  font:inherit;letter-spacing:inherit;line-height:inherit}
.flegal a::before,.flegal .consent-open::before{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;
  background:var(--current);transform:scaleX(0);transform-origin:left;
  transition:transform 220ms var(--ease-out)}
.flegal a:hover,.flegal .consent-open:hover{color:var(--white)}
.flegal a:hover::before,.flegal .consent-open:hover::before{transform:scaleX(1)}
.flegal .consent-open:focus-visible{outline:2px solid var(--current-lift);outline-offset:3px}
.credit{font-size:.66rem;color:var(--ghost);margin-top:10px}
@media(max-width:820px){.fgrid{grid-template-columns:1fr 1fr}}

/* ── FILM FRAME ───────────────────────────────────────────────────────────
   The poster and the PLAY ring. Shared: the homepage explainer (§03b) and
   the Cape Horn CFD reel on the technology page use the same frame.

   Both pages now play the film IN the frame — the homepage because its
   chapter strip seeks the running film (home.js §05), the technology reel
   because it starts itself on scroll (site.js §6). So the in-place rules
   moved here from home.css, where they were while only the homepage needed
   them, and so did the chapter strip below — both films have one. */
/* The frame is 16/9 and, until now, sized by width alone — which is fine on a
   desktop and wrong on a laptop. At 1366x600 of viewport (a 768px panel once
   the browser chrome and the taskbar have taken their cut) a full-width frame
   inks 1229x692: taller than the screen it is sitting on, so the reader sees
   a slab of video with the chapter strip under it cut off by the fold. That
   was the note back from review — "too big for laptop screen".

   Capping the WIDTH rather than the height is what keeps it 16/9. A
   max-height on an aspect-ratio box loses the race with width:100%: the box
   letterboxes instead of shrinking, the poster (object-fit:cover) crops, and
   the frame stops matching the film inside it. Capping width shrinks the
   whole frame and centres it.

   The 168px is the rest of the screen the frame has to share: the fixed
   header, and the chapter strip plus the link under it. Short viewports only
   — at 900px of height the cap computes past the wrap width and nothing
   moves. */
.vframe{position:relative;border:1px solid var(--line-signal);border-radius:var(--r-lg);
  overflow:hidden;aspect-ratio:16/9;background:#000;cursor:pointer;
  max-width:calc((100svh - 168px) * 16 / 9);margin-inline:auto}
.vframe::after{content:"";position:absolute;inset:-60px;z-index:-1;
  background:radial-gradient(closest-side,rgba(59,123,255,.30),transparent);filter:blur(60px)}
.vframe video,.vframe img.poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  transition:filter var(--t-base),transform 9s linear}
.vframe:hover img.poster{filter:saturate(.4) brightness(.6)}
.vframe .playring{
  position:absolute;left:50%;top:50%;translate:-50% -50%;width:92px;height:92px;border-radius:50%;
  border:1px solid rgba(255,255,255,.7);display:grid;place-items:center;
  font-family:var(--mono);font-size:.62rem;letter-spacing:.2em;
  transition:background var(--t-base),border-color var(--t-base),scale var(--t-base);
}
.vframe .playring::before{content:"";position:absolute;inset:-1px;border-radius:50%;
  border:1px solid transparent;border-top-color:var(--current-lift);
  animation:spin 12s linear infinite}
@keyframes spin{to{rotate:360deg}}
.vframe:hover .playring{background:var(--current);border-color:var(--current);scale:1.06}
.vframe video{object-fit:contain;background:#000;
  opacity:0;pointer-events:none;transition:opacity var(--t-base)}
.vframe.playing video{opacity:1;pointer-events:auto}
.vframe.playing img.poster{opacity:0}
.vframe.playing{cursor:default}
.vframe.playing .playring,.vframe.playing::after{display:none}

/* Same width cap as the frame, for the same reason: once .vframe starts
   shrinking on a short viewport, a caption row still inked to the full wrap
   would hang off both ends of the film it belongs to. Everything under the
   frame is part of the frame's block and stays the frame's width. */
.vcap{display:flex;justify-content:space-between;align-items:center;gap:24px;
  margin-top:26px;flex-wrap:wrap;
  max-width:calc((100svh - 168px) * 16 / 9);margin-inline:auto}
/* max-width:none because the global p{max-width:46ch} — 52ch on a dense page
   — was still winning here, and it is the wrong rule for this paragraph. It
   is a credit line sitting opposite a link in a flex row, not body text, and
   the reading measure was breaking a one-line credit over two lines while
   half the row beside it stood empty. It still wraps when the row is
   genuinely too narrow for it; it just is not pre-wrapped. */
.vcap p{color:var(--mist);max-width:none}

/* ── CHAPTER STRIP ────────────────────────────────────────────────────────
   Shared with the frame above it. It was homepage-only while the homepage
   was the only film long enough to argue in movements; the Cape Horn reel
   earned two the moment the camera went under the water, so the rules moved
   here from home.css with the frame.

   Timestamps are gone — nobody picks a chapter by its start time, and
   00:00 / 01:26 / 01:47 / 02:40 in --current-lift was the loudest thing in
   the strip, which put the emphasis on the clock rather than on what the
   chapter contains. The label is now the whole control: near-body size,
   sentence case, white, with a quiet index numeral above it.

   data-t still carries the seek — home.js §05 for the homepage's own frame,
   site.js §6 for any [data-autofilm] one. Only the printed time went. */
.chapters{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,170px),1fr));
  gap:10px;margin-top:12px;
  /* Tracks the frame — see the cap on .vframe. */
  max-width:calc((100svh - 168px) * 16 / 9);margin-inline:auto}
.chapters button{
  position:relative;display:grid;grid-template-columns:1fr auto;align-items:center;
  column-gap:12px;row-gap:8px;
  /* A flat fill read as a panel. The gradient gives the card a lit top edge,
     which is what makes it sit up off the page as something pressable. */
  background:linear-gradient(180deg,rgba(255,255,255,.085),rgba(255,255,255,.03));
  border:1px solid rgba(255,255,255,.20);border-radius:var(--r-md);
  cursor:pointer;padding:13px 15px 14px;text-align:left;color:var(--white);
  font-family:var(--sans);font-size:1.14rem;font-weight:500;letter-spacing:-.015em;
  line-height:1.2;
  transition:background var(--t-fast),border-color var(--t-fast),
             translate var(--t-fast) var(--ease-snap),box-shadow var(--t-fast);
}
.chapters button .cn{grid-column:1/-1;font-family:var(--instrument);
  font-size:.58rem;font-weight:400;letter-spacing:.2em;color:var(--current-lift);
  transition:color var(--t-fast)}

/* The play chip. Same device as the ring on the film, at control size — it is
   what tells you at a glance that these START something rather than being
   labels under a video. Decorative: the button's text is already the label,
   so the span carries aria-hidden and adds nothing to the accessible name. */
.chapters button .cp{
  width:30px;height:30px;border-radius:50%;align-self:center;
  border:1px solid rgba(255,255,255,.34);display:grid;place-items:center;
  color:rgba(255,255,255,.78);
  transition:background var(--t-fast),border-color var(--t-fast),
             color var(--t-fast),scale var(--t-fast) var(--ease-snap)}
.chapters button .cp::before{content:"";width:0;height:0;margin-left:2px;
  border-left:7px solid currentColor;
  border-top:5px solid transparent;border-bottom:5px solid transparent}

.chapters button:hover,.chapters button:focus-visible{
  background:linear-gradient(180deg,rgba(62,140,255,.26),rgba(62,140,255,.11));
  border-color:var(--current);translate:0 -3px;
  box-shadow:0 14px 32px -14px rgba(62,140,255,.7)}
.chapters button:hover .cp,.chapters button:focus-visible .cp{
  background:var(--current);border-color:var(--current);color:#fff;scale:1.09}
.chapters button:hover .cn,.chapters button:focus-visible .cn{color:var(--charge)}
/* Presses land. Without this the card lifts on hover and then nothing
   acknowledges the click itself. */
.chapters button:active{translate:0 -1px;
  box-shadow:0 5px 14px -10px rgba(62,140,255,.7);transition-duration:60ms}

.chapters button[aria-current="true"]{
  background:linear-gradient(180deg,rgba(62,140,255,.30),rgba(62,140,255,.14));
  border-color:var(--current)}
.chapters button[aria-current="true"] .cp{
  background:var(--current);border-color:var(--current);color:#fff}
.chapters button[aria-current="true"] .cn{color:var(--charge)}

@media(max-width:760px){.chapters{grid-template-columns:1fr 1fr;gap:8px}
  .chapters button{font-size:1.02rem;padding:12px 13px 13px;column-gap:10px}
  .chapters button .cp{width:27px;height:27px}}
@media(prefers-reduced-motion:reduce){
  .chapters button:hover,.chapters button:focus-visible,.chapters button:active{translate:none}
  .chapters button:hover .cp,.chapters button:focus-visible .cp{scale:none}}



/* ── REDUCED MOTION ───────────────────────────────────────────────────────
   Mandatory, spec A4. This is the GLOBAL floor — pages with pinned or
   horizontal scroll must additionally unwind those in their own block. */
@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;
    transition-duration:.001ms!important}
  [data-reveal]{opacity:1;transform:none}
}

/* ── ODOMETER ROLL ─────────────────────────────────────────────────────
   Built by buildRoll() in site.js and positioned every frame by
   paintRolls() off the number's own elapsed time — there is no transition
   here, because the roll is driven frame by frame from JS. It used to be
   scrubbed by the reader's scroll; see the note in site.js §2 for why it
   now plays on its own clock.

   Every glyph — digits and the absorbed affixes — is a 1em box in one
   inline-flex row, so they share a bottom edge and no baseline correction
   is needed. See the note in site.js. */
[data-count].rolling{display:inline-flex;align-items:flex-end;line-height:1}
/* the strip holds ~40 throwaway glyphs; keep them out of selections and
   copied text, where they would paste as 0123456789012345678… */
.r-win{display:inline-block;overflow:hidden;height:1em;line-height:1;
  user-select:none;-webkit-user-select:none}
/* While the wheel is turning the slot is masked top and bottom, so digits
   arrive and leave rather than being guillotined at the box edge. Dropped
   the moment the column lands, or every settled number on the site would
   read as permanently half-faded. */
[data-count].rolling:not(.landed) .r-win{
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 30%,#000 70%,transparent 100%);
          mask-image:linear-gradient(180deg,transparent 0,#000 30%,#000 70%,transparent 100%)}
.r-strip{display:block;will-change:transform,filter}
.r-d{display:block;height:1em;line-height:1;text-align:center;font-variant-numeric:tabular-nums}
.r-sep{display:inline-block;line-height:1;white-space:pre}
/* the click of the wheel stopping */
[data-count].landed{animation:rollland .7s var(--ease-out)}
@keyframes rollland{
  0%{text-shadow:0 0 26px rgba(147,198,255,.85),0 0 6px rgba(147,198,255,.6)}
  100%{text-shadow:0 0 0 rgba(147,198,255,0)}}


/* ── COOKIE CONSENT ───────────────────────────────────────────────────────
   A bar across the foot of the window, not a modal. Nothing on this site is
   gated behind the answer, so darkening the page and trapping focus would
   overstate what is being asked — and a dimmed overlay is exactly the
   pressure that makes a "yes" worth less than it looks.

   It sits on --abyss with a blur rather than a flat panel so the page keeps
   moving underneath it, which is the same trick the header uses. The top
   rule is the only edge: no border box, in a design that has no boxes.

   Hidden state is the `hidden` attribute; .is-up is the transition. Keeping
   those separate means the bar is genuinely display:none for assistive tech
   when it is away, rather than a transparent thing still in the tab order. */
.consent{
  position:fixed;left:0;right:0;bottom:0;z-index:120;
  background:rgba(var(--abyss-rgb),.94);
  backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
  border-top:1px solid var(--line-strong);
  transform:translateY(100%);
  transition:transform 420ms var(--ease-out);
}
.consent.is-up{transform:none}
.consent[hidden]{display:none}

.consent-in{
  max-width:1240px;margin:0 auto;padding:clamp(18px,2.4vh,26px) clamp(20px,4vw,48px);
  display:flex;align-items:center;justify-content:space-between;gap:clamp(20px,3vw,48px);
  flex-wrap:wrap;
}
/* 62ch here capped the notice at ~465px in a 1240px bar, so it stacked into
   four short lines with a lake of empty space between it and the buttons.
   A one-paragraph notice is not body copy and does not need a reading
   measure — it needs to be over in two lines. */
.consent-say{max-width:78ch;flex:1 1 460px}
/* An h2 by structure — the bar is a landmark and needs a name — but it is
   not competing with the page's own headings, so it is sized as a label. */
.consent-h{
  font-size:.72rem;font-weight:600;letter-spacing:.26em;text-transform:uppercase;
  color:var(--current-lift);margin:0 0 9px;
}
.consent-h:focus-visible{outline:2px solid var(--current-lift);outline-offset:6px}
/* max-width:none because the global p{max-width:46ch} was still winning
   here — the notice sat at 350px inside a 624px column and stacked to four
   lines while the space beside it went empty. The site-wide measure is for
   reading body copy down a page; this is one notice across a bar. */
.consent-p{color:var(--mist);font-size:.95rem;line-height:1.5;margin:0;max-width:none}

.consent-do{display:flex;align-items:center;gap:12px;flex-wrap:wrap;flex:0 0 auto}
/* Both buttons take this — identical box, identical weight. Do not give
   Accept the primary fill: an accept that is louder than the reject is the
   dark pattern regulators actually name, and it costs a valid consent. */
.consent-btn{padding:13px 30px;font-size:.94rem;min-width:132px;justify-content:center}
.consent-more{font-size:.88rem;margin-left:4px}

@media(max-width:720px){
  .consent-in{flex-direction:column;align-items:flex-start;gap:18px}
  /* flex-basis follows the main axis, so the 460px that sets the notice's
     WIDTH in the row layout was setting its HEIGHT once this stacks —
     460px of empty bar between the text and the buttons, three quarters of
     a phone screen. The basis has no job in a column here. */
  .consent-say{flex:0 0 auto;max-width:none}
  .consent-do{width:100%}
  /* Equal width as well as equal weight once they stack, so neither reads
     as the default by being the bigger target. */
  .consent-btn{flex:1 1 0;min-width:0}
  .consent-more{width:100%;margin-left:0}
}

/* Hidden in the markup and revealed by consent.js only when analytics is
   configured — a settings link for a thing the site does not do is worse
   than no link. Stated rather than left to the UA sheet because the parent
   is a flex container, where a bare [hidden] child can still take a slot.
   The empty <li> would otherwise draw .flegal's "·" separator. */
.flegal li:has([data-consent-open][hidden]){display:none}
.flegal [data-consent-open][hidden]{display:none}
