/* ============================================================================
   RESPONSIVE v72 - the desktop and mobile journeys in one document.

   WHY THEY COULD NOT SIMPLY BE CONCATENATED
   Both prototypes were written standalone and both claim the ids stage, bg, cv,
   txt, seawrap, range and state. Each is namespaced instead: every id in its
   markup and every id selector in its CSS carries a prefix, d- or m-.
   The desktop module reaches the DOM through exactly one helper -
   `$ = document.getElementById` - so prefixing that single function covers every
   lookup it makes, including the ids mkTx() stamps at runtime. The mobile module
   calls getElementById directly, so those call sites were rewritten.

   WHY ONLY ONE SIDE BOOTS
   Each module's IIFE became a named boot function. The shell boots one, and
   boots the other only if the viewport crosses the breakpoint - so the asset set
   of the layout you are not looking at is never fetched. That matters: the two
   sets are 8.4 MB and 3.7 MB. Both modules also own a scroll driver, a HUD and
   global wheel/key listeners, so each one's setTarget and its layout function
   return early unless it is the active one; otherwise the hidden journey would
   scroll along with the visible one and fight it for the wheel.

   ASSETS ARE FILES, NOT BASE64. v65 was 11.4 MB because 211 assets were inlined;
   externalised it is 130 KB. Two inlined asset sets in one document was never
   going to be shippable, and the folder layout is what the WordPress theme wants
   anyway (assets/js, assets/img).

   BREAKPOINT: aspect first, width second. A 900 px portrait tablet wants the
   mobile composition and an 800 px landscape window does not.
   ============================================================================ */
@font-face{font-family:Morabba;font-style:normal;font-weight:500;font-display:block;src:url(../fonts/b3f74e519b4f.otf) format('opentype')}
@font-face{font-family:Morabba;font-style:normal;font-weight:800;font-display:block;src:url(../fonts/5f0d9f5a300c.otf) format('opentype')}
@font-face{font-family:Morabba;font-style:normal;font-weight:900;font-display:block;src:url(../fonts/b05b67894901.otf) format('opentype')}

/* ---- the loading gate -----------------------------------------------------
   Both journeys decode a lot before they can draw a single frame: the desktop
   set is 208 sprites and the mobile one 112. Until now that happened behind a
   blank stage with a HUD that still said "decoding" - so a missing asset folder
   and a slow disk looked exactly the same, and neither was recoverable.
   The gate counts every Image the modules construct, shows real progress, and
   refuses to fail silently: anything that does not arrive is named. */
/* v30: THE GATE IS WHITE. It was #0d0d0f, and it is the full-viewport layer that sits
   over the journey until a module reports ready — so between the hero transition (which
   resolves to white) and the first frame of the truck the page went hard black and then
   faded. That is the "black background before the truck section" the client reported; it
   was never a section, it was this. White matches both sides of it, so there is nothing
   left to notice. */
.journey #fj-load{position:fixed;inset:0;z-index:50;display:flex;flex-direction:column;
 align-items:center;justify-content:center;gap:18px;background:#fefefe;
 color:#4f5863;transition:opacity .45s ease;
 font:400 14px/1.7 Morabba,Vazirmatn,Tahoma,system-ui,sans-serif}
.journey #fj-load[hidden]{display:none}
.journey #fj-load.gone{opacity:0;pointer-events:none}
.journey #fj-bar{width:min(260px,58vw);height:2px;background:rgba(15,20,25,.12);
 border-radius:2px;overflow:hidden}
.journey #fj-fill{height:100%;width:0;background:#d89b3c;transition:width .25s ease}
.journey #fj-pct{color:#8b8d94;font:11px/1 ui-monospace,SFMono-Regular,monospace;direction:ltr}
.journey #fj-load .fj-strip{background:rgba(255,255,255,.94)}
.journey #fj-err{max-width:min(420px,84vw);color:#e0a04a;text-align:center;display:none;
 font-size:12.5px;line-height:1.8}
