/* ============================================================
   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 */
/* The lagoon is the brightest beat in the piece and the only one lit from
   above. It keeps the daylight gold rather than the night blue, and its scrim
   is the lightest of any photographic beat — hold turquoise down hard and it
   turns grey, which throws away the one frame that says Indian Ocean. */
body[data-beat="lagoon"]  { --sky:#0A2A3A; --scrim:.34; }
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. */
.plate[data-film="dawn"]{background-image:url("assets/dawn.jpg?v=1")}

/* ---- THE BEACON ----
   His note: the frame "still look still". It is a photograph of a parked
   aeroplane, and there is no film of this beat to reach for — so the motion has
   to come from the one thing on a parked jet that genuinely moves. The plate
   already renders the port navigation light on the wingtip; this makes it
   flash, at an anti-collision beacon's real rate of roughly 45 a minute.
   Nothing is painted over the photograph: the light is already there, this only
   lifts it.

   ⚠️ ANCHORED TO THE PHOTO, NOT THE SECTION. The plate is background-size:cover
   at background-position:52% 60%, so a percentage of the div slides off the
   wing the moment the window changes shape. .fx__fit reproduces the cover box
   in container units, and the general rule for a background-position of X% Y%
   is left:X%; top:Y%; translate:-X% -Y%. Measured once off the source file:
   the light sits at 1767,727 of 2000x1116 = 88.35% / 65.14%.

   ⚠️ Desktop only. The phone plate is dawn-v.jpg, a different photograph whose
   frame does not contain the wingtip at all. */
.plate[data-film="dawn"] .fx{position:absolute;inset:0;overflow:hidden;
  pointer-events:none;container-type:size}
.plate[data-film="dawn"] .fx__fit{position:absolute;left:52%;top:60%;
  translate:-52% -60%;--ar:1.7921;
  width:100%;height:100%;                          /* fallback, no cq units */
  width:max(100cqw,calc(100cqh * var(--ar)));
  height:max(100cqh,calc(100cqw / var(--ar)))}
/* 0.58% because that is what the lit part of the real lamp measures in the
   source — 12px of 2000. At 1% it read as a sticker stuck on the wing. */
.plate[data-film="dawn"] .beacon{position:absolute;left:88.35%;top:65.14%;
  width:.58%;aspect-ratio:1;translate:-50% -50%;border-radius:50%;
  background:#ff5230;opacity:0;animation:beacon 1.35s linear infinite}
/* the halo is sized in % of the DOT, not of the viewport, so it stays in
   proportion at any window shape instead of swelling on a wide screen */
.plate[data-film="dawn"] .beacon::after{content:"";position:absolute;
  inset:-170%;border-radius:50%;pointer-events:none;
  background:radial-gradient(closest-side,rgba(255,90,55,.5),rgba(255,70,35,0) 72%)}
/* a real beacon is a short hard flash in a long dark, not a slow throb */
@keyframes beacon{0%,88%,100%{opacity:0}90%,97%{opacity: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;
}
/* ---- THE THREE PLAISANCE-ONLY PLATES ----
   London has no equivalent of any of these, which is the whole reason the
   piece exists. Its dusk beat is gone: this argument does not run dawn-to-dusk
   across one day, it runs "already here" — so the day ends at night, once. */

/* the island hop — the one frame that says Indian Ocean without a caption.
   Kept wide and barely graded: the water is doing the work, and pushing
   saturation on a lagoon is how a real photograph starts looking generated. */
.plate--lagoon{
  background-image:url("assets/lagoon.jpg?v=1");
  background-size:cover;background-position:50% 46%;
  filter:brightness(.96) saturate(1.04) contrast(1.03);
  animation-duration:54s;
}
/* the last miles, flown. Warmed toward the afternoon it was generated in.
   ⚠️ Not London's heliPad — that was a dense high-rise skyline, which is the
   same failure as the alpine apron: right aircraft, wrong hemisphere. */
.plate--heli{
  background-image:url("assets/heli.jpg?v=1");
  background-size:cover;background-position:50% 52%;
  filter:brightness(.94) saturate(1.02) contrast(1.04);
  animation-duration:50s;animation-direction:alternate-reverse;
}
/* based here, not visiting. Held down harder than the rest because the beat
   is the quiet one and the town lights should be the brightest thing left. */
.plate--night{
  background-image:url("assets/night.jpg?v=1");
  background-size:cover;background-position:50% 56%;
  filter:brightness(.92) saturate(.96) contrast(1.06);
  animation-duration:58s;
}
@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 beacon holds steady rather than flashing — a flashing light is exactly
     what reduced-motion is asked for */
  .plate[data-film="dawn"] .beacon{animation:none;opacity:.9}
}
/* 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.
   ⚠️ `terms` and `blueprint` carry the same no-op darkener, and are deliberately
   left alone: they are 69% and 79% full of type, so they have no void to fix. */
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%);
}

