/* ============================================================
   MONARK v2 — the story map on About.
   Its generator (v2/build_map_f.py) is deleted; this is hand-edited now.
   Loaded after v2.css, on About only.

   Everything is nested under .mapf so the two stylesheets stay out of each
   other's way, and the classes v2.css also defines carry an mf- prefix
   (.mf-pin, .mf-lb, .mf-chip, .mf-stick, .mf-next, .mf-d).
   ============================================================ */
.mapf{
  --hot:#ff5040;
  --star:polygon(50% 0,58% 42%,100% 50%,58% 58%,50% 100%,42% 58%,0 50%,42% 42%);
  padding-top:clamp(28px,4vw,52px);   /* first section on the page: .sec's 72-168px left a dead band under the sticky nav */

  .head{margin-bottom:clamp(14px,2vw,26px)}

  .meta{font-size:.64rem;letter-spacing:.2em;text-transform:uppercase;color:var(--bone-dim);
    margin:14px 0 0}

  .meta b{color:var(--red);font-weight:700;white-space:nowrap}

  .legend{margin-top:7px;display:flex;flex-wrap:wrap;gap:.2em 1.1em;
    font-size:.6rem;letter-spacing:.2em;text-transform:uppercase;color:var(--bone-dim)}

  .legend i{font-style:normal;color:var(--red);margin-right:.5em}

  /* the map -------------------------------------------------------------- */
  .map{position:relative;overflow:clip;aspect-ratio:2.45;touch-action:none;
    cursor:grab;user-select:none;-webkit-user-select:none;
    border:1px solid rgba(224,34,27,.28);
    background:radial-gradient(120% 90% at 50% 0,rgba(224,34,27,.09),transparent 60%),#080707;
    box-shadow:inset 0 0 90px rgba(224,34,27,.07)}

  .map.grab{cursor:grabbing}

  .ov{position:absolute;inset:0;width:100%;height:100%}

  /* red rim + vignette — map-d's fresnel atmosphere, flattened */
  .vig{position:absolute;inset:0;pointer-events:none;z-index:2;background:
    radial-gradient(118% 134% at 50% 46%,transparent 60%,rgba(224,34,27,.10) 90%,rgba(224,34,27,.22) 100%),
    radial-gradient(118% 134% at 50% 46%,transparent 34%,rgba(8,7,7,.5) 78%,rgba(8,7,7,.86) 100%)}

  /* land hatch + graticule ------------------------------------------------ */
  .ov .bg{pointer-events:none}

  /* This used to be `.ov path,.ov line{vector-effect:non-scaling-stroke}`, so
     that a dot stayed the same number of CSS px at every zoom. It did work,
     and in WebKit it costs a fortune: non-scaling-stroke drops the fast path
     and makes the engine compute stroke geometry in device space for every
     one of ~38,000 subpaths, every frame. Measured on the real engine at
     390x844, with the mask already gone: 18 fps with it, 57 with it off.
     The same constancy comes for free from arithmetic instead. stroke-width
     in user units, multiplied by the camera's scale s, IS the screen width —
     so render() writes `wanted_px / s` and the result is identical, pixel for
     pixel, with none of the cost. --gw carries the graticule's share of that;
     the halftone's is written per path by GROW. Routes keep their own
     vector-effect:none below and are unaffected. */
  .ov .grat line{stroke:#e0221b;stroke-opacity:.07;stroke-width:var(--gw,.7)}

  .ov .grat-f{opacity:0;transition:opacity .4s}

  .map.z2 .ov .grat-f{opacity:1}

  .ov .grat-f line{stroke-opacity:.045}

  /* ---- the non-scaling overlay ---------------------------------------- */
  .lyr{position:absolute;inset:0;z-index:3;pointer-events:none}

  .mf-pin{position:absolute;left:0;top:0;width:0;height:0}

  .mf-pin>*{position:absolute;left:0;top:0}

  /* markers: origins dim and small, current cities bright with staggered rings */
  .dt{width:11px;height:11px;border-radius:50%;background:var(--hot);
    translate:-50% -50%;box-shadow:0 0 12px 2px rgba(255,80,64,.5)}

  .rg{width:19px;height:19px;border-radius:50%;border:1.4px solid var(--hot);
    translate:-50% -50%;animation:ping 2.6s linear infinite}

  .p1{animation-delay:-.65s}

  .p2{animation-delay:-1.3s}

  .p3{animation-delay:-1.95s}

  .bc{width:24px;height:24px;border-radius:50%;border:1.5px solid var(--hot);
    translate:-50% -50%;animation:beacon 1.3s steps(1,end) infinite}

  /* labels — plain HTML, so one CSS px at every zoom level */
  .mf-lb{font-size:.68rem;font-weight:700;letter-spacing:.18em;text-transform:uppercase;
    color:var(--bone);white-space:nowrap;line-height:1;
    text-shadow:0 0 5px #080707,0 0 5px #080707,0 0 5px #080707,0 0 9px #080707,0 1px 2px #080707}

  .mf-lb .sh{display:none}

  .l-l{translate:calc(-100% - 17px) -50%}

  .l-r{translate:17px -50%}

  .l-t{translate:-50% calc(-100% - 15px)}

  .l-b{translate:-50% 15px}

  .nb{color:var(--hot);animation:blink 1.3s steps(1,end) infinite}

  /* operator squares — filled red, initials in ink (map-d's badges) */
  .sq{width:28px;height:28px;padding:0;border:1px solid var(--red);
    background:var(--red) center 22%/cover no-repeat;
    display:grid;place-items:center;cursor:pointer;line-height:1;pointer-events:auto;
    font-size:0;color:transparent;overflow:hidden;
    translate:calc(-50% + var(--x,0px)) calc(-50% + var(--y,0px));
    transition:scale .16s,border-color .16s,box-shadow .16s}

  /* screened red like every other photo here; the screen lifts on hover, which
     is also the only way a 28px face reads as a person rather than a swatch */
  .sq::before{content:"";position:absolute;inset:0;background:var(--red);
    mix-blend-mode:multiply;opacity:.38;transition:opacity .16s}

  .sq:hover::before,.sq:focus-visible::before,.sq.on::before{opacity:0}

  .sq:hover,.sq:focus-visible,.sq.on{border-color:var(--hot);scale:1.28;
    box-shadow:0 0 0 4px rgba(224,34,27,.24),0 0 20px 2px rgba(224,34,27,.5)}

  /* The four squares sit in a row over whichever city they are in. These used
     to be inline on the buttons, which no media query could beat — they live
     here so touch can open the row from a 30px pitch to 44px, wide enough for
     four finger-sized hit boxes to tile instead of stealing from each other. */
  .sq{--y:-32px}
  .sq[data-m="tommaso"]{--x:-45px}
  .sq[data-m="bogdan"] {--x:-15px}
  .sq[data-m="rikardo"]{--x:15px}
  .sq[data-m="elia"]   {--x:45px}
  /* Beat 0 — .s0 with no .s1 (mapf.js toggles s0..s5 as b>=i) — is the one
     beat where the four stand on different cities, and there the lane order
     has to track the cities, not the names. Bogdan and Rikardo share
     Sanremo and want adjacent lanes; Tommaso stands alone on Roma, which is
     ~92px east of Sanremo on the phone camera, so his default negative lane
     walked his face straight onto Rikardo's: 4px apart, measured at
     375x667. The two swap lanes here and NOWHERE else, so beats 1-5 stay
     byte-identical to the fan that was tuned for them — Rikardo's beat-5
     lane above all, which parks him on Milano next to the card, and which
     a swap in the base block pushed 88px left underneath it. */
  .map:not(.s1) .sq[data-m="tommaso"]{--x:15px}
  .map:not(.s1) .sq[data-m="rikardo"]{--x:-45px}

  /* event stars — the Monark 4-point mark, deliberately not a square.
     The clip lives on ::before: on .ev itself it would also clip the tooltip. */
  .ev{width:20px;height:20px;padding:0;border:0;background:none;cursor:pointer;
    pointer-events:auto;
    translate:calc(-50% + var(--x,0px)) calc(-50% + var(--y,0px));transition:scale .16s}

  .ev::before{content:"";position:absolute;inset:0;background:var(--red);
    clip-path:var(--star);transition:background .16s}

  .ev:hover,.ev:focus-visible,.ev.on{scale:1.3}

  .ev:hover::before,.ev:focus-visible::before,.ev.on::before{background:var(--bone)}

  .ev.mf-next{width:29px;height:29px;animation:blink 1.3s steps(1,end) infinite}

  .ev.mf-next::before{background:var(--hot)}

  .ev.mf-next:hover::before,.ev.mf-next:focus-visible::before,.ev.mf-next.on::before{background:#fff}

  .sq::after,.ev::after{content:attr(data-label);position:absolute;left:50%;bottom:calc(100% + 10px);
    translate:-50% 0;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .18s;
    font-size:.6rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--bone);
    background:rgba(8,7,7,.92);border:1px solid rgba(224,34,27,.5);padding:.45em .7em;
    clip-path:none;z-index:9}

  .sq:hover::after,.sq:focus-visible::after,.ev:hover::after,.ev:focus-visible::after{opacity:1}

  .ev::after{font-weight:400}

  /* the Amsterdam cluster: one chip at world zoom, an exploded fan past z3 */
  .clu,.fan{position:absolute;left:0;top:0;width:0;height:0;transition:opacity .3s}

  .clu>*,.fan>*{position:absolute;left:0;top:0}

  .mf-chip{font-size:.56rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase;
    color:var(--hot);white-space:nowrap;line-height:1;
    translate:calc(-50% + var(--x,0px)) calc(-50% + var(--y,0px));
    text-shadow:0 0 5px #080707,0 0 5px #080707,0 0 9px #080707}

  .fan{opacity:0;pointer-events:none}

  .map.z3 .fan{opacity:1;pointer-events:auto}

  .map.z3 .clu{opacity:0;pointer-events:none}

  /* pointer-events inherits, and the stars set it back to auto — so with the
     fan faded out at world zoom its lit stars were still there to tap, and a
     finger on Amsterdam at beat 5 opened a night nobody could see. The
     ::after too: on phones it is the finger pad, with its own auto. */
  .map:not(.z3) .fan>*,.map:not(.z3) .fan>*::after{pointer-events:none}

  .ldr{pointer-events:none;
    height:1px;margin-top:-.5px;width:var(--r);background:linear-gradient(90deg,
    rgba(224,34,27,.2),rgba(255,80,64,.85));transform-origin:0 50%;
    transform:rotate(var(--a))}

  /* A date label is decoration and must never be a click target: it is centred
     on its own --x/--y, so the four near-horizontal ones in the fan reach back
     over their own star and, without this, swallow the tap that was meant for
     it. Cost a hit-test run to find. */
  .ecode{font-size:.5rem;letter-spacing:.12em;color:var(--bone-dim);white-space:nowrap;
    pointer-events:none;
    line-height:1;translate:calc(-50% + var(--x,0px)) calc(-50% + var(--y,0px));
    text-shadow:0 0 5px #080707,0 0 5px #080707,0 0 9px #080707}

  .ecode.hot{color:var(--hot);font-weight:700}

  .clu .ev{--x:-30px;--y:22px}

  .chip-clu{translate:calc(-100% - 44px) calc(-50% + 22px)}

  .chip-blu{translate:-50% 18px}

  /* render() flags a pin whose dot is off the plate; its chip goes with it */
  .mf-pin.edge .mf-chip{visibility:hidden}

  /* HUD chrome ----------------------------------------------------------- */
  /* The sweep animated `top` from 4% to 96%, which asks for a layout on every
     frame, for ever, on the element that contains the whole map. On WebKit
     that one line cost 18fps of the 60: measured at 390x844, switching this
     animation off and nothing else took the page from 42 to 60. translate is
     a compositor property and costs none of that. It cannot use a percentage
     — that would resolve against the line's own 1px height — so js/mapf.js
     publishes the plate's travel as --scan-travel whenever it measures it. */
  .scan{position:absolute;left:0;right:0;top:4%;height:1px;pointer-events:none;z-index:2;
    background:linear-gradient(90deg,transparent,rgba(224,34,27,.4),transparent);
    animation:scan 7s cubic-bezier(.4,0,.6,1) infinite}

  .corners{position:absolute;inset:14px;pointer-events:none;z-index:4;background:
    linear-gradient(#e0221b,#e0221b) left top/26px 1px,
    linear-gradient(#e0221b,#e0221b) left top/1px 26px,
    linear-gradient(#e0221b,#e0221b) right top/26px 1px,
    linear-gradient(#e0221b,#e0221b) right top/1px 26px,
    linear-gradient(#e0221b,#e0221b) left bottom/26px 1px,
    linear-gradient(#e0221b,#e0221b) left bottom/1px 26px,
    linear-gradient(#e0221b,#e0221b) right bottom/26px 1px,
    linear-gradient(#e0221b,#e0221b) right bottom/1px 26px;
    background-repeat:no-repeat;opacity:.8}

  .hud{position:absolute;left:48px;right:48px;bottom:22px;pointer-events:none;z-index:4;
    display:flex;justify-content:space-between;align-items:flex-end;gap:14px;
    font-size:.58rem;letter-spacing:.2em;text-transform:uppercase;color:var(--bone-dim);
    text-shadow:0 0 8px #080707,0 0 4px #080707,0 0 2px #080707}

  .hud span{white-space:nowrap}

  .hud b{color:var(--red);font-weight:700}

  .hint{color:var(--red)}

  .hint .m{display:none}

  .zoom{position:absolute;top:28px;right:28px;z-index:4;display:flex;flex-direction:column;gap:6px}

  .zoom button{width:30px;height:30px;padding:0;display:grid;place-items:center;cursor:pointer;
    font-size:.86rem;line-height:1;color:var(--bone);background:rgba(8,7,7,.82);
    border:1px solid rgba(224,34,27,.5);transition:background .15s,color .15s}

  .zoom button:hover{background:var(--red);color:#0c0b0b}

  /* dossier -------------------------------------------------------------- */
  .dossier{position:absolute;top:12px;right:12px;bottom:12px;z-index:6;
    width:min(400px,88%);overflow:auto;padding:clamp(16px,1.8vw,26px);
    background:rgba(8,7,7,.985);border:1px solid rgba(224,34,27,.55);
    box-shadow:0 24px 60px -20px rgba(0,0,0,.9)}

  .dossier[hidden],.dossier>div[hidden]{display:none}

  .dpho{position:relative;display:block;float:left;width:96px;aspect-ratio:3/4;
    margin:0 14px 10px 0;overflow:hidden;border:1px solid rgba(224,34,27,.4)}

  .dpho::after{content:"";position:absolute;inset:0;background:var(--red);
    mix-blend-mode:multiply;opacity:.6;pointer-events:none}

  .d-photo{width:100%;height:100%;object-fit:cover;
    filter:grayscale(1) contrast(1.25) brightness(1.06)}

  .d-role{color:var(--red);margin:0;padding-right:28px}

  .d-route{margin:5px 0 0;letter-spacing:.2em;color:var(--bone-dim)}

  .d-name,.e-name{font-family:var(--disp);font-weight:400;text-transform:uppercase;
    font-size:clamp(1.5rem,2.4vw,2rem);line-height:1;margin:8px 0 12px}

  .d-bio{font-size:.8rem;line-height:1.55;margin:0;color:var(--bone-dim)}

  .d-x{position:absolute;top:8px;right:10px;background:none;border:0;cursor:pointer;
    font-size:1.5rem;line-height:1;padding:6px;z-index:2}

  .d-x:hover{color:var(--red)}

  .e-poster{width:100%;height:auto;max-height:min(46vh,420px);object-fit:contain;
    border:1px solid rgba(224,34,27,.4);margin-bottom:14px}

  .e-code{color:var(--red);margin:0;padding-right:28px}

  .e-code .nx{color:var(--hot);margin-left:.6em}

  .e-name{margin:6px 0 10px}

  .e-row{margin:0 0 4px;font-size:.68rem;letter-spacing:.14em;text-transform:uppercase;
    color:var(--bone-dim)}

  .e-row b{color:var(--bone);font-weight:700}

  .e-link{display:inline-block;margin-top:14px;padding:.6em 1.1em;text-decoration:none;
    font-size:.62rem;font-weight:700;letter-spacing:.18em;text-transform:uppercase;
    color:#0c0b0b;background:var(--red);border:1px solid var(--red);transition:background .15s}

  .e-link:hover{background:var(--hot);border-color:var(--hot)}

  .e-link.hot{background:var(--hot);border-color:var(--hot)}

  /* responsive ----------------------------------------------------------- */
  @media (max-width:720px){
    /* .story sits outside .wrap, so the stage is already full-bleed and the
       map needs no negative margin. It had one, from before the story: with
       width:100% that made a 390px plate that STARTED at -20px — 20px of map
       clipped off the left, a 20px hole on the right, and every control
       carrying a +var(--pad) correction to compensate. */
    .map{border-left:0;border-right:0}
    /* The graticule is 7%/4.5% of #e0221b on #080707 — about two RGB units.
       That survives a desk monitor in a dark room and disappears on a phone
       at half brightness in daylight, which is where this page's traffic is.
       The halftone gets the same treatment in js/mapf.js (GROW s0m). */
    .ov .grat line{stroke-opacity:.12}
    .ov .grat-f line{stroke-opacity:.075}

    .mf-lb{font-size:.62rem;letter-spacing:.1em}
    .mf-pin.org .mf-lb{font-size:.56rem}
    .nb{display:none}
    /* City names, like desktop. The airport codes only come out at world
       zoom (beat 5, z<4.5): Europe is 30px wide there and "Amsterdam" hangs
       off the left edge of a 390px plate; "AMS" fits. */
    .map:not(.z2) .mf-lb .lg{display:none}
    .map:not(.z2) .mf-lb .sh{display:inline}
    .clu .ev{--x:-28px;--y:58px}
    /* -8, not -14: at world zoom Amsterdam is 39px from the left edge of a
       320px plate and the chip lost its first letter */
    .chip-clu{translate:calc(-50% - 8px) calc(-50% + 76px)}
    /* Touch used to get SMALLER markers than a mouse — 24/17px against
       desktop's 28/20 — and the ::after that carries the hover tooltip was
       switched off with it. On touch that tooltip is dead weight, so the
       pseudo is reused as the transparent finger pad the glyph never had. */
    /* 34, not desktop's 28. The red screen below is what makes a face read as
       a face, and on a phone it never lifts — there is no hover — so the only
       other lever is size. The lanes stay 44px apart, so 34 still tiles. */
    .sq{width:34px;height:34px;overflow:visible}
    /* The screen is .38 on desktop because a mouse can lift it. On a phone it
       is permanent, and at this size a photo under a 38% red multiply is a
       swatch, not a person — which is half of "people placed at random". */
    .sq::before{opacity:.16}
    /* The operator row parks on Amsterdam at --y:-32px and the fan explodes
       around the same point. On a phone the row's four 44px pads span the
       whole width of the ring's upper half, and they sat on top of three of
       the ten stars — the top-right pair and the far-left one were not
       tappable at all. So when the fan is open (z3: story beats 3 and 6, or a
       deep zoom in explore) the row steps up out of the ring, clear of the
       top date label at -88px. Only when it is open: at the origin beats the
       faces have to stay on their own city dots. Desktop is left alone, its
       row is 118px narrower and clears the stars already.
       Lift only the faces that are actually parked on Amsterdam: Rikardo and
       Elia leave for Milano and Hong Kong at beat 5 (.s4), and lifting them
       there walks Rikardo's face 118px up off Milano and straight back into
       the fan at 320px — which is exactly the bug this rule exists to fix.
       And .s1, because z3 is not "the fan is open" — it is z>12, nothing
       more, and KEYS_M[0] is a deep zoom on Italy that clears that threshold
       on its own (z=17 when it was w:75, z=38 now). So this rule fired at
       the origin beat too, where there is no fan at all, and lifted
       all four faces 118px out to sea while Treviso, Sanremo and Roma sat
       labelled and empty underneath — under a card whose whole text is
       "Treviso. Sanremo, twice. Roma. Four operators start here". Desktop
       never showed it: its beat-0 camera stays under z=12. .s1 is beat 1 and
       up, the same idiom .map:not(.s1) uses above for beat 0 alone.
       tests/mapdiag.py asserts it now (OFF-CITY). */
    .map.z3.s1 .sq[data-m="tommaso"],
    .map.z3.s1 .sq[data-m="bogdan"],
    .map.z3.s1:not(.s4) .sq[data-m="rikardo"],
    .map.z3.s1:not(.s4) .sq[data-m="elia"]{--y:-118px}
    .sq{transition:scale .16s,border-color .16s,box-shadow .16s,translate .3s}
    .ev{width:20px;height:20px}
    .ev.mf-next{width:28px;height:28px}
    .sq::after,.ev::after{content:"";inset:-8px;translate:none;padding:0;
      border:0;background:none;opacity:1;transition:none;pointer-events:auto}
    /* the face grew to 34, so the pad shrinks to keep the same 44px box the
       44px lane pitch was cut for — at -8px the four would overlap by 6px
       and the outer two would steal from their neighbours */
    .sq::after{inset:-5px}
    /* 36px on the stars, not 44: the closest pair on the ellipse is 40px
       apart and a bigger pad eats the next night. The finale star is 28px
       already, so its pad is thinner for the same 40px box. */
    .ev::after{inset:-8px}
    .ev.mf-next::after{inset:-6px}
    .sq[data-m="tommaso"]{--x:-66px}
    .sq[data-m="bogdan"] {--x:-22px}
    .sq[data-m="rikardo"]{--x:22px}
    .sq[data-m="elia"]   {--x:66px}
    /* Beat 0 only, same city-not-name reason as the base fan — but stated as
       the geometry rather than as a swap. Two of the four stand alone on
       their own city (Tommaso on Roma, Elia on Treviso) and want the dot's
       own lane; only Bogdan and Rikardo share Sanremo and have to step
       apart. The old +22/-66 pair was cut when the beat-0 camera was w:75
       and Roma sat ~92px east of Sanremo; at w:34 it is ~193px east, and
       Elia's +66 lane hung his face off the right edge of a 390px plate. */
    .map:not(.s1) .sq[data-m="tommaso"],
    .map:not(.s1) .sq[data-m="elia"]{--x:0px}
    .map:not(.s1) .sq[data-m="bogdan"]{--x:-22px}
    .map:not(.s1) .sq[data-m="rikardo"]{--x:-66px}
    /* these two stand alone on the plate, so they get the full 44 */
    .clu .ev::after,.mf-pin.blu .ev::after{inset:-12px}
    /* world zoom (beat 5): Amsterdam sits 45px from the left edge, so the two
       faces still parked on it close ranks over the dot instead of hanging
       off the plate at -66 */
    .map:not(.z2) .sq[data-m="tommaso"]{--x:-16px}
    .map:not(.z2) .sq[data-m="bogdan"]{--x:16px}
    /* Milano is 10px from Amsterdam at that scale, so Rikardo's face, parked
       on it, lands on Bogdan's; it steps right, past the MIL label */
    .map:not(.z2) .sq[data-m="rikardo"]{--x:40px;--y:-36px}
    .hud{font-size:.5rem;letter-spacing:.1em;left:14px;right:14px;bottom:6px}
    /* .co went first; the operators readout overflows the plate too and gets
       clipped mid-word ("4 OPERATOR"). On a phone the hint is the half that
       does any work — and once the EXPLORE button is up, the button is.
       Child combinator on purpose: as a descendant selector it also matched
       .ex (last child of .hint) and .m (last child of .ex), and the explore
       hint never showed on a phone. */
    .hud .co,.hud>span:last-child{display:none}
    .hint .mf-d{display:none}
    .hint .m{display:inline}
    .map.s5 .hud .st{display:none}
    /* left, not right: the fan's rightmost star sits 84px right of Amsterdam
       and a 44px zoom column on the right lands on top of it at 320px */
    .zoom{top:12px;left:12px;right:auto;gap:4px}
    .zoom button{width:44px;height:44px;font-size:1rem}
    .corners{inset:8px}
    /* a bottom sheet, and opaque: at .985 the chapter card underneath
       ghosted through the bio */
    .dossier{position:fixed;inset:auto 0 0 0;top:auto;width:auto;max-height:76dvh;
      background:#080707;border-left:0;border-right:0;border-bottom:0}
    .d-x{top:4px;right:6px;padding:10px;min-width:44px;min-height:44px}
  }

  @media (prefers-reduced-motion:reduce){
    *,*::before,*::after{animation:none!important;transition:none!important}
    .scan{display:none}
  }

  /* ============ VERSION F additions: scroll story ======================= */
  /* The story is a tall track with a sticky 100vh stage; scroll position picks
     the chapter. The camera is the same view/tick easing as map-e — beats only
     set new targets. Free pan/zoom is gated behind EXPLORE (end of story). */
  .story{position:relative}

  /* no min-height floor: the map is already height-limited by the desktop
     max-height rules, so a floor only ever bit on a short viewport — a phone
     in landscape got a 520px stage in a 390px window, with the map centred
     in it and EXPLORE 80px below the fold. */
  .mf-stick{position:sticky;top:0;height:100vh;display:flex;align-items:center;overflow:clip}

  .stagewrap{position:relative;margin-inline:auto;
    width:min(1440px,100% - var(--pad)*2,calc((100vh - 44px)*2.45))}

  .story .map{aspect-ratio:2.45;max-height:calc(100vh - 44px);width:100%}

  .map.story-mode{touch-action:pan-y;cursor:default}

  .map:not(.explore) .zoom{opacity:0;pointer-events:none}

  .zoom{transition:opacity .35s}

  .map:not(.explore) .hint .ex{display:none}

  .map.explore .hint .st{display:none}

  /* chapter rail — dossier-styled card, left side, one chapter at a time */
  .rail{position:absolute;inset:0;z-index:5;pointer-events:none}

  .ch{position:absolute;left:clamp(10px,1.8vw,26px);top:50%;translate:0 calc(-50% + 12px);
    width:min(330px,38%);
    padding:clamp(14px,1.6vw,22px);
    background:rgba(8,7,7,.94);border:1px solid rgba(224,34,27,.55);
    box-shadow:0 24px 60px -20px rgba(0,0,0,.9);
    opacity:0;visibility:hidden;transition:opacity .45s,visibility .45s,translate .45s}

  .ch.on{opacity:1;visibility:visible;pointer-events:auto;translate:0 -50%}

  .kick{font-size:.6rem;font-weight:700;letter-spacing:.22em;text-transform:uppercase;
    color:var(--red);margin:0}

  .ch h3{font-family:var(--disp);font-weight:400;text-transform:uppercase;
    font-size:clamp(1.25rem,1.9vw,1.85rem);line-height:.95;margin:8px 0 10px}

  .cap{font-size:.74rem;line-height:1.55;color:var(--bone-dim);margin:0}

  .cap b{color:var(--bone)}

  /* poster slot — taped flyer, full colour (user call). The season-one slot is
     a two-image crossfade buffer: a bare src swap pops. */
  .cpost{margin:14px 0 0;transform:rotate(-1.6deg)}

  .ccode{display:block;margin:0 0 6px;font-size:.56rem;letter-spacing:.18em;
    text-transform:uppercase;color:var(--bone-dim);line-height:1.7}

  .ccode b{color:var(--hot)}

  .cframe{position:relative;display:block;border:1px solid rgba(224,34,27,.4);background:#000}

  /* contain, not cover: a flyer that is cropped is a flyer nobody can read.
     The box stays fixed because the season-one slot crossfades two images in
     it, so the poster letterboxes on the black frame instead of resizing it. */
  .cframe img{display:block;width:100%;height:min(236px,26vh);object-fit:contain}

  .cframe.xf{height:min(236px,26vh)}

  .cframe:not(.xf){display:inline-block;width:auto}

  .cframe:not(.xf) img{width:auto;height:min(230px,25vh)}

  /* The finale card is the widest thing in the story and its flyer is portrait,
     so stacking poster over button left a hole down the right half. Copy keeps
     the full width; poster and CTA share the last row, spread to the edges and
     bottom-aligned, which also buys back the button's height for the poster. */
  .ch[data-ch="5"]{display:flex;flex-wrap:wrap;align-items:flex-end;
    justify-content:space-between;gap:0 12px}

  .ch[data-ch="5"]>.kick,.ch[data-ch="5"]>h3,.ch[data-ch="5"]>.cap{flex:1 0 100%}

  .ch[data-ch="5"] .cpost{margin-bottom:0}

  .ch[data-ch="5"] .e-link{margin-top:0}

  .ch[data-ch="5"] .cframe img{height:min(268px,28vh)}

  .cframe.xf img{position:absolute;inset:0;height:100%;max-height:none;opacity:0;
    transition:opacity .4s}

  .cframe.xf img.show{opacity:1}

  .ch .e-link{pointer-events:auto}

  /* origin pins — dim, only at z2 (same tier map-e reserved for them) */
  .mf-pin.org .dt{width:7px;height:7px;background:var(--red-dark);box-shadow:none}

  .mf-pin.org{opacity:0;transition:opacity .4s}

  .map.z2 .mf-pin.org{opacity:1}

  /* story gating: sN = "beat N reached". Reveals accumulate; scrolling back
     removes the class and the transition undraws it. */
  .story-mode .mf-pin.mil,.story-mode .mf-pin.hkg{opacity:0;transition:opacity .5s}

  .map.s4 .mf-pin.mil,.map.s4 .mf-pin.hkg{opacity:1}

  .story-mode .mf-pin.blu{opacity:0;transition:opacity .5s}

  .map.s3 .mf-pin.blu{opacity:1}

  /* season-one fan lights night by night (JS adds .lit by scroll progress);
     MNK-007 and its leader stay dark until explore — it belongs to the finale */
  .story-mode .fan>*{opacity:0;visibility:hidden;transition:opacity .35s,visibility .35s}

  .story-mode .fan>.lit{opacity:1;visibility:visible}

  /* blink/beacon animate opacity, and a running animation outranks the
     opacity:0 above in the cascade — stop it while the story hides them */
  .story-mode:not(.s5) .fan .ev.mf-next{animation:none}

  .story-mode:not(.s4) .clu{opacity:0}

  /* routes — drawn strokes. vector-effect:none ON PURPOSE: with
     non-scaling-stroke Chromium turns dasharray into screen px (see memory),
     which breaks the pathLength=1 draw-on trick. User-unit widths are tuned
     for the zoom each route is seen at. */
  .rt path{fill:none;vector-effect:none;stroke-linecap:butt;
    stroke-dasharray:1;stroke-dashoffset:1;transition:stroke-dashoffset 1.3s ease .15s}

  .rt .r-o{stroke:var(--red);stroke-opacity:.55;stroke-width:.2}

  .rt .r-c{stroke:var(--hot);stroke-opacity:.9;stroke-width:.8;
    transition:stroke-dashoffset 1.3s ease .15s,opacity .6s}

  /* the comets are drawn in world units, so the beat-5 push-in on Amsterdam
     (w:70, ~18x) turns them into bands across the plate. They have said their
     piece by then — fade them for the finale, but only while the story drives:
     .s5 survives into explore, where you want them back on a zoom-out. */
  .story-mode.s5 .rt .r-c{opacity:0}

  .rt .r-hkg{stroke-width:.9;transition-duration:2s}

  .rt .r-o:nth-of-type(2){transition-delay:.4s}

  .rt .r-o:nth-of-type(3){transition-delay:.65s}

  .map.s1 .r-o{stroke-dashoffset:0}

  .map.s4 .r-c{stroke-dashoffset:0}

  /* chapter ticks — orientation + jump; smooth scroll rides the scrub */
  .prog{position:absolute;right:26px;top:50%;translate:0 -50%;z-index:6;
    display:flex;flex-direction:column;gap:10px}

  .prog button{width:10px;height:10px;padding:0;cursor:pointer;background:none;
    border:1px solid var(--red);opacity:.55;transition:opacity .3s,background .3s}

  .prog button.past{background:var(--red-dark);border-color:var(--red-dark);opacity:.7}

  .prog button.act{background:var(--hot);border-color:var(--hot);opacity:1}

  .map.explore~.prog{display:none}

  /* explore toggle — story never auto-unlocks: an unlocked map eats the wheel
     and the page can no longer scroll back up over it */
  .exbtn{position:absolute;right:28px;bottom:44px;z-index:6;padding:.7em 1.1em;cursor:pointer;
    font:700 .6rem/1 var(--mono);letter-spacing:.18em;text-transform:uppercase;
    color:#0c0b0b;background:var(--red);border:1px solid var(--red);
    opacity:0;pointer-events:none;transition:opacity .35s}

  .map.s5 .exbtn,.map.explore .exbtn{opacity:1;pointer-events:auto}

  .exbtn:hover{background:var(--hot);border-color:var(--hot)}

  .exbtn .back{display:none}

  .map.explore .exbtn .go{display:none}

  .map.explore .exbtn .back{display:inline}

  /* COOKING chip — blinking, animated ellipsis */
  .cook{color:var(--hot);font-weight:700;translate:-50% 16px;opacity:0;transition:opacity .4s}

  .map.s4 .cook{opacity:1;animation:blink 1.3s steps(1,end) infinite}

  .cook .cd::after{content:"";animation:cookdots 1.6s steps(1,end) infinite}

  .ch[data-ch="4"]{left:50%;top:auto;bottom:16px;translate:-50% 12px}

  .ch[data-ch="4"].on{translate:-50% 0}

  @media (max-width:720px){
    /* A COLUMN: map on top at a fixed shape, chapter card underneath it.
       This was a column once and was replaced by a full-bleed portrait stage
       with the card floating over it, because back then the plate was
       whatever height the card left it — 650px under a caption-only chapter,
       416px under the finale and its flyer — and every chapter flip resized
       the plate and re-framed the camera mid-story. That is a real bug and
       the cure was right; the shape it left behind was not.

       A plate-carree world map is a 2.45:1 object. The full-bleed stage made
       the plate 390x780, i.e. 1:2, so the window showed TWO units of latitude
       for every unit of longitude — the exact inverse of the map's own shape.
       Measured against desktop at the same beat: desktop's window is 200x81
       world units, the phone's was 108x216. At beat 4 the phone asked for a
       window 1200 units tall on a plate that is 509 tall, so fit() clamped
       and centred it and most of the stage was empty. Every camera key was
       then tuned to hide that, and the owner's verdict on the result was
       "una mappa brutta fatta male".

       So: keep the constant plate, drop the portrait. aspect-ratio holds the
       shape no matter which chapter is up, max-height keeps it from eating a
       short viewport, and the card sits in .rail below it with a fixed box of
       its own. The whole --ch apparatus goes with the overlap — the camera no
       longer has to lift the subject clear of the card, and EXPLORE and the
       hint go back to plain offsets off the plate's own bottom edge. */
    .stagewrap{width:100%;height:100%;display:flex;flex-direction:column;
      justify-content:center;gap:10px}
    .story .map{max-height:47dvh;aspect-ratio:1.3;height:auto;flex:0 0 auto}
    /* explore is a free pan-and-zoom, not a framed shot: give it the stage,
       and take the rail out of the column so there is a stage to give */
    .map.explore{max-height:none;aspect-ratio:auto;flex:1 1 auto}
    .map.explore~.rail{display:none}
    /* The six cards stack in one grid cell instead of being absolutely
       positioned, so .rail is exactly as tall as the tallest chapter and the
       map+card column can be centred in the stage as one block. They are
       visibility:hidden rather than display:none, so all six measure and the
       box never changes height between chapters — which is the property the
       old overlay bought with --ch, kept here for free. flex:0 1 shrinks the
       rail before the map on a short viewport, and the card scrolls. */
    .rail{position:relative;display:grid;flex:0 1 auto;min-height:0}
    /* align-self:start so each card's box hugs its own text: the grid row is
       as tall as the tallest chapter (which is what keeps the map from moving
       between beats), but a short chapter must not draw a 344px frame around
       three lines of copy. */
    /* max-height caps the card at the rail, which .rail's flex:0 1 shrinks on
       a short viewport: without it the finale card ran 18px past the fold on a
       320x568 and the album button went with it. overflow:auto then scrolls
       the copy inside instead of pushing it off the screen. */
    .ch,.ch[data-ch="4"]{position:static;grid-area:1/1;align-self:start;width:auto;
      max-height:100%;margin-inline:8px;overflow:auto;padding:12px;translate:0 12px;
      /* opaque, like the dossier. It no longer covers the plate, but it still
         backs onto the page and the grain, and a transparent card over that
         ghosted at .94. */
      background:#080707}
    .ch.on,.ch[data-ch="4"].on{translate:none}
    .ch h3{font-size:1.1rem}
    .cap{font-size:.68rem}
    .cframe img,.cframe.xf,.cframe:not(.xf) img,.ch[data-ch="5"] .cframe img{height:132px}
    /* explore wants the plate: the card steps out and the controls drop to
       the bottom edge. .rail follows .map inside .stagewrap, hence ~. */
    .map.explore~.rail .ch{opacity:0;visibility:hidden;pointer-events:none}
    /* Sardinia is 11px from Amsterdam at world zoom on a 390px plate and its
       chip lands on Milano's; it is the beat-4 subject, not a world-scale
       one. After .map.s3 .mf-pin.blu on purpose: same specificity. */
    .map:not(.z2) .mf-pin.blu{opacity:0}
    .exbtn{right:12px;bottom:10px;padding:0 1.2em;min-height:44px;
      display:inline-flex;align-items:center}
    /* Six 8px dots in a column were the least tappable thing on the site, and
       a column of 44px boxes is taller than the map, so on a phone it is a
       row. It used to be a row laid OVER the plate's top edge, and on the
       full-bleed 780px stage there was room for that. On the 300px landscape
       plate there is not: the fan reaches ~88px above Amsterdam, the lifted
       face row sits above that, and a 46px band along the top left the faces
       under buttons that are painted over them — untappable, which is what
       tests/mapdiag.py was reporting as MISS [under prog].
       So it is a row IN the column now, between the map and the card, a
       sibling of .map rather than a child (about.html). It costs 46px of
       stage and buys the whole plate back — and with nothing overlaying the
       map, camAt() needs no phone correction at all. Desktop is unmoved: the
       stage box and the map box are the same rectangle there, so absolute
       positioning against .stagewrap lands exactly where it did. */
    .prog{position:static;align-self:center;translate:none;
      flex-direction:row;gap:0}
    .prog button,.prog button.past,.prog button.act{width:46px;height:46px;
      border:0;background:none;opacity:1;display:grid;place-items:center}
    .prog button::before{content:"";width:9px;height:9px;
      border:1px solid var(--red);opacity:.55;transition:opacity .3s,background .3s}
    .prog button.past::before{background:var(--red-dark);
      border-color:var(--red-dark);opacity:.7}
    .prog button.act::before{background:var(--hot);border-color:var(--hot);opacity:1}
  }

  /* A 568px phone leaves ~300px between the progress row and a card with a
     flyer in it, and the open fan needs ~260 of that: the lifted operator row
     at -132, the date labels at ±88, the stars between. Buy the height back
     from the flyer, not from the map. Only the frame goes — .ccode stays, so
     CH.03 still reads the nights out one by one as you scroll, which is the
     whole beat. */
  @media (max-width:720px) and (max-height:700px), (max-height:500px){
    /* .cframe:not(.xf) is display:inline-block at (0,2,0) — a bare .cframe
       here hides the crossfade frame and leaves the other two standing, i.e.
       exactly the wrong one. Both selectors, on purpose.
       The second query is a phone on its side: 844x390 gets the desktop
       layout in a 326px stage, where the finale card is 384px tall — title
       under the nav, album button below the fold. Same cure, and the two
       hud readouts go too; three of them do not fit a 730px plate. */
    .cframe,.cframe:not(.xf){display:none}
    .cpost{margin-top:8px;transform:none}
    .ch,.ch[data-ch="5"]{padding:10px}
  }
  @media (max-height:500px){
    .hud .co,.hud>span:last-child{display:none}
  }
}

@keyframes ping{from{scale:.5;opacity:.85}to{scale:2.9;opacity:0}}
@keyframes beacon{0%,54%{opacity:.95}54.01%,100%{opacity:0}}
@keyframes blink{0%,54%{opacity:1}54.01%,100%{opacity:.4}}
@keyframes scan{0%,100%{translate:0 0}50%{translate:0 var(--scan-travel,280px)}}
@keyframes cookdots{0%{content:""}25%{content:"."}50%{content:".."}75%{content:"..."}}

/* the stage parks under v2's sticky 64px nav, not under nothing. The two
   size rules are desktop-only: the mobile media query above deliberately
   makes the map the whole stage and drops the max-height, and these sit
   after it, so unguarded they would win back on phones. */
.mapf .mf-stick{top:var(--nav-h);height:calc(100dvh - var(--nav-h))}
@media (min-width:721px){
  /* The stage is a full viewport tall and the map, capped at 1440px wide by
     .stagewrap, is only width/2.45 tall — 504px in a 836px stage at 1440x900.
     align-items:center split the leftover 330px evenly, so the page opened on
     a 166px band of nothing between the legend and the map. max-content caps
     the stage at its one in-flow child (.stagewrap; .rail is inset:0 inside
     it), and padding-block puts a deliberate band back. The height stays for
     the short-viewport case, where the map is height-capped and max-content
     is the taller of the two. */
  .mapf .mf-stick{max-height:max-content;padding-block:clamp(10px,1.6vh,22px)}
  .mapf .story .map{max-height:calc(100dvh - var(--nav-h) - 28px)}
  .mapf .stagewrap{width:min(1440px,100% - var(--pad)*2,
    calc((100dvh - var(--nav-h) - 28px)*2.45))}
}
