/* The three full-bleed sections: testimonial deck, closing portrait,
   and the connect panel over the globe. */
/* ── Recommendations: a swipe deck 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. */
  /* 370svh = 35vh of runway — though the stack'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 card has settled before the cards start moving.
     + 224vh of cards (16vh each, all 14 of them swiping off)
     + 13vh, which lands the last card's exit right where the closing section
     crosses the fold — no empty stage between them.
     Changing this only changes the per-card pace; the lead-in and tail are
     fixed in vh in src/ui/scroll-sections.js, and the flight is pinned by
     data-anchor. */
  .station.voices-station { display: block; padding: 0; min-height: 370svh;
                            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; }

  .deck { position: relative; width: min(86vw, 412px); height: min(74svh, 566px); }
  .tcard {
    position: absolute; inset: 0;
    display: flex; flex-direction: column;
    background: var(--card-solid); border-radius: 20px;
    padding: 26px 26px 0;
    overflow: hidden;          /* lets the footer band meet the rounded corners */
    box-shadow: var(--shadow);
    transform-origin: 50% 60%;
    /* No transition: position is a continuous function of scroll, set every
       frame. A transition here would lag the wheel and restart mid-flight. */
    will-change: transform, opacity;
    backface-visibility: hidden;
  }
  /* 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;
  }
  .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 ─────── */
  /* 300svh: the drive into the headset is 1.5 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. */
  .note-station { display: block; position: relative; z-index: 0; padding: 0;
                  min-height: 300svh; 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 in turn starts 8svh before the note ends: the arrow is
     drawn 20svh before, and the pen is damped behind the scroll, so a fast
     scroll needs the extra beat or the head comes up under an arrow still
     being drawn. Only when the note is shown. */
  .note-station:not([hidden]) + .outro-station { margin-top: -8svh; }
  .note-station[hidden] { display: none; }
  .note-sticky { position: sticky; top: 0; height: 100svh; overflow: hidden; }
  .note-sheet { position: absolute; inset: 0; }
  /* 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; }
  .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. */
  .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;
                 transition: opacity .25s ease; will-change: transform;
                 filter: drop-shadow(0 6px 10px rgba(0, 0, 0, .18)); }
  .note-pencil.on { opacity: 1; }
  /* 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; }
  }

  .station.outro-station { background: none; display: block; padding: 0;
                           min-height: 300svh; }
  .outro-sticky { position: sticky; top: 0; height: 100svh; 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. */
  /* 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;
    font-size: clamp(18px, calc(var(--vw, 600px) * 0.072), 46px); font-weight: 600;
    letter-spacing: var(--ls-display); line-height: 1.12;
    text-shadow: 0 0 22px rgba(120, 170, 255, .55), 0 2px 14px rgba(0, 0, 0, .55);
  }
  /* 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; }
  .visor-copy span {
    position: absolute; left: 0; right: 0; top: 0;
    display: block; opacity: var(--l, 0);
    transform: translateY(calc((1 - var(--l, 0)) * 10px));
    filter: blur(calc((1 - var(--l, 0)) * 5px));
  }
  :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; }
    .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; }
  }