/* 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%);
}

/* The three Plaisance plates all put a BRIGHT BAND exactly where the body
   copy sits — the night apron's floodlights, the lagoon's pale reef line, the
   rotary's sunlit sand. Caught on the close beat: the heading held but the
   note crossed the amber band and lost its ground entirely.

   ⚠️ The fix is a ground, never a paler ink and never a heavier scrim
   ([[feedback_hairline_type_needs_ground]]). Raising --scrim would flatten the
   whole photograph to rescue four lines of text — and on the lagoon it would
   turn turquoise to grey, which throws away the only frame that says Indian
   Ocean. Same wash as boarding/aircraft, which solve the same problem. */
body[data-beat="dawn"] .beat__in::before,
body[data-beat="night"] .beat__in::before,
body[data-beat="lagoon"] .beat__in::before,
body[data-beat="heli"] .beat__in::before{
  background:radial-gradient(76% 68% at 26% 50%,
    color-mix(in srgb,#000 78%,transparent) 0%,
    color-mix(in srgb,#000 46%,transparent) 46%,
    transparent 72%);
}
body[data-beat="dawn"] .beat--right .beat__in::before,
body[data-beat="night"] .beat--right .beat__in::before,
body[data-beat="lagoon"] .beat--right .beat__in::before,
body[data-beat="heli"] .beat--right .beat__in::before{
  background:radial-gradient(76% 68% at 74% 50%,
    color-mix(in srgb,#000 78%,transparent) 0%,
    color-mix(in srgb,#000 46%,transparent) 46%,
    transparent 72%);
}

/* ---------------- 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 ---------------- */
.spec{
  display:flex;flex-wrap:wrap;gap:0 2.4rem;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{display:block;font-weight:400;color:var(--ink);letter-spacing:.02em}

/* ============================ 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}
}

/* ---------------- 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}

/* ---- THE WORDMARK RETREATS ----
   The nav is fixed and carried no ground, so any beat taller than the viewport
   scrolled its own copy straight through the wordmark. Found on an iPhone 17
   Pro at 402pt, four separate times: "Djet" struck through by "answer is no.",
   wedged between SECURITY and IMMIGRATION on the terminal ladder, sitting on
   "Light" in the reach brackets, and laid across plain body copy on the plans
   beat.

   ⚠️ Scoped to the phone ON PURPOSE. All four collisions were proven at 402pt
   on a real device; desktop was never walked in that pass, so it is left
   exactly as he last approved it rather than changed on an assumption
   ([[feedback_stay_in_requested_scope]]). If the same overlap turns out to
   happen on a wide screen, lift this out of the media query — but look first.

   A scrim was the obvious fix and it was the wrong one. Built from --sky it
   read well over the ladder, but on the OPENER — the one beat that never
   collided, because its own copy sits well below the nav — it laid a dark band
   across the top of the dawn sky. 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 with nothing behind it and is doing real work. By the second beat it
   has been read, and every collision happened while the eye was somewhere else
   entirely. Retiring it costs nothing that was being looked at, and costs zero
   pixels of film — which a scrim never could.

   The FR·EN pill stays: it is a control, not a credit, and it already carries
   its own backdrop, which is exactly why it stayed legible through all four
   collisions while the brand beside it did not.

   ⚠️ Visibility is the DEFAULT and the class only takes it away
   ([[project_no_fullviewport_blend]]). If p.js never runs, or a frozen
   timeline stalls it, the brand is simply always there — the failure mode is
   the old behaviour, never a wordmark pinned invisible. */
/* ⚠️ NOT scoped to the phone any more, and it was wrong to be. Desktop was
   left alone on the grounds that it had not been walked — correct caution,
   but it then went unwalked. When it was finally measured at 1280x820 the
   LIVE page struck the wordmark at 12 scroll positions: "The base", "The
   aeroplane is", "already here." all passing straight through it. Wider
   screens are worse, not better, because the display type is larger. */
.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{
  position:relative;display:flex;align-items:baseline;gap:.5em;text-decoration:none;
  font:300 1.35rem/1 'Newsreader',Georgia,serif;letter-spacing:.02em;
}
.brand span{font:400 .62rem/1 'Geist Mono',ui-monospace,monospace;letter-spacing:.22em;
  text-transform:uppercase;color:var(--dim)}

/* ---------------- FR · EN ----------------
   French is the default because this is the MAURITIAN operator, which is the
   entire reason the piece exists. Biggin Hill stays English for the same reason
   in reverse. Both columns ship in the markup and CSS chooses — no fetch, no
   flash, and view-source shows the page is genuinely bilingual.

   `display:revert` and not `inline`: these spans wrap headings, list items and
   figcaption cells, so forcing one display type would break the ones that are
   not inline to begin with. */
.t-fr,.t-en{display:none}
body.lang-fr .t-fr{display:revert}
body.lang-en .t-en{display:revert}

.lang{
  position:relative;display:grid;grid-template-columns:1fr 1fr;
  --lang-pad:3px;padding:var(--lang-pad);
  border:1px solid var(--faint);border-radius:999px;
  background:color-mix(in srgb,var(--sky) 62%,transparent);
  backdrop-filter:blur(6px);
  pointer-events:auto;            /* .nav sets none so the films stay draggable */
  flex:none;                      /* or the nav row squeezes it below its labels */
}
.lang input{position:absolute;width:0;height:0;opacity:0;pointer-events:none}
.lang label{
  position:relative;z-index:2;cursor:pointer;text-align:center;
  padding:.34rem .62rem;border-radius:999px;
  font:400 .58rem/1 'Geist Mono',ui-monospace,monospace;letter-spacing:.18em;
  color:var(--dim);transition:color .3s ease;
}
.lang input:checked + label{color:var(--sky)}
.lang input:focus-visible + label{outline:2px solid var(--accent);outline-offset:2px}
/* the moving pill, placed before it is allowed to animate */
.lang::before{
  content:"";position:absolute;z-index:1;
  top:var(--lang-pad);bottom:var(--lang-pad);left:var(--lang-pad);
  width:calc((100% - var(--lang-pad) * 2) / 2);
  border-radius:999px;background:var(--ink);
}
body.lang-en .lang::before{transform:translateX(100%)}
.lang.ready::before{transition:transform .42s cubic-bezier(.2,.8,.2,1)}
/* ⚠️ Tap target padded with ::after, never with real padding — real padding
   reflows the nav row and pushes the brand off its baseline. */
.lang label::after{
  content:"";position:absolute;left:0;right:0;top:50%;
  height:max(100%,44px);transform:translateY(-50%);
}
@media (prefers-reduced-motion:reduce){
  .lang.ready::before{transition:none}
}
.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)}
.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;
  display:inline-flex;align-items:center;gap:.5em;
  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 svg{flex:none}
.sn-back:hover{opacity:1}
/* Fixed at every width, so on a phone it floats over copy that runs the full
   width of the screen. p.js measures that and gets it out of the way. Opacity
   only — nothing moves, so there is nothing to suppress for a calm reader. */
.sn-back{transition:opacity .28s ease}
.sn-back.tuck{opacity:0;pointer-events:none}

/* 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 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. Same answer London gave, and
     the same dimensions: a separate 9:16 generation at 1536x2752, down to
     1200x2150. Whole aeroplane across the lower half, dawn sky towering above
     it — which is also where the type sits, so the sky is doing two jobs.

     This is not a nicety. The cover is what every screen at or under 860px
     sees, so on a phone these two rules ARE the opener. */
  .plate[data-film="dawn"]{background-image:url("assets/dawn-v.jpg?v=1");background-position:50% 60%}
  /* dawn-v.jpg is a different photograph and its frame does not contain the
     wingtip, so there is no light here to lift. The beacon would be a red dot
     sitting on empty apron. */
  .plate[data-film="dawn"] .fx{display:none}
  /* the same portrait frame, in tight on the door — the cane stands directly
     behind the airstairs here, which is what boarding has to show */
  .plate--board{background-image:url("assets/dawn-v.jpg?v=1");background-size:210%;background-position:50% 66%}
  /* Their own 9:16 frames, like the opener — not crops of the landscape ones.

     These three used to carry background-size:170/180/175%, which LETTERBOXED
     them: a percentage background-size sets the WIDTH, so a 2000x1116 frame at
     170% of a 375px viewport paints 638x356 and fills 44% of an 812px-tall box —
     a band of photograph with bare --sky above and below. Covering the height
     needed ~388%, so every one of those values was broken, and it shipped.

     `cover` fixed the gap but cost the frame: it shows only the centre 26% of a
     landscape plate, which turned the lagoon into a mid-fuselage crop. That is
     the same limitation the opener had, and the same answer applies — generate
     for the phone rather than crop for it. 1536x2752 down to 1200x2150, six
     credits, composed subject-in-the-middle with sky or water for the type.

     ⚠️ The letterbox was found by LOOKING, not measuring. Every mobile pass read
     back "170%", "no overflow", "inside the viewport" — all true. The plate sits
     in .stage{overflow:hidden}, so a letterboxed plate produces no overflow and
     no layout fault to measure. Only a painted phone screenshot showed it. */
  .plate--lagoon{background-image:url("assets/lagoon-v.jpg?v=1");background-size:cover;background-position:50% 50%}
  .plate--heli{background-image:url("assets/heli-v.jpg?v=1");background-size:cover;background-position:50% 50%}
  .plate--night{background-image:url("assets/night-v.jpg?v=1");background-size:cover;background-position:50% 52%}
  .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{gap:0 1.4rem}
  .spec li{flex:1 1 8rem}
  .foot__row{flex-direction:column}
  /* Stays fixed here, like every other demo on the board. It does land on
     the copy at this width — measured ink, not block boxes, at 45% of scroll
     positions on the right and 62% on the left, which is why it keeps the
     right — so p.js tucks it away for the passages where type is under it.
     A little more presence than on desktop: it sits on film, not on a
     margin. */
  .sn-back{opacity:.78}
  /* Fixed furniture has to be given its own room or it sits on the end of
     the document. Without this the closing disclaimer runs under the link
     for the whole last screen — measured 100% of that beat tucked, so the
     way out disappeared exactly where a reader finishes and wants it. The
     pill occupies 44px off the bottom; this clears it with room to spare. */
  footer{padding-bottom:calc(4.25rem + env(safe-area-inset-bottom))}
}