/* once the journey is running, a failure notice moves out of the gate and becomes
   a strip over it - visible, but not a wall */
.journey #fj-load.fj-quiet{background:transparent;justify-content:flex-start;pointer-events:none}
.journey #fj-load.fj-quiet #fj-bar, .journey #fj-load.fj-quiet #fj-pct, .journey #fj-load.fj-quiet>div:first-child{display:none}
.journey #fj-err.fj-strip{margin-block-start:12px;background:rgba(254,254,254,.94);
 padding:10px 14px;border-radius:8px;max-width:min(560px,92vw)}
/* off-screen, not display:none - the parser and the accessibility tree both keep
   it, which is the point: the desktop sea copy is painted on canvas and would
   otherwise not exist as text at all */
.journey .fj-seo{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);
 clip-path:inset(50%);white-space:nowrap}
.journey__semantic{position:absolute!important;width:1px!important;height:1px!important;
 margin:-1px!important;padding:0!important;overflow:hidden!important;clip:rect(0 0 0 0)!important;
 clip-path:inset(50%)!important;white-space:normal!important}



.journey .fj-root{position:fixed;inset:0}
.journey .fj-root[hidden]{display:none}

/* ---- desktop module ---- */



.journey .stage{position:relative;width:1440px;height:671px;overflow:hidden;background:#fefefe;
 transform-origin:center;direction:ltr}
/* three layers, because the copy has to be real DOM AND still pass behind the truck:
   #d-bg  the dark band and its markings        z 1
   #d-txt the copy                              z 2
   #d-cv  the vehicles - transparent above them z 3   */
.journey #d-bg, .journey #d-cv{position:absolute;left:0;top:0;width:1440px;height:671px}
.journey #d-bg{z-index:1} .journey #d-cv{z-index:3}
/* v34: the readout is a small instrument, not a line of text - tabular figures so the number
   never jitters, the unit demoted to a caption, and a hairline tying them together. */
.journey #d-speed{position:absolute;right:40px;left:auto;top:28px;z-index:8;opacity:0;color:#0e1013;
 display:flex;align-items:baseline;gap:9px;transition:opacity .25s ease}
.journey #d-speed b{font:600 26px/1 ui-monospace,SFMono-Regular,monospace;letter-spacing:.01em;
 font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}
.journey #d-speed i{display:block;width:1px;height:15px;background:rgba(14,16,19,.22);align-self:center}
.journey #d-speed span{font:500 10px/1 ui-monospace,SFMono-Regular,monospace;letter-spacing:.3em;
 color:rgba(14,16,19,.5)}

/* ---- the copy layer -------------------------------------------------------------------
   Everything readable is REAL DOM inside the stage, not canvas paint: selectable, copyable,
   and in the markup for search engines. The stage carries the fit() transform, so these are
   positioned in the same 1440x671 space the canvas draws in.
   Each block is two stacked copies of the same text - the settled one and an accent one that
   shows only through a narrow moving window. That is the same reveal the canvas version had,
   rebuilt with clip-path. The accent copy is aria-hidden and unselectable so it never lands in
   a copy-paste or in a screen reader. */
/* v38: the wordmark is TWO copies of the same word at the same place. The light one sits under
   the band (z 0), so the asphalt crops it. The dark one sits over the band (z 2) and is clipped
   to start exactly at the band's top edge. The letterforms therefore run continuously off the
   white and onto the road, changing tone at the kerb instead of stopping at it. */
/* the sea sits UNDER everything: the road, the copy and the vehicles all draw over it */
.journey #d-seawrap{position:absolute;inset:0;z-index:0;opacity:0;overflow:hidden}
.journey #d-seabg{position:absolute;inset:0;background:#02143a url(../img/92fdd5f23158.webp) center/cover no-repeat;
 /* v48. MEASURED, not eyeballed: the reference's open water away from the vessel is rgb 2/38/95
    with bright wave highlights (p90 on blue = 110). Ours was 1/28/64, p90 69 - about a third
    darker and flat, which is the client's "around the ship it is black". Now that the clip is
    masked into the wake, the plate is the only thing out there, so the plate carries the grade. */
 filter:brightness(2.25) saturate(0.58)}
