/* The three full-bleed sections: testimonial deck, closing portrait,
   and the connect panel over the globe. */
/* ── Recommendations: a cloud of cards on its own white page ─── */
  /* The page's white is a LAYER here, not the section's background. It used to
     be opaque from the section's first pixel, which meant the map went dark at
     prog 3.23 — before the aircraft is even airborne — so the take-off out of
     Riyadh happened behind an opaque page and nobody ever saw it. */
  /* 270svh = 35vh of runway — though the stage's entrance is NOT keyed to
     scroll at all: it rides the aircraft's own position down the route
     (`AIR_IN`/`AIR_OUT` in src/ui/scroll-sections.js), so it lands on the same
     beat of the flight whatever the viewport. This 35vh only has to be long
     enough that the caption has settled before the scroll locks for the
     cards' entrance (which plays on its own clock, not the scroll's)
     + 124vh of exit: the cloud gathers, rises and fades as you scroll on
     + 11vh, which lands the fade's end right where the closing section
     crosses the fold — no empty stage between them.
     Changing this only changes the exit's pace; the lead-in and tail are
     fixed in vh in src/ui/scroll-sections.js, and the flight is pinned by
     data-anchor. (370svh until 11 Sep: the swipe deck's 14 cards × 16vh.) */
  .station.voices-station { display: block; padding: 0; min-height: 270svh;
                            position: relative; background: none; }
  .voices-station::before {
    content: ""; position: absolute; inset: 0; z-index: 0;
    background: var(--bg); opacity: var(--deckPaper, 1);
  }
  .voices-station > * { position: relative; z-index: 1; }
  .voices-sticky {
    position: sticky; top: 0; height: 100svh;
    display: flex; align-items: center; justify-content: center;
    pointer-events: auto;
  }
  .voices { background: none; box-shadow: none; padding: 0; max-width: none;
            display: flex; flex-direction: column; align-items: center; }
  .voices-h { font-size: var(--fs-lead); font-weight: 400; color: var(--ink-soft);
              letter-spacing: var(--ls-lead); margin: 0 0 40px; text-align: center; }

  /* The cloud's stage. Every card is placed inside it by
     src/ui/scroll-sections.js: a tile while it floats (--k 0), the full card
     once opened (--k 1), and every size in between is this one variable —
     the padding, the corner, the portrait, the label and the quote all read
     it, so the tile grows into the card rather than swapping for it. */
  .deck { position: relative; width: min(94vw, 980px); height: min(88svh, 760px);
          touch-action: pan-y; }   /* a sideways drag pans the cloud; up and down still scrolls */
  /* On a touch screen the stage takes the whole drag. `pan-y` let the browser
     claim anything with a vertical component, so a diagonal drag scrolled the
     page 70px and turned the cloud barely at all — "when i orbit top part
     website gets scrolling" (Khusan, 12 Sep). The deck is shorter here to pay
     for it: 76svh inside a 100svh sticky leaves a band about 100px deep above
     and below the cloud where the page still scrolls natively. The sphere
     loses nothing, because on a narrow screen its radius is set by the WIDTH,
     not the height. */
  @media (hover: none) {
    .deck { height: min(76svh, 760px); touch-action: none; }
  }
  .voices-cap {
    position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
    margin: 0; font-size: 11px; letter-spacing: .08em; color: var(--ink);
    white-space: nowrap; pointer-events: none;
  }
  .tcard {
    --kt: 0;                   /* the open body's own fade: in late, out at once */
    --sh: 1;                   /* the drop shadow's weight: one shadow on a pile, not fourteen */
    --k: 0;                    /* 0 the tile … 1 the open card: a crossfade of two layers */
    position: absolute; left: 0; top: 0; width: 118px; height: 182px;
    background: var(--card-solid);
    border-radius: calc(14px + 6px * var(--k));
    overflow: hidden;          /* lets the footer band meet the rounded corners */
    box-shadow: 0 1px 1px rgba(0,0,0,calc(.04 * var(--sh))), 0 10px 34px rgba(0,0,0,calc(.10 * var(--sh)));
    transform-origin: 50% 50%;
    contain: layout style paint;   /* the opening card's reflow stays inside it */
    /* No transition: position is a continuous function of scroll and of the
       pointer, set every frame. A transition here would lag the wheel. */
    will-change: transform, opacity;
    backface-visibility: hidden;
    outline: none;
  }
  .tcard:focus-visible { outline: 3px solid color-mix(in srgb, var(--ink) 30%, transparent); outline-offset: 2px; }

  /* Fourteen promoted layers, each carrying a 34px blur, re-rasterised every
     time the sphere changes their scale. On a phone at DPR 3 that is the
     cloud's real cost — "this part is lagging on the mobile" (Khusan,
     12 Sep) — and the blur is the expensive half. A touch device gets a
     tighter shadow: at the size the cards are there it reads the same, and
     the raster is a fraction of the area. */
  @media (hover: none) {
    .tcard { box-shadow: 0 1px 1px rgba(0,0,0,calc(.04 * var(--sh))),
                         0 4px 12px rgba(0,0,0,calc(.10 * var(--sh))); }
  }
  :root.lite .tcard { box-shadow: 0 1px 1px rgba(0,0,0,calc(.04 * var(--sh))),
                                  0 5px 14px rgba(0,0,0,calc(.10 * var(--sh))); }

  /* The open card's content, ALWAYS laid out at the card's full size — the
     deck sets --fw/--fh once per resize — and centred in whatever size the
     card currently is. The card clips it. Nothing here reflows while a card
     opens, so the quote's lines never re-wrap and the text simply fades in
     where it will finally sit ("Texts are getting expanded/type effect",
     Khusan, 12 Sep). */
  .tbody {
    /* Pinned to the card's TOP LEFT, not centred on it. Centred, the body's
       own portrait slot sits outside a card that is not yet full size — the
       image would have to fly out of the frame to reach it — and the card is
       positioned by its top left while it opens (src/ui/scroll-sections.js)
       so that pinning it here costs no extra drift. */
    position: absolute; left: 0; top: 0;
    width: var(--fw, 412px); height: var(--fh, 566px);
    display: flex; flex-direction: column;
    padding: 26px 26px 0;
    opacity: var(--kt, 0);
    pointer-events: none;
  }
  /* The portrait is NOT part of that fade. There is only one portrait on a
     card — the tile's — and it travels into this slot as the card opens
     (src/ui/scroll-sections.js sets its transform). This one holds the space
     for it and is never drawn; it used to be a second portrait that faded in
     at a different size in a different place, so the image blinked out
     mid-open and reappeared as a thumbnail: "keep the image ... place them on
     it is place" (Khusan, 12 Sep). */
  .tbody > .tface { visibility: hidden; }
  .ttile {
    position: absolute; inset: 0; padding: 10px;
    display: flex; flex-direction: column;
    pointer-events: none;
  }
  /* Only the tile's LABEL fades; its portrait stays, and moves. */
  .ttile .tname { opacity: clamp(0, 1 - var(--k) * 2, 1); }
  .ttile .tface { transform-origin: 0 0; will-change: transform; }
  /* Carries either two initials or a portrait; .has-photo is added by
     src/ui/deck.js only once the image has actually decoded. */
  .tface.has-photo { padding: 0; overflow: hidden; background: var(--surface); }
  .tface img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .tface {
    width: 52px; height: 52px; border-radius: 11px; flex: none;
    background: var(--surface); color: var(--ink-soft);
    display: grid; place-items: center;
    font-size: 15px; font-weight: 600; letter-spacing: .02em;
  }
  /* The tile's portrait fills the tile's width, edge to edge of the padding. */
  .ttile .tface { width: 100%; height: auto; aspect-ratio: 1; border-radius: 14px; font-size: 22px; }
  .tname { margin-top: 6px; }
  /* Two lines at most: a long name wraps rather than ending in dots. */
  .tname b { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
             font-size: 13px; line-height: 1.2; font-weight: 600; letter-spacing: -.012em; color: var(--ink); }
  /* The role too: two lines, never dots ("Senior Product De…" — Khusan). */
  .tname i { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
             margin-top: 2px; font-style: normal; font-size: 11px; line-height: 1.2; color: var(--ink-faint); }
  .tcard p {
    margin: 22px 0 0; flex: 1 1 auto; overflow: hidden;
    font-size: 13.5px; line-height: 1.6; letter-spacing: -.012em; color: var(--ink-soft);
  }
  /* A tinted band, not a rule. Negative margins break it out of the card's
     own 26px padding so it runs edge to edge, and the card's overflow:hidden
     trims it to the corner radius. */
  .tcard footer {
    display: flex; align-items: center; justify-content: space-between; gap: 14px;
    margin: 18px -26px 0; padding: 18px 26px 20px;
    background: var(--surface);
  }
  .twho b { display: block; font-size: 14px; font-weight: 600; letter-spacing: -.012em; color: var(--ink); }
  .twho i { display: block; margin-top: 2px; font-style: normal; font-size: 12.5px; color: var(--ink-faint); }
  /* Holds a wordmark when one loads, and the company's name as text until it
     does — so a missing logo reads as a label, never as a gap. */
  .tco { font-size: 12px; letter-spacing: .04em; color: var(--ink-faint); white-space: nowrap; }
  .tco.has-logo { font-size: 0; }
  .tco img {
    height: auto; width: auto; max-height: 26px; max-width: 92px;
    display: block; opacity: .72;
  }
  /* The wordmarks are dark ink on transparent; on the night band they would
     vanish. Dark mode is hidden today but still wired, so keep it honest. */
  :root[data-theme="dark"] .tco img { filter: invert(1); opacity: .6; }

  /* ── Closing portrait: big type, figure cut out in front ─────── */
  /* 1080svh (300 until 11 Sep): the drive into the headset is 9.3 viewports of scroll from 0.30vh in,
     and the closing line needs a beat of pinned frame after it lands. */
  /* ── The note in Khusan's hand (src/ui/note.js) ─────────────────
     A white sheet pinned for 240svh of scroll, with two drawings written onto
     it in turn. The sheet is the page's own paper (--bg), and the drawings'
     paper is keyed out by the script, so they sit on it seamlessly. Hidden by
     the `hidden` attribute until the script has both files. */
  /* 260svh, from 300 (12 Sep). The writing is a fixed share of the pinned
     scroll, so a shorter box writes the note faster per turn of the wheel —
     "still from canvas to image section scroll feels very small and heavy"
     (Khusan) — and the dead beat after the last stroke goes with it. 260 is
     the floor: below it the portrait, which rises through a full viewport
     before it pins, would start coming up under a pen still writing. */
  .note-station { display: block; position: relative; z-index: 0; padding: 0;
                  min-height: 260svh; background: var(--bg); margin-top: -100svh; }
  /* Overlaps: the note starts a viewport before the deck's section ends, under
     the deck's own layer (z 0 against its 1), so it is already pinned the moment
     the last card has left instead of sliding in over a screen of white.

     The closing section no longer starts early. It used to overlap by 8svh,
     a beat bought back because the pen was damped well behind the scroll and
     the head could come up under an arrow still being drawn. The pen follows
     in 50ms now rather than 125 (src/ui/note.js), and the note's box was cut
     to 260svh, which packs the last stroke right up against the portrait's
     rise: the two meet with about 4svh to spare, and any overlap on top of
     that would put the head back under the pen. Only when the note is shown. */
  .note-station:not([hidden]) + .outro-station { margin-top: 0; }
  .note-station[hidden] { display: none; }
  .note-sticky { position: sticky; top: 0; height: 100svh; overflow: hidden; }
  /* The board (11 Sep): the note is written on a whiteboard now, not a sheet
     — a tinted ground with a dot grid that pans with the pages (the grid is
     on the camera layer), a tool panel, and a canvas for what a visitor
     draws. The drawings' paper is keyed out, so the ink sits on the grid. */
  .note-sheet { position: absolute; inset: 0; background: var(--board, #f4f4f6); }
  /* The camera: a tall layer holding both pages, moved by the script with a
     translate and a small scale. Origin at the top centre, so the scale keeps
     the page centred and the translate alone decides what is in frame. */
  .note-cam { position: absolute; left: 0; right: 0; top: 0; height: 188svh;
              transform-origin: 50% 0; will-change: transform;
              background-image: radial-gradient(circle, var(--board-dot, #cfcfd4) 1px, transparent 1.4px);
              background-size: 22px 22px; background-position: 11px 11px; }
  .board-draw { position: absolute; inset: 0; width: 100%; height: 100%; display: block;
                pointer-events: none; }   /* drawn onto from the sticky's own pointer events */
  /* The tool panel, as a whiteboard's: a white bar at the foot of the frame.
     The pressed tool is tinted; the pencil's own slot is empty while the
     pencil is out writing (it rests there otherwise, see .note-pencil). */
  .board-tools {
    position: absolute; left: 50%; bottom: max(18px, env(safe-area-inset-bottom, 0px) + 12px);
    transform: translateX(-50%); z-index: 6;
    display: flex; align-items: center; gap: 4px; padding: 6px;
    background: var(--card-solid); border-radius: 16px;
    box-shadow: 0 1px 1px rgba(0,0,0,.04), 0 10px 34px rgba(0,0,0,.12);
  }
  .bt { width: 44px; height: 44px; border: 0; border-radius: 11px; background: none; color: var(--ink);
        display: grid; place-items: center; cursor: pointer; padding: 0; -webkit-tap-highlight-color: transparent; }
  .bt svg { width: 24px; height: 24px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; stroke-linecap: round; }
  .bt[aria-pressed="true"] { background: var(--accent, #78a6ec); color: #fff; }
  .bt:focus-visible { outline: 2px solid var(--accent, #78a6ec); outline-offset: 2px; }
  .bt-sep { width: 1px; height: 26px; background: color-mix(in srgb, var(--ink) 12%, transparent); margin: 0 4px; }
  /* The pencil tool's slot: the real pencil stands in it (script-placed); the
     glyph is for where the pencil is still — reduced motion — and is hidden
     otherwise. A pressed slot with the pencil gone reads as "out writing". */
  .bt-pencil .bt-glyph { display: none; }
  .note-sticky.pencil-still .bt-pencil .bt-glyph { display: block; }
  /* Drawing: while a drawing tool is pressed, a finger draws instead of
     scrolling; the hand tool (the default on touch) gives the scroll back. */
  .note-sticky.drawing { touch-action: none; cursor: crosshair; }
  .note-sticky.drawing .board-tools, .note-sticky.drawing .note-cta { cursor: pointer; }
  .note-page { position: absolute; left: 9vw; width: 82vw; height: 78svh; display: block; }
  .note-page:nth-of-type(1) { top: 11svh; }
  .note-page:nth-of-type(2) { top: 99svh; }
  /* The link laid over the drawn "send an email" box. Nothing to see: the box
     is the drawing. Live only once the pen has drawn it. */
  /* The pencil: tip at the left middle of its box; the script sets the transform
     so the tip sits on the pen front, leaning up and to the right like a held
     pencil. Hidden between pages and when the writing is done. */
  /* In the sheet's frame (not the camera's) since 11 Sep: it rests standing
     in the tool panel's pencil slot and flies out to write, scaled with the
     camera while it does (the script sets the transform). z above the board
     canvas and the panel. */
  .note-pencil { position: absolute; left: 0; top: 0; width: clamp(96px, 9.5vw, 150px); height: auto;
                 transform-origin: 0 0; pointer-events: none; opacity: 0; z-index: 7;   /* over the panel: at rest it stands in the slot, cut at the slot's foot */
                 transition: opacity .25s ease; will-change: transform;
                 filter: drop-shadow(0 6px 10px rgba(0, 0, 0, .18)); }
  .note-pencil.on { opacity: 1; }
  .note-sticky.pencil-still .note-pencil { display: none; }
  /* The pencil takes the colour of the ink under it (src/ui/note.js picks the
     class); the change lands mid-hop, faded rather than snapped. */
  .note-pencil polygon, .note-pencil rect { transition: fill .25s ease; }
  /* The pencil in the ink under it: black on the signature page and for the
     drawn box and its label on the message, the message's blue (sampled from
     media/note/message.png) for the rest of it. */
  .note-pencil.black { --pencil: #2b2b2f; --pencil-hi: #4b4b51; --pencil-lo: #141416; --lead: #1a1a1a; }
  .note-pencil.blue { --pencil: #78a6ec; --pencil-hi: #a6c6f6; --pencil-lo: #5484d2; --lead: #6a9ae6; }
  .note-cta { position: absolute; display: block; pointer-events: none; border-radius: 8px; }
  .note-cta.on { pointer-events: auto; }
  .note-cta:focus-visible { outline: 2px solid var(--accent); outline-offset: 6px; }
  @media (prefers-reduced-motion: reduce) {
    .note-station { min-height: auto; padding: 8svh 0; margin-top: 0; }
    .note-station:not([hidden]) + .outro-station { margin-top: 0; }
    .note-sticky { position: static; height: auto; }
    .note-sheet, .note-cam { position: static; height: auto; transform: none !important; }
    .note-page { position: relative; inset: auto; width: 82vw; height: 62svh; margin: 0 auto 4svh; }
    .board-draw, .board-tools, .note-pencil { display: none; }   /* the board is the moving version's */
  }

  .station.outro-station { background: none; display: block; padding: 0;
                           min-height: 550svh; }   /* the drive is 4.22vh from 0.08vh in, PLUS the sticky's own
                                                       viewport, which is never scrolled past: 0.08 + 4.22 + 1.0 + margin.
                                                       Trimming below that cuts the drive short — at 1020svh against a
                                                       9.3vh drive the closing line reached only 0.56 opacity at the foot
                                                       of the page (12 Sep). Re-derive this whenever the drive changes
                                                       length in src/ui/scroll-sections.js. */
  /* dvh, not svh: on a phone the browser's toolbar collapses as you scroll and
     the visible height grows; a sheet cut at the small height left a strip of
     the canvas — the map's sky — showing under the portrait (Khusan, 11 Sep).
     The dynamic height follows the visible area, so the portrait, anchored to
     the sheet's bottom, stays flush with the screen's. */
  .outro-sticky { position: sticky; top: 0; height: 100dvh; overflow: hidden; }
  /* The page's own white is a layer now, not the section background — it has
     to clear so the canvas behind can show the planet. */
  .outro-sticky::before {
    content: ""; position: absolute; inset: 0; z-index: 0;
    background: var(--bg); opacity: var(--paper, 1);
  }

  /* Scroll drives a zoom into the headset that hands over to the closing
     vignette (src/scene/connect.js) the same way the globe hands over to the
     map. An earlier version handed over to VR footage; there is no video now. */
  /* The warp (src/ui/warp.js): between the portrait (2) and the visor lines
     (5). In with the first line, out with the glass (--warp); clipped to the
     visor's glass by the script (clip-path) while the zoom is still driving. */
  .warp { position: absolute; inset: 0; z-index: 4; pointer-events: none;
          width: 100%; height: 100%; display: block;
          opacity: var(--warp, 0); background: #000; }   /* pure dark: the streaks on black, the planet only at the end */
  :root.no-motion .warp { display: none; }
  /* Vignette over the planet, so the type has something to sit against. */
  .vr-lens { position: absolute; inset: 0; z-index: 6; pointer-events: none;
             opacity: var(--vz, 0);
             background: radial-gradient(78% 66% at 50% 50%,
                         rgba(0,0,0,0) 44%, rgba(0,0,0,.30) 76%,
                         rgba(0,0,0,.62) 100%); }
  /* The line and the buttons sit in the lower part of the frame, under the
     planet of light, the way the reference sets its caption over the sea. */
  .connect { position: absolute; inset: 0; z-index: 7; padding: 0 6vw 11svh;
             display: flex; flex-direction: column;
             align-items: center; justify-content: flex-end; gap: 36px;
             text-align: center; opacity: var(--cz, 0); pointer-events: none; }
  .connect > * { pointer-events: auto; }
  /* Scrim. The closing scene is bright and busy — grains and the sea's
     glitter — and a text-shadow alone will not carry white type over it.
     Darkest through the middle where the headline sits and at the foot where
     the credit does. */
  .connect::before {
    content: ""; position: absolute; inset: 0; z-index: -1;
    background:
      linear-gradient(180deg, rgba(0,0,0,.30), rgba(0,0,0,0) 30%,
                              rgba(0,0,0,0) 62%, rgba(0,0,0,.42));
  }
  /* Nothing inside the closing frame should select or drag; the whole surface
     is a control for the planet. */
  .outro-sticky { -webkit-user-select: none; user-select: none; }
  .outro-sticky.grabbable { cursor: grab; }
  .outro-sticky.grabbing  { cursor: grabbing; }
  /* The type must not swallow the drag; the buttons still take their clicks. */
  .connect h2 { pointer-events: none; }
  /* Set exactly as the hero headline: same clamp, weight, tracking and
     leading, so the page opens and closes in one voice. */
  .connect h2 { position: relative; margin: 0; color: #f5f5f7;
                font-size: clamp(34px, 6.4vw, 66px); font-weight: 600;
                letter-spacing: var(--ls-display); line-height: var(--lh-display);
                min-height: 1.06em;
                text-shadow: 0 2px 18px rgba(0, 0, 0, .55),
                             0 6px 60px rgba(0, 0, 0, .45); }
  /* Apple's own pairing on a dark page: one filled blue pill, one plain blue
     text link with a chevron. Frosted glass with a lit edge is the visionOS
     material, not the marketing-page one — it read as neither. */
  .connect .links { gap: 30px; align-items: center; }
  .connect .links a.solid {
    height: auto; padding: 12px 23px; border: none; border-radius: 980px;
    font-size: var(--fs-body); font-weight: 400; line-height: 1.18;
    color: #fff; background: #0071e3; box-shadow: none;
    -webkit-backdrop-filter: none; backdrop-filter: none;
    transition: background .2s ease;
  }
  .connect .links a.solid:hover { background: #0077ed; transform: none; }
  .connect .links a:not(.solid) {
    height: auto; padding: 0; border: none; background: none;
    -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: none;
    font-size: var(--fs-body); font-weight: 400; line-height: 1.18;
    color: #2997ff;                                  /* Apple's dark-mode blue */
    transition: opacity .2s ease;
  }
  .connect .links a:not(.solid)::after {
    content: "\203A"; margin-left: 5px; font-size: 1.15em; line-height: 0;
    position: relative; top: .06em;
  }
  .connect .links a:not(.solid):hover { opacity: .78; transform: none; }
  .credits.on-video { color: rgba(255, 255, 255, .72); }
  .outro {
    position: relative; margin: 0 auto; max-width: min(94vw, 1180px);
    min-height: 100svh; padding: 12vh 0 0; text-align: center;
    background: none; box-shadow: none;
    /* Driven from scroll, staggered head -> figure -> links. Default 1 so the
       section is never invisible before the module runs. */
    --rv-h: 1; --rv-p: 1;
  }
  /* Outranks .card / .card.in, which would otherwise hide the whole section. */
  .station.outro-station .card.outro { opacity: 1; transform: none; }
  .voices-sticky { will-change: transform, opacity; }
  /* .card h2 (0,1,1) outranks a bare .outro-h (0,1,0) — hence the matching
     specificity here, or the headline silently renders at card-title size. */
  /* The headline lives inside the visor now (.visor-copy); the block stays for
     assistive tech only. Specificity matches the rule below, which would otherwise
     put it back on screen. */
  .outro .outro-h.visually-hidden {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; opacity: 0; transform: none;
  }
  /* Inside the glasses. Anchored on the point the portrait scales about — the
     visor's centre, which holds still on screen through the zoom — and sized to
     the visor's width, so the three lines sit in the lens at every zoom. Set in
     the display face and weight; smaller, because it is a screen inside a
     photograph, not the page's headline. Each line lifts in with a little blur,
     the way a display comes on. */
  .visor-copy {
    position: absolute; left: 0; top: 0; z-index: 5; pointer-events: none;
    width: var(--vw, 60vw);
    transform: translate(var(--vx, 50vw), var(--vy, 50vh)) translate(-50%, -50%);
    text-align: center; color: #f5f5f7;
    /* Bolder and bigger since 11 Sep (Khusan: "make titles more bold", then
       "bigger"): the lines play on a full-frame black now, over the warp —
       display size, tight, with the warp's own blue in the glow. */
    font-size: clamp(28px, calc(var(--vw, 600px) * 0.09), 100px); font-weight: 800;
    letter-spacing: -.035em; line-height: 1.02;
    text-shadow: 0 0 48px rgba(120, 170, 255, .60), 0 0 12px rgba(160, 200, 255, .35), 0 2px 18px rgba(0, 0, 0, .6);
  }
  /* All three lines on the SAME spot — the block is one line tall and each span
     is stacked on it — so a line replaces the one before it in place, and the
     crossfade between two is a dissolve, not two lines briefly stacked. */
  .visor-copy { height: 1.12em; }
  /* Each line arrives out of the depth: a little small and soft, lifting
     into focus — and leaves the same way. */
  .visor-copy span {
    position: absolute; left: 0; right: 0; top: 0;
    display: block; opacity: var(--l, 0);
    transform: translateY(calc((1 - var(--l, 0)) * 18px)) scale(calc(0.94 + 0.06 * var(--l, 0)));
    filter: blur(calc((1 - var(--l, 0)) * 8px));
  }
  :root.lite .visor-copy span { filter: none; }   /* an animated blur re-rasterises every frame */
  .visor-copy span:nth-child(1) { --l: var(--l1, 0); }
  .visor-copy span:nth-child(2) { --l: var(--l2, 0); }
  .visor-copy span:nth-child(3) { --l: var(--l3, 0); }
  :root.no-gl .visor-copy, :root.no-motion .visor-copy { display: none; }
  :root.no-gl .outro .outro-h.visually-hidden, :root.no-motion .outro .outro-h.visually-hidden {
    position: relative; width: auto; height: auto; overflow: visible; clip: auto;
    white-space: normal; opacity: 1;
  }

  .outro .outro-h {
    position: relative; z-index: 1; margin: 0;
    font-size: clamp(34px, 7.6vw, 104px); font-weight: 600;
    letter-spacing: -.042em; line-height: .98;
    opacity: var(--rv-h);
    transform: translateY(calc((1 - var(--rv-h)) * 30px));
  }
  /* Sized by HEIGHT, not width — that's what guarantees it reaches up into
     the type on a tall viewport instead of stranding itself at the bottom.
     Masked at the base so it dissolves into the page rather than hard-cutting. */
  /* Sized by HEIGHT, not width — that's what guarantees it reaches up into
     the type on a tall viewport instead of stranding itself at the bottom.
     Masked at the base so it dissolves into the page rather than hard-cutting.
     The wrapper exists so the dust canvas can share its box. */
  .portrait-wrap {
    position: absolute; left: 50%; bottom: 0;
    height: min(74svh, 660px); z-index: 2; pointer-events: auto;
    opacity: var(--rv-p);
    /* Pivot on the headset, so scaling up drives straight into the lens. */
    transform-origin: 50% 43.6%;
    /* --rv-in lifts the portrait while its sheet is still rising into the
       frame, so the head rides the sheet's leading edge instead of arriving
       half a screen behind it. Driven by the scroll itself, below; the script
       sets it only where view timelines are missing. */
    transform: translateX(-50%) translateY(calc((1 - var(--rv-p)) * 42px - var(--rv-in, 0px)))
               scale(var(--pz, 1));
  }
  /* The lift is a pure function of where the sheet is, so it runs on a view
     timeline of the section — as a TRANSFORM on a wrapper of its own, which
     the compositor animates at the display's rate, in the same frame as the
     sheet it rides — rather than being written by the script on each scroll
     event: the sheet is moved by the compositor, and a main thread held up by
     a heavy frame (the closer's scene, a 120 Hz display) left the head a step
     behind the sheet, moving in jumps. (A custom property on a scroll
     timeline would still be resolved on the main thread; only transform and
     opacity leave it.) The entry range runs from the section's top at the
     bottom of the frame to its top at the top, which is exactly when the
     sheet pins. The easing is the script's smoothstep, near enough. Script
     fallback where view timelines are missing: lift() in
     src/ui/scroll-sections.js sets --rv-in on the wrap, and is skipped when
     this applies. */
  .portrait-lift { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
  @supports (animation-timeline: view()) {
    .station.outro-station { view-timeline: --outro block; }
    .portrait-lift { animation: rv-lift cubic-bezier(.33, 0, .67, 1) both;
                     animation-timeline: --outro; animation-range: entry 0% entry 100%;
                     will-change: transform; }
    @keyframes rv-lift { from { transform: translateY(-30svh); } to { transform: translateY(0); } }
  }
  .portrait-cut {
    display: block; height: 100%; width: auto;
    /* Belt and braces with draggable="false" in the markup — Safari honours
       -webkit-user-drag, and without it the portrait is what you grab when you
       try to spin the planet. */
    -webkit-user-drag: none; -webkit-touch-callout: none;
    filter: grayscale(1) contrast(1.06);
    -webkit-mask-image: linear-gradient(to bottom, #000 76%, transparent 99%);
    mask-image: linear-gradient(to bottom, #000 76%, transparent 99%);
  }
  .dust { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }

  /* CC-BY requires the credit to travel with the work. */
  /* Bottom-RIGHT: the scroll cue lives bottom-left and a centred credit ran
     straight through it. */
  .credits { position: absolute; right: 22px; bottom: 16px;
             z-index: 9; margin: 0; max-width: min(64vw, 420px); text-align: right;
             font-size: 10.5px; line-height: 1.5; letter-spacing: .03em;
             color: var(--ink-faint); }
  .credits a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
  /* A disclosure: closed, the two words and a chevron; open, the list grows
     upward from the same corner (the box is anchored at the bottom) and fades
     in. No marker, no box, no border — the mono line as it was. */
  .credits summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center;
                     gap: 7px; user-select: none; transition: opacity .2s ease; }
  .credits summary::-webkit-details-marker { display: none; }
  .credits summary:hover { opacity: .78; }
  .credits summary::after { content: ''; width: 5px; height: 5px; margin-top: -3px;
                            border-right: 1px solid currentColor; border-bottom: 1px solid currentColor;
                            transform: rotate(45deg); transition: transform .25s ease; }
  .credits[open] summary::after { transform: rotate(-135deg); margin-top: 3px; }
  .credits ul { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 5px;
                animation: credits-in .3s ease both; }
  .credits .credits-note { opacity: .7; }
  @keyframes credits-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

  .outro.no-portrait .portrait-wrap { display: none; }
  .links { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; }
  .links a { display: inline-flex; align-items: center; height: 40px; padding: 0 22px;
             border-radius: 980px;
             font-size: var(--fs-sub); font-weight: 400; letter-spacing: var(--ls-body);
             text-decoration: none; border: 1px solid var(--hair); color: var(--accent);
             transition: transform .16s cubic-bezier(.32,.9,.24,1), border-color .2s; }
  .links a:hover { transform: translateY(-2px); border-color: var(--accent); }
  .links a.solid { background: var(--accent); border-color: var(--accent); color: #fff; }
  :root[data-theme="dark"] .links a.solid { color: #000; }

  :root.no-gl #gl { display: none; }

  @media (max-width: 720px) {
    .hud-bl { bottom: 18px; left: 18px; }
    /* The visor's lines: the visor fills a phone, so the line's size, which
       follows the visor's width, hit its desktop cap and sat too big on the
       face. A lower cap on narrow screens. */
    .visor-copy { font-size: clamp(16px, calc(var(--vw, 600px) * 0.072), 30px); }
    .station { padding: 88px 18px; }
    .st-left, .st-right { justify-content: center; }
    .station { align-items: flex-end; }
    .station[data-station="0"] { align-items: flex-start; }
    .card { max-width: 100%; }
    .outro { padding-top: 9vh; }
    .portrait-wrap { height: min(56svh, 430px); }
  }
  @media (prefers-reduced-motion: reduce) {
    .card { opacity: 1 !important; transform: none !important; }
    .outro { --rv-h: 1; --rv-p: 1; --rv-in: 0px; }
    .portrait-lift { animation: none; }
    .station.outro-station { min-height: auto; }
    .outro-sticky { position: static; height: auto; }
    .connect { position: static; opacity: 1; padding: 14vh 6vw; background: #0b0b0d; }
    .scrollcue i::after { animation: none; }
  }
