/* ============================================================
   DJET — one crossing, horizon to horizon.

   The whole page is a single sky. There are no section edges:
   the films live on ONE fixed stage behind everything, and only
   the light changes as you scroll. Sections are transparent
   scrollers that decide which plate is lit and what the palette
   does — they never paint a background of their own.

   That is the entire idea, and every rule below serves it.
   ============================================================ */

/* ---- fonts (self-hosted; Newsreader speaks, Geist explains) ---- */
@font-face{font-family:'Newsreader';font-style:normal;font-weight:200 500;font-display:swap;
  src:url("/djet/fonts/newsreader-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Newsreader';font-style:normal;font-weight:200 500;font-display:swap;
  src:url("/djet/fonts/newsreader-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:'Newsreader';font-style:italic;font-weight:200 500;font-display:swap;
  src:url("/djet/fonts/newsreader-italic-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Geist';font-style:normal;font-weight:300 600;font-display:swap;
  src:url("/djet/fonts/geist-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:'Geist';font-style:normal;font-weight:300 600;font-display:swap;
  src:url("/djet/fonts/geist-latin-ext.woff2") format("woff2");
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
@font-face{font-family:'Geist Mono';font-style:normal;font-weight:400;font-display:swap;
  src:url("/djet/fonts/geistmono-latin.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+2000-206F,U+20AC,U+2122,U+2212,U+FEFF,U+FFFD}

/* ---------------- the light, hour by hour ----------------
   One variable set. Each beat re-tints it, and because the
   change lives on <body> every layer moves together — the
   scrim, the rules, the type — so the sky never tears. */
:root{
  --sky:#0B1017;          /* what sits behind everything */
  --ink:#EEF3F7;          /* type on film */
  --dim:color-mix(in srgb, var(--ink) 58%, transparent);
  --faint:color-mix(in srgb, var(--ink) 16%, transparent);
  --accent:#C9A227;       /* daylight gold — charter */
  --scrim:.42;            /* how hard the film is held down */
  --measure:34rem;

  --step:clamp(1rem, .7rem + 1.4vw, 1.45rem);
  --gap:clamp(1.5rem, 1rem + 3vw, 4rem);
  --edge:max(1.25rem, env(safe-area-inset-left));
}

/* first light: cold overcast, one gold band at the horizon */
body[data-beat="dawn"]    { --sky:#10161D; --scrim:.30; }
body[data-beat="boarding"]{ --sky:#12203A; --scrim:.40; }
/* the brightest plate in the piece, and it carries the most copy —
   the fleet scale sits on it, so it needs holding down harder */
body[data-beat="aircraft"]{ --sky:#0E2C4E; --scrim:.50; }
/* No film at all: the blueprint sits on the page's own ground, which is why
   `blueprint` matches no plate — light() simply unlights everything. */
body[data-beat="blueprint"]{ --sky:#080C11; --scrim:0; }
body[data-beat="hour"]    { --sky:#05070B; --scrim:.10; }  /* the quiet one is already dark */
body[data-beat="dusk"]    { --sky:#2B1F22; --scrim:.30; --accent:#E0A46A; }
/* Terms on a plain ground, like the blueprint: the two beats that are
   documents rather than pictures. Borrowing the night city here made the
   composite's own frame arrive twice. */
body[data-beat="terms"]   { --sky:#0A0D12; --scrim:0; }
/* THE TURN, and it has no picture at all. Every treatment of that rooftop
   helipad was a compromise — daylight graded down to pretend at 03:14.
   Black is not a fallback here: the page going dark IS the turn, and it
   makes the composite that follows land out of nothing. */
body[data-beat="dark"]    { --sky:#03050A; --scrim:0; --accent:#7FA8C4; }
/* rotary, after the turn — same cold as the rest of the second half */
body[data-beat="heli"]    { --sky:#060A11; --scrim:.40; --accent:#7FA8C4; }
/* the composite: same hour as `night`, held a touch harder because a lit
   aircraft sits on top of the city */
body[data-beat="nightjet"]{ --sky:#04070C; --scrim:.44; --accent:#7FA8C4; }
body[data-beat="night"]   { --sky:#04070C; --scrim:.50; --accent:#7FA8C4; }
/* THE FLIGHT DECK, and like the turn it has no picture — `deck` matches no
   plate, so light() unlights everything and the type sits on the page's own
   ground. It got here the long way: the beat used to run a cockpit film, but
   that film is broad daylight and this beat now sits after 03:14. Graded down
   it measured correctly (0.179, between the pad and the night city) and still
   read as mud, because night reads as night through points of light against
   dark and that frame has none — a silhouette against sky survives being
   pulled down, a subject against ground does not.
   So the two silences now BRACKET the ambulance half: 03:14 bare, the city and
   the pad lit between them, the flight deck bare again, then the close. A
   touch above `dark` so it is an echo of the turn rather than a repeat, and
   below `night` so the run still descends into the close. */
body[data-beat="deck"]    { --sky:#04060B; --scrim:0;   --accent:#7FA8C4; }
/* was --scrim:1, which crushed the city out entirely — see the #close block */
body[data-beat="close"]   { --sky:#04070C; --scrim:.62; --accent:#7FA8C4; }

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;overflow-x:clip}
html,body{margin:0}
body{
  background:var(--sky);
  color:var(--ink);
  font:300 var(--step)/1.6 'Geist',ui-sans-serif,-apple-system,system-ui,sans-serif;
  overflow-x:clip;
  transition:background-color 1.1s cubic-bezier(.4,0,.2,1);
}
img,video{max-width:100%;display:block}
a{color:inherit}

/* ============================ THE STAGE ============================
   Every film lives here, fixed to the viewport. Because the stage IS
   the viewport, any overlay anchored to the viewport is anchored to
   the cover-fit box automatically — the crop and the type can never
   drift apart, which is the usual way this kind of page breaks. */
.stage{position:fixed;inset:0;z-index:0;background:var(--sky);overflow:hidden}
.plate{
  position:absolute;inset:0;opacity:0;
  transition:opacity 1.2s cubic-bezier(.4,0,.2,1);
  background:var(--sky) center/cover no-repeat;
  will-change:opacity;
}
.plate.lit{opacity:1}

/* No filter here, unlike every other plate — it arrives already at its hour.
   This used to be a baked composite (a daylight helicopter luma-keyed over our
   night-city clip). It is now a single real camera: our own black machine
   lifting off a lit rooftop pad, so the aircraft, the pad and the city share
   one geometry instead of a cutout floating on an aerial. The old composite
   had them disagreeing — machine side-on, city an oblique looking down.

   It runs in SLOW MOTION at 2.5x, and the frames are real: Vision optical flow
   synthesises the in-betweens (.claude/slowmo.swift), so 34 source frames
   become 84 and the output stays at 24fps. Holding frames instead would have
   dropped it to ~10fps effective and strobed the rotor and the wash — the same
   failure the 16fps Wan route has. 6.92s, which is finally near house length.
   It PING-PONGS: a lift-off has no seamless forward loop, so it plays out and
   back. The loop is continuous by construction rather than by a dissolve. Only
   the first 1.4s of the source is usable — past that the camera orbits off the
   pad and at ~3.2s the airframe loses coherence altogether.
   Cut by .claude/pingpong.swift; the 16:9 centre band is also what drops the
   generator's burned-in watermark, and the portrait cut shaves 210px off the
   top for the same reason. */
/* first reading after load: land on the right plate without a fade */
body.no-fade .plate,body.no-fade{transition:none}
.plate video{
  position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:center;
}

/* A still plate, not a dead one. The photograph is not painted over —
   it is the plate that moves, very slowly, so the frame breathes without
   anything being added on top of it. */
.plate--still{
  background-position:52% 60%;
  transform-origin:50% 62%;
  animation:drift 42s ease-in-out infinite alternate;
}
/* ---- ONE FLEET, ONE LIVERY ----
   The day half used to run on stock: a white-and-blue Gulfstream at beats
   2, 3 and 4 and a cream one at dusk, against our black aircraft at the
   open and in the composite. One operator, four paint schemes — and no
   filter fixes that, because a grade cannot repaint an aeroplane. It can
   only take the whole frame down with it.

   So every jet beat is now OUR aircraft. Two generated frames, framed
   four ways and graded to their hour, which is exactly what a small
   operator's site looks like: one aeroplane, photographed once. */
/* ⚠️ THE OPENER HAS ITS OWN STILL, and the others must not be pointed at it.
   The film is now the ORIGINAL CLIP'S 4.5-8.0s cut, which is 8.6% brighter than
   `dawn.jpg` (that is frame 0). Left on the old poster the film popped visibly
   when it loaded — measured 12.93/255 mean difference against 0.48 for a
   matching still. `dawn-open.jpg` is this cut's own first frame, so the swap is
   invisible: 0.78/255.
   `dawn.jpg` stays exactly where it is — .plate--board and .plate--dusk are
   crops of it. (og.jpg was cut from it too until 14 Aug; it now comes from the
   film's own first frame, so dawn.jpg is load-bearing for those two plates
   only — but that is still two, so leave it alone.) This one line is the only place the
   opener's still is set (narrow has its own at the foot of the file). */
.plate[data-film="dawn"]{background-image:url("assets/dawn-open.jpg?v=1")}

/* 07:40 — in tight on the door and the airstairs, cooled toward morning */
.plate--board{
  background-image:url("assets/dawn.jpg?v=1");
  background-size:230%;background-position:31% 66%;
  filter:brightness(1.06) saturate(.72) hue-rotate(6deg) contrast(1.04);
  animation-duration:48s;animation-direction:alternate-reverse;
}
/* the fleet — the black jet in the air, its own frame */
.plate--flight{
  background-image:url("assets/flight.jpg?v=1");
  background-size:cover;background-position:50% 42%;
  filter:brightness(.92) saturate(.85) contrast(1.06);
  animation-duration:56s;
}
/* 19:20 — the same apron the day started on, wider and warmer */
.plate--dusk{
  background-image:url("assets/dawn.jpg?v=1");
  background-size:150%;background-position:64% 58%;
  filter:brightness(1.02) saturate(1.18) sepia(.16) hue-rotate(-8deg);
  animation-duration:50s;animation-direction:alternate-reverse;
}
@keyframes drift{
  from{transform:scale(1.04)}
  to  {transform:scale(1.11) translate3d(-1.1%,-0.7%,0)}
}
@media (prefers-reduced-motion:reduce){
  .plate--still{animation:none;transform:scale(1.04)}
}
/* The scrim is the only thing between film and type. It is a
   variable, not a fixed value, because a bright apron at dusk and
   a black cabin need very different amounts of holding down. */
.stage::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(to bottom,
    color-mix(in srgb, #000 calc(var(--scrim) * 90%), transparent) 0%,
    color-mix(in srgb, #000 calc(var(--scrim) * 42%), transparent) 38%,
    color-mix(in srgb, #000 calc(var(--scrim) * 96%), transparent) 100%);
  transition:background 1.1s cubic-bezier(.4,0,.2,1);
}

/* ============================ SCROLLERS ============================ */
main{position:relative;z-index:1}
.beat{
  min-height:100svh;
  display:grid;align-content:center;
  padding:calc(14svh + env(safe-area-inset-top)) var(--edge) calc(12svh + env(safe-area-inset-bottom));
}
/* THE TURN IS PUNCTUATION, NOT A BEAT. It holds a time stamp and one line —
   204px of content in a 760px viewport, 27% filled, which is the emptiest beat
   on the page by a wide margin and why it read as broken rather than as silent.
   A full viewport is the wrong measure for two lines, so it gets a shorter one
   and the void becomes proportionate to what it actually carries.
   Not lower than this: 26svh of that is the .beat padding (28 on narrow, see
   the media query), so 68 leaves ~42svh for the type and the headline still
   sets in three lines at 375px without touching the edges. */
#arrival{min-height:68svh}
.wrap{width:min(76rem,100%);margin-inline:auto}
.beat__in{max-width:var(--measure);position:relative}
.beat--right .beat__in{margin-left:auto}
.beat--wide .beat__in{max-width:46rem}

/* The full-bleed scrim is weakest in the middle band — which is exactly
   where the type sits. Thin light text over a busy plate then reads as
   "too pale", and the wrong instinct is to darken the colour. It needs
   a ground instead: a soft local wash that follows the text column and
   never shows an edge. */
.beat__in::before{
  content:"";position:absolute;inset:-9rem -8rem;z-index:-1;pointer-events:none;
  background:radial-gradient(70% 62% at 26% 50%,
    color-mix(in srgb,#000 58%,transparent) 0%,
    color-mix(in srgb,#000 26%,transparent) 42%,
    transparent 68%);
}
.beat--right .beat__in::before{background:radial-gradient(70% 62% at 74% 50%,
    color-mix(in srgb,#000 58%,transparent) 0%,
    color-mix(in srgb,#000 26%,transparent) 42%,
    transparent 68%)}
/* the cabin plate is already black — a ground on top of it would only
   turn the one quiet frame into mud */
body[data-beat="hour"] .beat__in::before{background:none}

/* SAME RULE, OPPOSITE SIGN — for the two beats that have no plate at all.
   The ground above is a DARKENER, and you cannot darken #03050A: over bare
   sky it contributes exactly nothing, so the type ends up on a mathematically
   flat field. Measured, the turn is 27% filled with 556px of void at 1280x760
   (every other beat is 53-79%), and flat + empty is what makes it read as a
   failed asset rather than as a silence. The eye takes texture for a surface
   and flatness for something missing.
   So: a faint pool of light instead of a wash of dark, in the night accent, a
   few percent at its centre. It does not light the beat — it just stops the
   field being dead behind the words.
   ⚠️ `blueprint` was written off here as "79% full of type, so no void to fix".
   Two things were wrong with that. It has no `.beat__in` AT ALL — `.beat--plan`
   stacks a head, a drawing and a caption straight into `.wrap` — so it never
   carried even the no-op darkener this comment credited it with. And the 79%
   was measured at one viewport: on a phone the beat is 47% full with 428px of
   void, because the drawing scales down to 335x85 while the box does not. It
   gets its own lift at the foot of the plan block, narrow only. Desktop sits at
   0.58-0.69, inside the normal band, and is left alone. */
body[data-beat="dark"] .beat__in::before,
body[data-beat="deck"] .beat__in::before{
  background:radial-gradient(76% 68% at 26% 50%,
    color-mix(in srgb,#7FA8C4 7%,transparent) 0%,
    color-mix(in srgb,#7FA8C4 3%,transparent) 45%,
    transparent 72%);
}
body[data-beat="dark"] .beat--right .beat__in::before,
body[data-beat="deck"] .beat--right .beat__in::before{
  background:radial-gradient(76% 68% at 74% 50%,
    color-mix(in srgb,#7FA8C4 7%,transparent) 0%,
    color-mix(in srgb,#7FA8C4 3%,transparent) 45%,
    transparent 72%);
}

/* THE GAP BEFORE THE TURN — `plans` (data-film="terms") was left out of the
   lift above on the reasoning that it is "69% full of type". That was measured
   at one tall viewport. On a wide, short window it is 54% full at 1920x975,
   and it is the beat immediately BEFORE the turn, which is also plate-less.
   The two voids stack: 214px at the foot of `plans` plus 183px above the
   turn's kicker is 397px of unbroken flat sky with no photograph anywhere in
   it — the page reads as having failed to load. That is the gap he pointed at.

   ⚠️ COPYING THE `dark` LIFT HERE DOES NOTHING, and it was tried: 7% of the
   accent spread over a 76%x68% pool on a 975px `beat--wide` box is invisible —
   two screenshots either side of it were indistinguishable. Same failure as
   the attitude indicator: correct on every axis and still ghosting in the void
   it was added to fill.
   ⚠️ THE LEVER IS GEOMETRY, NOT STRENGTH. Taking the beat off full height is
   what shows: at 74svh the whole ways list comes into frame together and the
   dead band drops from 397px to ~299px. min-height is a FLOOR, so narrow
   viewports — where the ways stack and the box is content-tall anyway — are
   untouched by it. The tighter, stronger pool below is the secondary help. */
#plans{min-height:74svh}
body[data-beat="terms"] .beat--right .beat__in::before{
  background:radial-gradient(58% 52% at 74% 50%,
    color-mix(in srgb,#7FA8C4 18%,transparent) 0%,
    color-mix(in srgb,#7FA8C4 8%,transparent) 45%,
    transparent 72%);
}

/* A white fuselage in full sun is the hardest ground there is: the gold
   kicker all but vanishes on it. These two plates get a deeper wash —
   the alternative is raising the scrim, which would flatten the one
   image that has to look expensive. */
body[data-beat="boarding"] .beat__in::before,
body[data-beat="aircraft"] .beat__in::before{
  background:radial-gradient(74% 66% at 26% 50%,
    color-mix(in srgb,#000 72%,transparent) 0%,
    color-mix(in srgb,#000 40%,transparent) 44%,
    transparent 70%);
}
body[data-beat="boarding"] .beat--right .beat__in::before,
body[data-beat="aircraft"] .beat--right .beat__in::before{
  background:radial-gradient(74% 66% at 74% 50%,
    color-mix(in srgb,#000 72%,transparent) 0%,
    color-mix(in srgb,#000 40%,transparent) 44%,
    transparent 70%);
}

/* ---------------- type ---------------- */
.kicker{
  font:400 .74rem/1 'Geist Mono',ui-monospace,monospace;
  letter-spacing:.18em;text-transform:uppercase;
  color:var(--accent);margin:0 0 1.1em;
  display:flex;align-items:center;gap:.7em;
}
.kicker::after{content:"";flex:1;height:1px;background:currentColor;opacity:.35;max-width:5rem}

h1,h2{
  font-family:'Newsreader',Georgia,serif;font-weight:300;
  line-height:1.04;letter-spacing:-.015em;margin:0 0 .5em;
  text-wrap:balance;
}
h1{font-size:clamp(2.6rem,1.6rem + 5.2vw,5.6rem)}
h2{font-size:clamp(2rem,1.3rem + 3.4vw,3.9rem)}
h1 em,h2 em{font-style:italic;color:var(--accent)}

.lede{font-size:clamp(1.05rem,.95rem + .5vw,1.3rem);color:var(--ink);opacity:.92;margin:0 0 1em}
.note{color:var(--dim);max-width:30rem;margin:0}
.note + .note{margin-top:.9em}

/* Hairline type always gets a ground under it — on film, thin light
   text with no shadow reads as "too pale" and the instinct is to
   darken the colour, which is never the actual fix. */
.beat h1,.beat h2,.beat .lede,.beat .kicker,.beat .note{
  text-shadow:0 1px 34px color-mix(in srgb,#000 62%,transparent),
              0 1px 3px  color-mix(in srgb,#000 34%,transparent);
}

/* ---------------- the spec strip ---------------- */
/* Three facts, one per line, the number inline with its label.
   They were a wrapping row of blocks — number over label — which fitted two
   and dropped the third onto its own line with a ragged edge above it. That
   was first read as a phone problem and fixed only there; it is not. This row
   lives in a ~350px text column at EVERY width (704–1054 of a 1280 viewport),
   so desktop wrapped 2+1 in exactly the same way. Measured, then looked at.

   The copy was always written as sentences — "90 min notice, most days", "1
   door between car and cabin" — so letting the number sit inline is closer to
   what it says than the stacked numerals ever were.

   ⚠️ No flex-wrap. In a column it does nothing while the height is auto, and
   the moment anything gives this a definite height it would silently start
   packing items into a second COLUMN instead. */
.spec{
  display:flex;flex-direction:column;gap:.15rem;margin:2.2rem 0 0;padding:0;list-style:none;
  border-top:1px solid var(--faint);padding-top:1.1rem;
}
.spec li{font:400 .82rem/1.9 'Geist Mono',ui-monospace,monospace;color:var(--dim)}
.spec b{font-weight:400;color:var(--ink);letter-spacing:.02em}

/* ⚠️ .spec once had a second, competing set of rules inside the big
   max-width:47.99em block at the foot of this file. A fix written up here
   silently lost to them — same specificity, earlier in source order — and
   looked like the CSS simply had no effect. Grep for ALL rules on a selector,
   media queries included, before adding one. Both sets are now merged above,
   so .spec is declared in exactly one place; keep it that way. */

/* ============================ THE COMPOSITE ============================
   Beat 11: the night city with the aircraft in it. Built to the Claude
   Design handoff — a 14s seamless loop where every layer is a pure
   function of one normalised clock. The runtime is NOT ported; d.js
   writes a handful of custom properties and everything reads off them.

   Authored in a fixed 1920×1080 space (--hs scales it), because every
   position below is in that space. Scaled to cover, not fit.
   ====================================================================== */
.plate--hero{background:#05070a}
.hero{
  position:absolute;left:50%;top:50%;
  width:1920px;height:1080px;margin:-540px 0 0 -960px;
  transform:scale(var(--hs,1));transform-origin:50% 50%;
  overflow:hidden;
}

/* 1 · city plate, carrying the camera float */
.hero__cam{position:absolute;inset:0;
  transform:scale(var(--camScale,1.09)) translate3d(var(--camX,0),var(--camY,0),0);
  transform-origin:50% 46%}
.hero__city{width:100%;height:100%;object-fit:cover;object-position:50% 52%;
  filter:brightness(.8) contrast(1.14) saturate(.9) blur(var(--cityBlur,1px))}

/* 2 · ambient haze */
.hero__haze{position:absolute;left:-20%;top:18%;width:140%;height:55%;
  background:radial-gradient(closest-side,rgba(120,150,185,.16),transparent 72%);
  filter:blur(40px);mix-blend-mode:screen;
  transform:translate3d(var(--hazeX,0),0,0)}

/* 3 · the aircraft group — position, scale and bank on different phases */
.hero__jet{position:absolute;left:470px;top:-330px;width:1330px;height:1995px;
  transform-origin:50% 44%;
  transform:translate3d(var(--jetX,0),var(--jetY,0),0) scale(var(--jetScale,1))
            rotate(var(--jetRot,0deg)) skewY(var(--jetSkew,0deg))}
.hero__air,.hero__ghost{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
.hero__air{filter:brightness(.62) contrast(1.16) saturate(.8) hue-rotate(-6deg)
  drop-shadow(0 30px 52px rgba(0,0,0,.7))}
.hero__ghost{z-index:-1;opacity:.14;
  filter:brightness(.62) contrast(1.16) saturate(.8) hue-rotate(-6deg) brightness(.45) blur(9px);
  transform:translate3d(var(--ghostX,1.6%),.6%,0)}
.hero__bounce{position:absolute;left:10%;top:44%;width:80%;height:22%;
  background:radial-gradient(closest-side,rgba(255,172,96,.30),transparent 70%);
  filter:blur(28px);mix-blend-mode:screen;opacity:var(--bounce,.8)}
/* the sheen is masked by the airframe, so it lights the metal and nothing else */
.hero__sheen{position:absolute;inset:0;mix-blend-mode:screen;opacity:var(--sheenO,.24);
  background:linear-gradient(104deg,transparent var(--sheenA,0%),
    rgba(226,238,255,.40) var(--sheenB,30%),transparent var(--sheenC,60%));
  -webkit-mask:url("assets/jet-cutout.png?v=1") center/contain no-repeat;
          mask:url("assets/jet-cutout.png?v=1") center/contain no-repeat}
.hero__exhaust{position:absolute;left:69%;top:39.5%;width:9%;height:3.4%;
  background:radial-gradient(closest-side,rgba(255,150,80,.40),transparent 70%);
  filter:blur(6px);mix-blend-mode:screen;transform:rotate(-6deg);opacity:var(--exhaust,.55)}
.hero__fan{position:absolute;left:66.3%;top:39.7%;width:6.8%;height:4.9%;
  transform:rotate(-6deg);border-radius:50%;overflow:hidden;mix-blend-mode:screen}
.hero__blades{position:absolute;inset:-25%;filter:blur(1.5px);
  background:repeating-conic-gradient(rgba(206,220,238,.30) 0deg 5deg,transparent 5deg 22deg);
  transform:rotate(var(--fan,0deg))}
.hero__hub{position:absolute;left:38%;top:38%;width:24%;height:24%;border-radius:50%;
  background:rgba(196,212,232,.30);filter:blur(2px)}
.hero__nav{position:absolute;display:block;border-radius:50%}
.hero__nav--wing{left:95.5%;top:44.5%;width:9px;height:9px;background:#31d158;
  box-shadow:0 0 23px 9px #31d158;opacity:.9}
.hero__nav--strobe{left:68%;top:26.5%;width:11px;height:11px;background:#fff;
  box-shadow:0 0 29px 11px #fff;opacity:var(--strobe,.03)}
.hero__nav--beacon{left:30%;top:55%;width:8px;height:8px;background:#ff3b30;
  box-shadow:0 0 21px 8px #ff3b30;opacity:var(--beacon,.05)}
.hero__streak{position:absolute;left:59.5%;top:26.5%;width:17%;height:3px;
  background:linear-gradient(90deg,transparent,rgba(200,225,255,.75),transparent);
  filter:blur(2.5px);mix-blend-mode:screen;opacity:var(--streak,0)}

/* 4 · foreground haze */
.hero__fore{position:absolute;left:0;bottom:4%;width:80%;height:30%;
  background:radial-gradient(closest-side,rgba(198,214,232,.05),transparent 70%);
  filter:blur(34px);mix-blend-mode:screen;
  opacity:var(--foreO,.2);transform:translate3d(var(--foreX,0),0,0)}

/* 5 · grade */
.hero__grade{position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg,rgba(4,7,11,.50) 0%,transparent 22%,
    transparent 62%,rgba(4,7,11,.55) 100%);
  box-shadow:inset 0 0 300px 70px rgba(2,4,7,.7)}

/* 6 · grain, jittered so it never sits still */
.hero__grain{position:absolute;inset:-20px;pointer-events:none;
  mix-blend-mode:overlay;opacity:.14;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
  background-position:var(--grainX,0) var(--grainY,0)}

@media (prefers-reduced-motion:reduce){
  .hero__grain{display:none}
  .hero__blades{animation:none}
}

/* ---------------- the cabin plan ----------------
   The one beat with no photograph. A drawing is ours, it is what an operator
   actually shows you, and the change of texture is the point — after five
   full-bleed films, a technical drawing on a plain ground lands. */
/* This beat carries a head, a drawing and a caption — three stacked blocks
   where every other beat carries one. It needs tighter air than the rest or
   the caption, which is half the point, falls below the fold. */
.beat--plan{
  align-content:center;
  padding-top:calc(7svh + env(safe-area-inset-top));
  padding-bottom:calc(5svh + env(safe-area-inset-bottom));
}
.plan__head{max-width:40rem;margin:0 0 clamp(.9rem,2vw,1.6rem)}
/* capped so the drawing and its caption are read together on a short
   laptop viewport — they are one statement, not two */
.plan{margin:0;width:100%;max-width:60rem}
.plan svg{width:100%;height:auto;display:block;overflow:visible}

.plan .p-hull{fill:none;stroke:var(--ink);stroke-width:2;opacity:.85}
.plan .p-hull-in{fill:none;stroke:var(--ink);stroke-width:1;opacity:.20}
.plan .p-thin{stroke:var(--ink);stroke-width:1;opacity:.28;fill:none}
.plan .p-seat{fill:none;stroke:var(--accent);stroke-width:1.6;opacity:.9}
.plan .p-table{fill:color-mix(in srgb,var(--accent) 16%,transparent);
  stroke:var(--accent);stroke-width:1;opacity:.75}
.plan .p-block{fill:none;stroke:var(--ink);stroke-width:1;opacity:.3;
  stroke-dasharray:3 4}
.plan .p-win{fill:none;stroke:var(--ink);stroke-width:1.2;opacity:.42}
.plan .p-door{stroke:var(--accent);stroke-width:3.5;opacity:.95;stroke-linecap:round}
.plan .p-lead{stroke:var(--ink);stroke-width:1;opacity:.22;fill:none}
.plan .p-label text{fill:var(--dim);font:400 13px/1 'Geist Mono',ui-monospace,monospace;
  letter-spacing:.1em;text-transform:uppercase;text-anchor:middle}
.plan .p-label text.p-below{dominant-baseline:hanging}

.plan figcaption{
  display:flex;flex-wrap:wrap;gap:.4rem 2.2rem;
  margin-top:clamp(.9rem,2vw,1.5rem);padding-top:.9rem;
  border-top:1px solid var(--faint);
  font:400 .82rem/1.7 'Geist Mono',ui-monospace,monospace;color:var(--dim);
}
.plan figcaption b{color:var(--accent);font-weight:400}

/* the drawing draws itself, once, and never owns whether it is readable */
.js .plan .p-hull{stroke-dasharray:3000;stroke-dashoffset:3000;
  transition:stroke-dashoffset 2.4s cubic-bezier(.4,0,.2,1)}
.js .plan.in .p-hull{stroke-dashoffset:0}
@media (prefers-reduced-motion:reduce){
  .js .plan .p-hull{stroke-dasharray:none;stroke-dashoffset:0;transition:none}
}

@media (max-width:47.99em){
  .plan .p-label text{font-size:19px}
  .plan figcaption{gap:.35rem 1.4rem;font-size:.76rem}

  /* ⚠️ "AIRSTAIR DOOR" AND "CLUB FOUR" COLLIDE HERE — measured, 3px of overlap
     at 375. The labels are centred on their lead lines at x=292 and x=429, so
     the distance between their centres is fixed at 38.9px once the drawing has
     scaled down, while the two half-widths add up to 41.7px.
     ⚠️ SHRINKING THE TYPE IS THE WRONG LEVER and was worked out rather than
     tried: clearing it that way needs a 21% reduction, and the line above
     already RAISES the label size to 19px because the drawing scales so far
     down. It would trade a collision for an illegible label.
     So the longest of the pair hangs off the END of its own lead instead of
     straddling it — which is how a technical drawing labels a feature anyway.
     Gap goes 3px overlapped to 22px clear. Only this one label moves: giving
     `club four` a start-anchor as well would push it into `two forward`, which
     measured 1.9px away. Desktop is untouched — it has room and he has
     approved it. */
  .plan .p-label text.p-lab-air{text-anchor:end}

  /* THE BLUEPRINT'S GROUND, narrow only. Same rule and same reasoning as the
     lift on `dark` and `deck`: over bare sky a darkener contributes nothing, so
     a plate-less beat with a big void reads as a failed asset rather than as a
     silence. Here the void is 428px of 812 with the drawing shrunk to a sliver.
     ⚠️ It hangs off `.wrap`, NOT `.beat__in` — this beat has no `.beat__in`.
     Centred at 50% because the plan is full-width, where the other lifts follow
     the text column to 26%/74%. */
  body[data-beat="blueprint"] .beat--plan .wrap{position:relative}
  body[data-beat="blueprint"] .beat--plan .wrap::before{
    content:"";position:absolute;inset:-7rem -8rem;z-index:-1;pointer-events:none;
    background:radial-gradient(72% 64% at 50% 50%,
      color-mix(in srgb,#7FA8C4 7%,transparent) 0%,
      color-mix(in srgb,#7FA8C4 3%,transparent) 45%,
      transparent 72%);
  }
}

/* ---------------- reach ----------------
   The fleet as a scale, not a catalogue. Three rules, and a hairline
   under each that runs as far as that aircraft does — so the page
   draws the range instead of listing it. No cards, no boxes. */
.reach{list-style:none;margin:2.3rem 0 0;padding:0}
.reach li{
  position:relative;display:grid;align-items:baseline;
  grid-template-columns:7.5rem 10rem 1fr;gap:.35rem 1.5rem;
  padding:1.05rem 0 1.2rem;border-top:1px solid var(--faint);
}
.reach li:last-child{border-bottom:1px solid var(--faint)}
.reach b{font:400 1.1rem/1 'Newsreader',Georgia,serif;color:var(--ink)}
.reach__t{font:400 .76rem/1 'Geist Mono',ui-monospace,monospace;
  letter-spacing:.09em;color:var(--accent)}
.reach__w{font-size:.86rem;color:var(--dim)}
.reach li::after{
  content:"";position:absolute;left:0;bottom:-1px;height:1px;
  width:var(--w);background:var(--accent);opacity:.6;
  transition:width 1.3s cubic-bezier(.33,0,.2,1);
}
/* The bar draws in — but the row is already readable, so nothing here
   decides whether the content can be seen. */
.js .reach li::after{width:0}
.js .reach.in li::after{width:var(--w)}
.js .reach.in li:nth-child(2)::after{transition-delay:.14s}
.js .reach.in li:nth-child(3)::after{transition-delay:.28s}

/* ---------------- the flight deck ----------------
   Same move as the cabin blueprint: no photograph, so the page draws. This beat
   is left-aligned with its empty space on the RIGHT, so the instrument goes
   there — the void gets a job instead of being a hole.
   Deliberately the blueprint's vocabulary and not a second one: --ink for
   structure, --accent for the thing that matters, hairline strokes, mono
   uppercase caption. The accent here is the night blue, because `deck` sits
   after the turn. */
.deck__grid{display:grid;gap:var(--gap);grid-template-columns:1fr}
@media(min-width:56em){
  .deck__grid{grid-template-columns:1.05fr .95fr;align-items:center}
}
.adi{margin:0;width:100%;max-width:24rem;justify-self:center}
.adi svg{width:100%;height:auto;display:block;overflow:visible}
.adi .a-bezel{fill:none;stroke:var(--ink);stroke-width:1.4;opacity:.42}
.adi .a-outer{opacity:.26}
.adi .a-horizon{fill:none;stroke:var(--accent);stroke-width:1.5;opacity:.55}
.adi .a-pitch{fill:none;stroke:var(--ink);stroke-width:1.2;opacity:.38}
.adi .a-tick{fill:none;stroke:var(--ink);stroke-width:1.2;opacity:.44}
.adi .a-index{fill:var(--ink);opacity:.32}
.adi .a-ptr{fill:var(--accent);opacity:.85}
/* the aircraft symbol is the only thing that never moves — that is the point,
   so it is the only stroke at full weight */
.adi .a-craft{fill:none;stroke:var(--accent);stroke-width:3.2;opacity:1;
  stroke-linecap:round}
.adi .a-hub{fill:var(--accent);opacity:1}
.adi figcaption{
  display:flex;flex-wrap:wrap;gap:.4rem 2rem;
  margin-top:clamp(.8rem,1.8vw,1.3rem);padding-top:.85rem;
  border-top:1px solid var(--faint);
  font:400 .8rem/1.7 'Geist Mono',ui-monospace,monospace;color:var(--dim);
}
.adi figcaption b{color:var(--accent);font-weight:400}
/* the bezel draws itself in, like the hull. r=112 so the circumference is ~704. */
.js .adi .a-outer{stroke-dasharray:720;stroke-dashoffset:720;
  transition:stroke-dashoffset 2.2s cubic-bezier(.4,0,.2,1)}
.js .adi.in .a-outer{stroke-dashoffset:0}
@media (prefers-reduced-motion:reduce){
  .js .adi .a-outer{stroke-dasharray:none;stroke-dashoffset:0;transition:none}
}

/* ---------------- ways to fly ---------------- */
.ways{margin:2.2rem 0 1.5rem}
.ways > div{
  display:grid;grid-template-columns:9.5rem 1fr;gap:.3rem 1.5rem;align-items:baseline;
  padding:1.05rem 0;border-top:1px solid var(--faint);
}
.ways > div:last-child{border-bottom:1px solid var(--faint)}
.ways dt{font:400 .78rem/1.3 'Geist Mono',ui-monospace,monospace;
  letter-spacing:.11em;text-transform:uppercase;color:var(--accent)}
.ways dd{margin:0;color:var(--dim);font-size:.92rem;max-width:34rem}
/* the caveat qualifies the plans, so it must not outrank them */
.ways + .note{font-size:.88rem;max-width:32rem}

/* ---- the plans get the fleet's language ----
   `.ways` and `.reach` were built as the same object — same grid rows, same
   hairline borders — but only the fleet ever got a drawn bar, so this beat read
   as a table while that one read as designed. He flagged it as feeling empty.
   Here the bar means how much of the aircraft is HELD for you: a single flight,
   a block of hours, or the whole year. Straight from the copy, not invented.
   ⚠️ Anchored RIGHT, not left like .reach, and deliberately allowed past 100%.
   This beat is beat--right, so its empty space is on the LEFT — growing the bars
   into it turns the void into the scale instead of leaving it a hole. The bars
   all finish on the same edge so they stay comparable. */
.ways > div{position:relative}
.ways > div:nth-child(1){--w:30%}   /* one flight */
.ways > div:nth-child(2){--w:72%}   /* ~25 hours a year, per the copy */
.ways > div:nth-child(3){--w:148%}  /* the whole year — runs out into the void */
.ways > div::after{
  content:"";position:absolute;right:0;bottom:-1px;height:1px;
  width:var(--w);background:var(--accent);opacity:.55;
  transition:width 1.3s cubic-bezier(.33,0,.2,1);
}
.js .ways > div::after{width:0}
.js .ways.in > div::after{width:var(--w)}
.js .ways.in > div:nth-child(2)::after{transition-delay:.14s}
.js .ways.in > div:nth-child(3)::after{transition-delay:.28s}

@media (max-width:47.99em){
  .reach li,.ways > div{grid-template-columns:1fr;gap:.15rem}
  .reach__t{margin-top:.15rem}
  /* --measure is 100% here, so the column IS the viewport and there is no void
     to grow into. Same ratios, rescaled so the longest bar ends at the edge
     instead of running off it. */
  .ways > div:nth-child(1){--w:20%}
  .ways > div:nth-child(2){--w:49%}
  .ways > div:nth-child(3){--w:100%}
}

/* ---------------- the turn ----------------
   Beat seven is the whole point of the piece: the same competence,
   but now someone is waiting at the other end. It gets more air
   than anything else and the type sits lower in frame. */
#arrival .beat__in{max-width:40rem}
#arrival h2{font-size:clamp(2.3rem,1.4rem + 4.2vw,4.6rem)}
/* ⚠️ uppercase matters now. This only ever held "03:14", where case is
   meaningless, so it never needed the transform every other label in this
   mono/accent treatment carries. The turn is a DISTANCE since the air
   ambulance came out, and "12 miles" was rendering lowercase against an
   otherwise uppercase page. Numbers are unaffected. */
.turn-time{
  font:400 .78rem/1 'Geist Mono',ui-monospace,monospace;
  letter-spacing:.34em;text-transform:uppercase;
  color:var(--accent);display:block;margin-bottom:2.4rem;
}

/* ---------------- closing ---------------- */
/* THE ENDING SHOWS ITS CITY. It always had the night plate behind it, but two
   things hid it: `--scrim:1`, and this section painting an opaque `--sky` over
   the stage. Transparent here, scrim down to .62, and the page closes on the
   same image it spent the whole night half inside.
   ⚠️ .close__grid is not .beat__in, so it never inherited the ground wash every
   other beat gets — it never needed one over pure black. Over the city it does:
   that band of the plate runs p50 12 but p95 160 and max 255 (real lights), and
   without a ground the .note copy measured about 1.5:1 against the bright spots.
   Vertical wash rather than a radial because this grid is full width, and a
   radial in a full-width column shows a straight edge — same reason the narrow
   query swaps them. */
#close{background:transparent;position:relative}
.close__grid{position:relative}
.close__grid::before{
  content:"";position:absolute;inset:-40svh -50vw;z-index:-1;pointer-events:none;
  background:linear-gradient(to bottom,
    transparent 0%,
    color-mix(in srgb,#000 40%,transparent) 26%,
    color-mix(in srgb,#000 56%,transparent) 50%,
    color-mix(in srgb,#000 40%,transparent) 74%,
    transparent 100%);
}
.close__grid{display:grid;gap:var(--gap);grid-template-columns:1fr}
@media(min-width:56em){.close__grid{grid-template-columns:1.1fr .9fr;align-items:start}}
/* just the mark now — no rule, no heading, nothing to read */
.name-note{display:grid;place-items:start center;align-content:center}
@media(min-width:56em){.name-note{place-items:center}}
/* Last frame on the page, so the glyph carries it rather than captions
   it. The negative indent optically aligns the ḏ with the text below —
   the letterform's own sidebearing sets it in otherwise. */
.glyph{
  font:300 clamp(5.5rem, 3rem + 13vw, 13rem)/.82 'Newsreader',Georgia,serif;
  color:var(--accent);display:block;
  margin:0 0 .1em -.055em;letter-spacing:-.02em;
}

.btn{
  display:inline-block;position:relative;
  font:400 .84rem/1 'Geist Mono',ui-monospace,monospace;letter-spacing:.14em;text-transform:uppercase;
  color:var(--sky);background:var(--ink);text-decoration:none;
  padding:1.05em 1.7em;border-radius:2px;
  transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .3s;
}
.btn:hover{transform:translateY(-2px)}
.btn--ghost{background:none;color:var(--ink);box-shadow:inset 0 0 0 1px var(--faint)}

/* Tap targets get their 44px from a pseudo-element, so padding the
   hit area never reflows the line it sits in. */
.btn::after,.sn-back::after,.brand::after{
  content:"";position:absolute;inset:50% 0 auto;
  transform:translateY(-50%);height:max(100%,44px);
}

/* ---------------- chrome ---------------- */
.nav{
  position:fixed;top:0;left:0;right:0;z-index:3;
  display:flex;align-items:center;justify-content:space-between;
  padding:max(1rem,env(safe-area-inset-top)) var(--edge) 1rem;
  pointer-events:none;
}
.nav > *{pointer-events:auto}
.brand{
  position:relative;display:flex;align-items:baseline;gap:.5em;text-decoration:none;
  font:300 1.35rem/1 'Newsreader',Georgia,serif;letter-spacing:.02em;
}

/* ---- THE WORDMARK RETREATS ----
   The nav is fixed and carries no ground, so any beat taller than the viewport
   scrolls its own copy straight through the wordmark. Walked on an iPhone 17
   Pro at 402pt and caught three times: the descender of "Operated," cutting
   through "Djet"; body copy struck clean across it on the fleet beat; and
   again on "should already be waiting, rather than found."

   The same fault was found and fixed on Plaisance first — the two share this
   file's nav almost line for line, which is why it was worth coming to look.
   ⚠️ But this is the LONDON demo and the two spines must never converge: this
   is only the header behaviour, and nothing about the argument on the page.

   A scrim was tried on Plaisance and rejected on the evidence: built from
   --sky it cleared the collisions but laid a dark band across the top of the
   OPENER, the one beat that never collided, because its copy sits well below
   the nav. That is painting over the photograph to solve a problem that beat
   does not have ([[feedback_dont_paint_over_his_photography]]).

   So the wordmark leaves instead. It is a title card: on the opener it sits on
   open sky doing real work, and by the second beat it has been read. Here the
   nav holds nothing else at all — no language pill, since London is EN-only —
   so past the opener the chrome is simply gone and the film is whole.

   ⚠️ Scoped to the phone, matching Plaisance. All three collisions were proven
   at 402pt; desktop was not walked, so it stays as approved rather than
   changed on an assumption ([[feedback_stay_in_requested_scope]]).

   ⚠️ Visibility is the DEFAULT and the class only takes it away
   ([[project_no_fullviewport_blend]]). If d.js never runs the brand is simply
   always there — the failure mode is the old behaviour, never a wordmark
   pinned invisible. */
/* ⚠️ NOT scoped to the phone, and the first version wrongly was. Desktop was
   assumed to clear because it was never walked; when it finally was, 1280x820
   put real glyph boxes through the wordmark at 69 of 175 scroll positions —
   and it is WORSE there, because the display type is far larger. At the
   opener "Djet BIGGIN HILL" sits buried under "we actually" and is barely
   findable. Measured with Range.getClientRects() over every text node, then
   looked at, because an element-box test called all 175 a collision and an
   assumption had already called them all clean. */
.brand{transition:opacity .45s ease,transform .45s ease}
body.past-open .brand{opacity:0;transform:translateY(-.4rem);pointer-events:none}
@media (prefers-reduced-motion:reduce){
  body.past-open .brand{transform:none}
}
.brand span{font:400 .62rem/1 'Geist Mono',ui-monospace,monospace;letter-spacing:.22em;
  text-transform:uppercase;color:var(--dim)}
.progress{
  position:fixed;left:0;top:0;height:2px;z-index:4;
  width:var(--p,0%);background:var(--accent);
  transition:width .18s linear,background-color 1.1s;
}

footer{position:relative;z-index:1;padding:0 var(--edge) calc(2rem + env(safe-area-inset-bottom));background:var(--sky)}
.foot__row{display:flex;flex-wrap:wrap;gap:.6rem 1.4rem;justify-content:space-between;
  border-top:1px solid var(--faint);padding-top:1.4rem;
  font:400 .78rem/1.6 'Geist Mono',ui-monospace,monospace;color:var(--dim)}
/* ⚠️ THE ONLY REAL CONTROL ON THE PAGE WITHOUT A TAP PAD — measured 90x15 at
   375, against the 44px minimum every other control here already meets. Padded
   with ::after and never with real padding: this link sits inside a flex row
   that wraps, so real padding would move the copyright line beside it. The
   pseudo-element is centred on the link and costs the layout nothing.
   (The 1x1 "Skip to the end" link is NOT a fault — it is a focus-reveal skip
   link and is meant to be invisible until tabbed to.) */
.foot__row a{position:relative}
.foot__row a::after{
  content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:100%;min-width:44px;height:44px;
}
.demo-note{margin:1.4rem 0 0;font-size:.82rem;color:var(--dim);max-width:46rem}
.demo-note strong{color:var(--ink);font-weight:500}

.sn-back{
  position:fixed;right:var(--edge);bottom:max(1rem,env(safe-area-inset-bottom));z-index:3;
  font:400 .72rem/1 'Geist Mono',ui-monospace,monospace;letter-spacing:.1em;
  text-decoration:none;color:var(--ink);opacity:.62;
  padding:.7em .9em;border-radius:2px;
  background:color-mix(in srgb,#000 42%,transparent);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
}
.sn-back:hover{opacity:1}

/* Not left:-9999px — an absolutely positioned element resolves against
   the viewport, so it escapes body's overflow clip and leaves the page
   genuinely scrollable sideways. Collapse it in place instead. */
.skip{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}
.skip:focus{position:fixed;width:auto;height:auto;margin:0;clip-path:none;
  left:var(--edge);top:1rem;z-index:9;background:var(--ink);color:var(--sky);padding:.7em 1em}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

/* ---------------- entrance ----------------
   The animation NEVER owns visibility. Without JS, or if a timeline
   never runs, everything is already on screen — the class only ever
   adds motion to something that is visible by default. */
.rise{transition:opacity .9s cubic-bezier(.4,0,.2,1),transform .9s cubic-bezier(.4,0,.2,1)}
.js .rise{opacity:0;transform:translateY(1.1rem)}
.js .rise.in{opacity:1;transform:none}
.js .rise:nth-child(2){transition-delay:.08s}
.js .rise:nth-child(3){transition-delay:.16s}
.js .rise:nth-child(4){transition-delay:.24s}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{transition-duration:.01ms !important;animation-duration:.01ms !important}
  .js .rise{opacity:1;transform:none}
  .plate{transition:none}
}

/* ---------------- narrow ---------------- */
@media (max-width:47.99em){
  :root{--measure:100%}
  .beat{padding-top:calc(16svh + env(safe-area-inset-top))}

  /* ⚠️ THE COMPOSITE HAD TO BE MADE CHEAPER OR THE TAB DIES. iOS Safari killed
     the page with "A problem repeatedly occurred" — first going down, then, once
     the film leak was capped, on the way back UP.
     The ghost is the worst offender per pixel: a SECOND copy of the 1330x1995
     cutout, carrying `blur(9px)` on top of four other filters, to sit at 0.14
     opacity. A blurred full-size layer costs a full-size backing store, and at
     14% behind the aircraft it is not perceptible on a 375px screen — it is a
     depth trick for a large display. Gone here, untouched on desktop.
     The air layer keeps its grade but loses the drop-shadow: a 52px shadow blur
     on a layer this size is the same expense again, for a shadow cast onto a
     night sky. */
  .hero__ghost{display:none}
  .hero__air{filter:brightness(.62) contrast(1.16) saturate(.8) hue-rotate(-6deg)}
  /* THE TURN, on a phone. 68svh still left it 24% filled here — 130px of type
     in 552, worse than the 39% it reaches on a desktop, because the headline
     sets smaller while the box does not shrink with it.
     ⚠️ Trimming the padding alone does NOTHING, which is worth knowing before
     reaching for it: min-height wins, and align-content:center simply
     re-centres the type in whatever is left. Measured — padding cut to 2svh
     held the section at 552px and the fill at 0.24, unchanged. min-height is
     the lever; the padding only has to get out of its way so the shorter
     height can actually be reached. Both together: 333px, fill 0.39, which is
     the desktop proportion exactly. */
  #arrival{
    min-height:41svh;
    padding-top:calc(11svh + env(safe-area-inset-top));
    padding-bottom:calc(11svh + env(safe-area-inset-bottom));
  }
  /* Its own portrait frame, not a crop of the landscape one — a 16:9
     plate squeezed into a phone can never show a whole aircraft, and the
     whole aircraft is the entire point of this opener. */
  .plate[data-film="dawn"]{background-image:url("assets/dawn-v.jpg?v=1");background-position:50% 62%}
  /* the portrait frame of the same aircraft, framed for a phone */
  .plate--board{background-image:url("assets/dawn-v.jpg?v=1");background-size:210%;background-position:38% 62%}
  .plate--dusk {background-image:url("assets/dawn-v.jpg?v=1");background-size:140%;background-position:58% 56%}
  .plate--flight{background-size:cover;background-position:50% 40%}
  /* The column is full width here, so a radial gets clipped by its own
     box and shows a straight edge top and bottom. A vertical wash that
     runs past both ends of the viewport cannot show one. */
  .beat__in::before,.beat--right .beat__in::before{
    inset:-40svh -50vw;
    background:linear-gradient(to bottom,
      transparent 0%,
      color-mix(in srgb,#000 30%,transparent) 26%,
      color-mix(in srgb,#000 46%,transparent) 50%,
      color-mix(in srgb,#000 30%,transparent) 74%,
      transparent 100%);
  }
  /* .spec used to be corrected here, with `gap:0 1.4rem` and `flex:1 1 8rem`.
     The 8rem basis was the actual cause of the 2+1 wrap, and it applied at
     every width, so the row is now built one-per-line in the base rule and
     there is nothing left to override on a phone. */
  .foot__row{flex-direction:column}
  /* Fixed, it lands on top of the body copy — and it is already offered
     twice in the closing section. Let it sit at the end of the page. */
  .sn-back{position:static;display:inline-block;margin:0 0 1.4rem var(--edge);
    background:none;backdrop-filter:none;-webkit-backdrop-filter:none;opacity:.7}
}