/* v52 - WHY THE OCEAN STILL WASN'T RIGHT, measured against the client's two new frames.
   Their open water:  mean rgb 42/64/110, luma std 20.7, p10/p90 45/99   (bow frame)
                      mean rgb 32/54/95,  luma std 10.6, p10/p90 49/71   (late frame)
   Ours at v51:       mean rgb  0.3/34/94, luma std  7.5, p10/p90 33/54

   Two separate faults, and only one of them is a grade.
   1. OUR RED CHANNEL IS ZERO. Their water carries 32-42 of red; ours carries 0.3. That is not
      a brightness problem, it is the plate: §5 recorded that this plate came from the poster
      frame of their clip, which measures effectively monochrome (7/7/8), and the blue is a TINT
      we laid over it. A tint has no red in it, so no filter can put red back. Desaturating and
      brightening gets red to about 15 and no further - swept eight combinations to confirm.
      The rest is added as a WASH, which is honest about what it is.
   2. OUR WATER HAS NO STRUCTURE. Their luma spread is 10-21; ours is 7.5, and `contrast()` only
      stretches texture that is already there. The plate is smooth, so the waves have to be
      generated: #d-seatexA/B carry a seamless multi-octave noise tile built once at boot and
      blended `overlay`, which lightens crests and darkens troughs - real wave structure rather
      than a sheen.

   BOTH OF THESE ARE STAND-INS. The proper fix is the same class of blocker as the wake clip: a
   licensed dark AERIAL OCEAN PHOTOGRAPH to replace the plate. With a real photograph the wash
   and the noise layers should both come out. */
/* v54: the wash was there to put red back into a plate that had none. The generated plate is
   built from the reference's own colour ramp, so it carries red at every level and the wash is
   no longer needed. Kept at zero rather than deleted, so it is one number to bring back. */
.journey #d-seawash{position:absolute;inset:0;background:rgba(74,98,142,0)}
.journey #d-seatexA, .journey #d-seatexB{position:absolute;inset:-20%;mix-blend-mode:overlay;background-repeat:repeat}
.journey #d-seatexA{background-size:186px 186px;opacity:.13;animation:texA 53s linear infinite}
.journey #d-seatexB{background-size:620px 620px;opacity:.08;animation:texB 91s linear infinite}
@keyframes texA{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(-300px,-210px,0)}}
@keyframes texB{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(480px,-960px,0)}}
@media (prefers-reduced-motion:reduce){.journey #d-seatexA, .journey #d-seatexB{animation:none}}
/* v50 GRADE. v48 set brightness 1.44 to hit the reference's open water. That number was solved
   on a QA frame in which the wake clip had never decoded (§4z FINDING 2), so it was compensating
   for light the clip was supposed to be adding. With the clip rendering, plate and blend stacked:
   measured open water was 1.6/45.6/110.6 with blue p90 128 against the reference's 2/38/95 / 110.
   MEASURED AGAIN, and the first correction was itself wrong. Sampling anywhere inside x 135-1278
   at SEA 70 % is sampling the WAKE ELEMENT, which is 1143 px wide there - the readings that said
   "16 % too bright" were taken on water the wake was lighting. Clear of it (x 0-110 and
   1310-1440) v49 measures 1.3/41.2/104.7 with blue p90 124 against the reference's 2/38/95 and
   110: over by about a tenth, not a sixth. The final number is 1.62 rather than something under
   1.44 because the swell layer below blends `overlay`, which darkens a base this dark - the two
   have to be solved together. Swept in the browser at SEA 72 %: 1.62 / 0.38 lands open water on
   1.3/35.2/97.5 against the reference's 2/38/95, and lifts the luma standard deviation from 5.7
   to 7.3, so the water has visible wave structure instead of being a flat sheet. Highlights sit
   a little hot (p90 125 against 110) - that is the swell doing the thing the client asked for,
   and it is the first number to pull back if the sea ever reads busy.
   TWO RULES FOR THE NEXT PASS: sample OUTSIDE the wake element, and sample with the video
   PLAYING. Both were broken before v50 and both produced confident, wrong readings. */
/* v50 SWELL. The client: "the whole sea has to have waves and move, and it is only the ship's
   movement that opens the wake out of the sea itself." The plate is one still, so the swell is
   made by INTERFERENCE rather than by a second clip: the same decoded WebP is painted twice at
   different scales and drifted in opposite directions on a long time-based loop, and where the
   two texture copies slide across each other the highlights travel. It reads as open water and
   costs two composited transforms - no extra bytes, because #d-seabg2 takes its background-image
   from #d-seabg at boot, so the WebP is decoded once. The hull's own wake is untouched: it stays
   on #d-wakev, pinned to the ship, which is the whole point of §4z. */
.journey #d-seapar{position:absolute;inset:-16% -12%;will-change:transform}
.journey #d-seabg2{position:absolute;inset:0;background-size:cover;background-position:center;
 opacity:.06;mix-blend-mode:overlay}
.journey #d-seabg{animation:swellA 41s ease-in-out infinite}
.journey #d-seabg2{animation:swellB 67s ease-in-out infinite}
@keyframes swellA{0%,100%{transform:scale(1.06) translate3d(0,0,0)}
                  50%    {transform:scale(1.06) translate3d(-1.1%,-1.7%,0)}}
@keyframes swellB{0%,100%{transform:scale(1.15) translate3d(0,0,0)}
                  50%    {transform:scale(1.15) translate3d(1.5%,2.1%,0)}}
/* the swell is ambient, not informative - it is the first thing to go */
@media (prefers-reduced-motion:reduce){.journey #d-seabg, .journey #d-seabg2{animation:none}}
/* v49. `Sea-wave.mp4` is NOT ocean footage. Pulled a frame: it is a top-down ship WAKE on pure
   black - a bright foam trail with the bow's V at the bottom, apex at 0.917 of its height and
   centred at 0.512 of its width, mean luminance 9.7. It is a SPRITE, meant to be screened over
   dark water and aligned to the hull. v43-v45 laid it over the whole frame as weather; v46-v48
   masked it into lobes. Both were wrong: it just has to be placed on the ship. `screen` deletes
   its black, so it needs no mask beyond a fade where the trail runs off the top. */
/* the blend has to sit on the MASKED element, not on the video inside it: a mask creates a
   stacking context, so `mix-blend-mode` on the <video> blended only against its own transparent
   group and the clip's black stayed black, painting the whole sea out. */
.journey #d-wakev{position:absolute;overflow:hidden;mix-blend-mode:screen;
 -webkit-mask-image:linear-gradient(to top,#000 0,#000 76%,transparent 100%);
         mask-image:linear-gradient(to top,#000 0,#000 76%,transparent 100%)}
.journey #d-wakeh{position:absolute;inset:0}
.journey #d-seavid{position:absolute;left:0;top:0;width:100%;height:100%;object-fit:fill}
.journey #d-markwrap{position:absolute;inset:0;z-index:0}
/* same layer as #d-txt, so DOM ORDER decides which paints on top. It is declared BEFORE #d-txt on
   purpose: rendered after, the giant word sat over the service copy and buried it. */
.journey #d-markroad{position:absolute;inset:0;z-index:2}
.journey #d-markd{color:#171a1e}
.journey #d-markd .acc{color:#2f3396}
.journey #d-txt{position:absolute;inset:0;z-index:2;perspective:1400px}
.journey .tx{transform-origin:right center;backface-visibility:hidden}
/* clip-path crops at the BORDER BOX and Persian ink sits outside it - the dot of خ rides above
   the line box, ی and ج hang below. So the text needs breathing room inside the box.

   v40 FIX: v39 put that padding on the WRAPPER. The accent copy is position:absolute with
   inset:0, which resolves against the padding box - so its text started 34 px ABOVE the settled
   copy's. That is the client's "the texts fall on top of each other in pieces": two copies of
   every string, offset by exactly the padding. The padding now goes on BOTH inner copies, and
   its size is per block (--pad) because 34 px is right for a 22 px title and nowhere near enough
   for a 320 px wordmark. */
.journey .tx{position:absolute;right:0;top:0;direction:rtl;text-align:right;will-change:transform}
.journey .tx .base, .journey .tx .acc{padding:var(--pad,30px) 0;margin:0;box-sizing:content-box}
.journey .tx .acc{position:absolute;left:0;right:0;top:0;user-select:none}
/* v37 FIX: the accent copy set its colour on the WRAPPER, but .svc h3 / .svc .d / #d-mark all
   set their own colour on the inner elements and won every cascade - so the accent copy was
   painted in the settled colour and the blue reveal was invisible. It has been dead since the
   canvas version moved into the DOM in v35. These selectors outrank them. */
.journey .tx .acc, .journey .tx .acc h3, .journey .tx .acc p{color:#4f52e0}
.journey #d-mark .acc{color:#4f52e0}
.journey #d-mark{font-weight:900;line-height:1.02;font-family:Morabba,Vazirmatn,Tahoma,system-ui,sans-serif;
 color:#e7e7e9;white-space:nowrap}
.journey .intro p{font:500 17px/1.75 Morabba,Vazirmatn,Tahoma,system-ui,sans-serif;
 color:rgba(244,244,245,.72);margin:0 0 16px}
.journey .svc h3{font:800 22px/1.2 Morabba,Vazirmatn,Tahoma,system-ui,sans-serif;color:#f4f4f5;
 margin:0;white-space:nowrap}
.journey .svc h3{margin:0 0 10px}
.journey .svc .d{font:500 13.5px/1.62 Morabba,Vazirmatn,Tahoma,system-ui,sans-serif;
 color:rgba(244,244,245,.6);margin:0}
.journey .hud{position:absolute;left:14px;right:14px;bottom:10px;display:flex;gap:14px;align-items:center;z-index:9;
 background:rgba(14,16,19,.9);padding:9px 12px;border-radius:8px;color:#fff;
 font:12px/1 ui-monospace,SFMono-Regular,monospace}
.journey .hud input[type=range]{flex:1;accent-color:#d89b3c}
.journey .hud b{min-width:250px;color:#f3bd64;font-weight:600;letter-spacing:.02em}

/* ---- mobile module ---- */
/* ============================================================================
   MOBILE v71.

   WHAT CHANGED FROM v66, AND WHY
   1. THE MIRROR IS GONE. v66 reflected the whole composition on the reasoning
      that an RTL build runs right-to-left. That confused DIRECTION OF TRAVEL with
      REFLECTION. The stack belongs on the left and the machine drives right-to-
      left to reach it - the motion carries the direction, the layout does not
      have to be flipped to say the same thing. Everything below is in the
      reference's own coordinates.
   2. The machine now ARRIVES. `machine-empty` (frame-mb_df) is the same render
      with an empty spreader, so the drive-in can be shown with the machine
      genuinely not yet carrying anything - and the white container can sit on
      the stack until it is lifted, instead of existing twice.
   3. The truck DESCENDS into the top view and stops facing the machine, the
      container is set on it, and then only the truck moves.
   4. The truck's white container MERGES into a white bay on the vessel's deck.
   5. Sea and clouds are in, from the client's own assets.

   REGISTRATION - every placement below is solved, not nudged:
     machine body, reference 3.4 s   x 276..450 of 486   (0.3580 W)
     machine body, seq frame_000     x 600..955 of 1240
       -> sequence frame drawn at 1.2506 W, origin x -0.0372 W
       -> its ground line (728/880) falls on 0.3846 H, and the horizon measured
          separately on the page's clear left margin is 0.3812 H. 3.7 px apart on
          a 1065 px box, from two unrelated measurements.
     machine-empty body               x 519..832 of 1080
       -> drawn at 0.9878 of the sequence frame, offset +0.0092 / +0.0080.
          Body heights agree to 1.5 %, so it is the same camera.
     crane-* sprites = turn/frame_48 in layers at 1016/1240. Their union scaled
          by 1.2205 lands on frame_48's own content box exactly.
     seated container 0.184 W x 0.391 H   vessel white bay 0.108 x 0.1057 of hull
       -> at hull width 1.0 W the bay is 0.108 W x 0.517 W. Aspects 0.217 and
          0.209 - 4 % apart, so the merge is a real alignment and not a dissolve.
   ============================================================================ */




.journey #m-stage{position:var(--fj-pos,fixed);inset:0;overflow:hidden;background:#fefefe;direction:ltr}
.journey #m-bg, .journey #m-cv{position:absolute;left:0;top:0;width:100%;height:100%}
.journey #m-bg{z-index:1} .journey #m-cv{z-index:3}

/* ---- the sea ---------------------------------------------------------------
   THE PLATE STILL DOES NOT CLOSE §4af.1, and the new asset should not be taken
   as closing it. Measured on the client's own files against the reference
   targets recorded in v52 (open water 32-42 / 54-64 / 95-110, luma std 10.6-20.7):
       ocean-overlay.webp   mean 1.2 / 30.7 / 76.5   luma std 6.68   high-pass 1.36
       ocean__1_.jpg        mean 1.0 / 41.6 / 88.8   luma std 5.50   high-pass 1.75
   Both carry a RED CHANNEL OF ABOUT 1 where the reference carries 32-42, and
   both are flat: the reference's water has six to fifteen times their local
   structure. Same two faults v52 wrote down.
   What is new and genuinely better: water-normal.webp is a real seamless normal
   map, so the wave structure it generates is photographed rather than invented
   the way v52's noise tile was. It is still a stand-in for a licensed dark
   aerial photograph. */
.journey #m-seawrap{position:absolute;inset:0;z-index:0;opacity:0;overflow:hidden;clip-path:inset(100% 0 0 0)}
.journey #m-seabg{position:absolute;inset:-8% -6%;
 background:#02143a url(../img/753f21a886bd.webp) center/cover no-repeat;
 filter:brightness(1.22) saturate(0.95)}
/* two copies of the normal map at different scales, drifting against each other.
   Where they slide across one another the highlights travel, which reads as
   swell - the v50 interference trick, now with a real tile. */
/* the swell is drawn on the canvas now - see seaPaint() */

/* the wake is PINNED TO THE HULL, not laid over the frame as weather - §4z.
   The clip is white foam on black, so `screen` deletes its background and it
   needs no matte; the blend has to sit on the masked element and not on the
   <video>, because a mask makes a stacking context. */
.journey #m-wakev{position:absolute;overflow:hidden;mix-blend-mode:screen;opacity:0;
 /* the stern is at the TOP of this hull and the wake trails upward from it, so
    the clip is opaque at its bottom - against the ship - and dissipates upward */
 -webkit-mask-image:linear-gradient(to top,#000 0,#000 26%,transparent 100%);
         mask-image:linear-gradient(to top,#000 0,#000 26%,transparent 100%)}
.journey #m-wakev video{position:absolute;left:0;top:0;width:100%;height:100%;object-fit:fill}



/* ---- copy ------------------------------------------------------------------
   Real DOM between the page and the vehicles, and a real <h1> - which v65 still
   does not have, since there the wordmark is a styled div. */
.journey #m-txt{position:absolute;inset:0;z-index:2;pointer-events:none}
.journey #m-txt .blk{position:absolute;will-change:transform,opacity}
.journey #m-mark{color:rgba(255,255,255,.055);font-weight:900;white-space:nowrap;line-height:.82}
.journey #m-txt .blk{direction:rtl}
.journey #m-road-copy{text-align:right;direction:rtl}
.journey #m-road-copy h2{font-size:var(--h1);line-height:1.18;margin:0 0 .5em;font-weight:800;color:#f4f4f5}
.journey #m-road-copy h2 .dim{color:rgba(244,244,245,.42)}
.journey #m-road-copy .intro{font-size:var(--body);line-height:1.75;color:rgba(244,244,245,.62);margin:0 0 2.4em}
.journey #m-road-copy .svc{margin:0 0 2.2em;padding-bottom:2.2em;border-bottom:1px solid rgba(255,255,255,.09)}
.journey #m-road-copy .svc:last-child{border-bottom:0}
.journey #m-road-copy .svc h3{font-size:var(--h2);line-height:1.3;margin:0 0 .55em;font-weight:800;color:#f4f4f5}
.journey #m-road-copy .svc p{font-size:var(--body);line-height:1.7;margin:0;color:rgba(244,244,245,.6)}
.journey #m-sea-copy{text-align:center;direction:rtl}
.journey #m-sea-copy h2{font-size:var(--h1);line-height:1.2;margin:0 0 1.6em;font-weight:800;color:#fff;
 text-shadow:0 2px 18px rgba(0,20,50,.55)}
.journey #m-sea-copy h2,
.journey #m-sea-copy .lead,
.journey #m-sea-copy .b{margin-inline:auto;text-align:center}
.journey #m-sea-copy .b{margin-block:0 2.6em;display:grid;justify-items:center}
.journey #m-sea-copy .b h3{font-size:var(--h2);margin:0 0 .5em;font-weight:800;color:#fff;
 text-shadow:0 2px 14px rgba(0,20,50,.6)}
.journey #m-sea-copy .b p{font-size:var(--body);line-height:1.7;margin:0;color:rgba(255,255,255,.86);
 text-shadow:0 2px 14px rgba(0,20,50,.6);max-width:32ch;text-align:center}

.journey .hud{position:fixed;left:10px;right:10px;bottom:calc(8px + env(safe-area-inset-bottom));
 display:flex;gap:10px;align-items:center;z-index:9;background:rgba(14,16,19,.9);
 padding:8px 11px;border-radius:8px;color:#fff;
 font:11px/1 ui-monospace,SFMono-Regular,monospace;direction:ltr}
.journey .hud input[type=range]{flex:1;accent-color:#d89b3c;min-width:0}
.journey .hud b{color:#f3bd64;font-weight:600;white-space:nowrap}

.journey #m-road-copy .vals{margin:0 0 2.4em;padding-inline-start:1.15em;font-size:var(--body);line-height:1.85;color:rgba(244,244,245,.6)}
.journey #m-road-copy .vals li{margin-bottom:.35em}
.journey #m-sea-copy .lead{font-size:var(--body);line-height:1.75;margin-block:0 2.4em;color:rgba(255,255,255,.86);text-shadow:0 2px 14px rgba(0,20,50,.6);max-width:34ch;text-align:center}

/* ---- the journey inside a scrolling page -----------------------------------
   Its roots were written as position:fixed because each prototype owned the
   whole viewport. Here they sit in a sticky stage: the stage holds the picture
   still while the spacer behind it supplies the scroll length, which is the same
   shape as the hero's own pin. The HUD is a development control and is not part
   of the page. */
/* v39: THE JOURNEY TAKES NO POINTER EVENTS. Hit-testing a FAQ question at the foot of the
   page returned `#d-cv` — the journey's canvas. `.faq` is pulled one screen up behind the
   sticky stage so the aircraft can hand over to it, and a fully transparent canvas still
   swallows clicks, so every question in that first screen was unclickable. Nothing in this
   section is interactive: the HUD is display:none and the modules' own wheel, key and pointer
   handlers all return early under FJ.driven. */
.journey{position:relative;background:#0d0d0f;pointer-events:none}
.journey__stage{position:sticky;top:0;block-size:100vh;block-size:100svh;overflow:hidden;
  background:#fefefe}
/* SCROLL LENGTH IS THE SPEED CONTROL. The journey's whole progress is mapped onto
   this spacer, so its height is how far you have to scroll to cross the sequence.
   At 760vh the whole journey - ten chapters - fitted in about 6 800 px on a
   desktop, which is roughly 680 px per chapter: one flick of a trackpad skips a
   chapter and a half, and going forward feels like it races. Doubled.
   There is no way to tell a two-finger gesture from a three-finger one in JS -
   the browser reports wheel deltas, not fingers - so this is the knob that
   actually governs how far a given gesture carries you. */
.journey__spacer{block-size:var(--journey-scroll,1500vh);pointer-events:none}
.journey .fj-root,.journey #fj-load{position:absolute;inset:0}
/* the mobile module rewrites its stage's cssText on every layout, and inline
   styles beat any selector - so the switch has to be a custom property it
   can carry rather than a rule we hope wins */
.journey{--fj-pos:absolute}
.journey #d-stage,.journey #m-stage{position:absolute}
/* v33: AND CENTRED. #d-stage is a 1440x671 block at the static origin of a full-viewport
   .fj-root, scaled with transform-origin:center — so its centre sits at (720, 335.5) of the
   WINDOW, not at the window's centre. At 1920 the picture lands 240 px to the left with
   240 px of bare stage background on the right; at 2560, 340 px. left/top 50 % with half the
   logical size as negative margins centres the box, and scaling about its own centre then
   keeps it there. This is why the aircraft's cut has been landing in the wrong place. */
.journey #d-stage{left:50%;top:50%;margin-left:-720px;margin-top:-335.5px}
/* the aircraft's own layer: OUTSIDE .journey__stage, because that is the element being
   clipped away to uncover the FAQ, and the aircraft has to fly over the FAQ rather than be
   cut with it. Same geometry as #d-stage so the module can keep drawing in 1440x671 space. */
.journey #fj-air{position:fixed;left:50%;top:50%;width:1440px;height:671px;
 margin-left:-720px;margin-top:-335.5px;z-index:9;pointer-events:none;opacity:0;
 transform-origin:center}
.journey .hud{display:none !important}
/* Real touch scrolling advances far less per gesture than a desktop wheel in
   responsive DevTools. Match the JS renderer contract and keep the whole mobile
   story long enough to scrub without hiding its first scene behind ten swipes. */
@media (max-width:63.99em), (pointer:coarse){
  .journey__spacer{--journey-scroll:1300svh}
  /* Mobile aircraft/shadow overlay: exactly the sticky viewport, above the
     Journey clip and the FAQ it reveals. */
  .journey #fj-air{left:0;top:0;width:100vw;height:100vh;height:100svh;
    margin:0;transform:none}
}


/* ---------- The journey's own progress screen is suppressed ----------
   It exists because the module was written to own the page and boot cold. It no
   longer boots cold: the entry loader fires varedat:ready, the journey starts
   downloading behind the hero, and it is finished long before the reader scrolls
   to it -- verified as ready without the reader ever scrolling.

   A reader who flicks straight down faster than the network can still arrive
   early. They now meet the section's own white and the first frame as it lands,
   rather than a percentage counter, which is the whole reason the entry loader
   was added. */
.journey #fj-load { display: none !important; }
