  .ft-root {
    --bg-fallback: #EDE5E0; /* mid-tone of the mockup background, shown only until bg-field.png loads */
    --ink: #1C1614;
    --ink-soft: #2A2321;
    --green: #0D8043;        /* Docufen Compliance Green, brand-guidelines skill */
    --green-dark: #0A6A37;
    --card-border: #DCD3CC;
    --card-line: #E6DFD9;
  }
  .ft-root * { box-sizing: border-box; }
  .ft-root, .ft-root { margin: 0; height: 100%; }
  .ft-root {
    font-family: "Manrope", "Open Sans", system-ui, sans-serif;
    color: var(--ink);
    /* background lifted straight from 01_docufen-hero-mockup-03-app-palette.jpg (text and building masked out and filled), no colour fitting */
    background-color: var(--bg-fallback);
    background-image: url("/flythrough/assets/bg-field.png");
    background-size: cover;
    background-position: center;
    background-attachment: fixed;
    -webkit-font-smoothing: antialiased;
  }
  .ft-root .hero {
    position: relative;
    min-height: 100vh;
    overflow: hidden;
    display: grid;
    grid-template-columns: minmax(0, 50fr) minmax(0, 50fr);
    grid-template-rows: auto 1fr;
    grid-template-areas: "nav nav" "content building";
    padding-bottom: clamp(32px, 5vw, 80px);
    column-gap: clamp(16px, 3vw, 56px);
    padding: 0 clamp(20px, 7.8vw, 158px);
    max-width: 2000px;
    margin: 0 auto;
  }
  .ft-root .nav { grid-area: nav; }
  .ft-root .content { grid-area: content; }

  /* nav */
  .ft-root .nav {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    padding-top: clamp(24px, 4.2vw, 58px);
    font-size: clamp(13px, 0.95vw, 19px);
    font-weight: 500;
  }
  .ft-root .logo { display: inline-flex; align-items: center; gap: 10px; font-weight: 500; font-size: clamp(20px, 1.5vw, 30px); color: var(--ink); text-decoration: none; }
  .ft-root .logo .mark { width: 1.45em; height: 1.45em; display: block; }
  .ft-root .logo .word { height: 1.0em; width: auto; display: block; }
  .ft-root .menu { display: flex; gap: clamp(14px, 1.3vw, 28px); list-style: none; margin: 0; padding: 0; justify-self: center; }
  .ft-root .menu li { display: flex; align-items: center; gap: clamp(14px, 1.3vw, 28px); white-space: nowrap; }
  .ft-root .menu li + li::before { content: "•"; opacity: .8; }
  .ft-root .menu a { color: var(--ink); text-decoration: none; }
  .ft-root .tools { justify-self: end; display: flex; align-items: center; gap: clamp(14px, 1.4vw, 30px); }
  .ft-root .tools svg { width: 1.3em; height: 1.3em; stroke: var(--ink); fill: none; stroke-width: 1.8; }
  .ft-root .lang { display: inline-flex; align-items: center; gap: 6px; }
  .ft-root .signin { color: #fff; background: var(--green); text-decoration: none; white-space: nowrap; border: 1.5px solid var(--green); border-radius: 10px; padding: .42em 1em; font-size: .88em; font-weight: 600; }
  .ft-root .signin:hover { background: #0A6A37; border-color: #0A6A37; color: #fff; }
  @media (max-width: 1200px) { .ft-root .signin { display: none; } }
  .ft-root .lang svg { width: .9em; height: .9em; }

  /* body */
  .ft-root .content {
    position: relative;
    z-index: 2;
    align-self: center;
    padding-bottom: clamp(40px, 7vw, 120px);   /* lifts the block so its centre sits on the building's mass, not its plinth */
  }
  .ft-root h1 {
    font-family: "Instrument Serif", "Playfair Display", Georgia, serif;
    font-weight: 400;
    font-size: clamp(40px, 4.9vw, 98px);
    text-wrap: balance;
    line-height: .98;
    letter-spacing: -.01em;
    margin: clamp(18px, 2.4vw, 46px) 0 clamp(16px, 2vw, 40px);
  }
  .ft-root h1 em { font-style: italic; }
  .ft-root .lede { font-size: clamp(15px, 1.35vw, 27px); line-height: 1.4; max-width: 34em; text-wrap: pretty; margin: 0 0 clamp(22px, 2.6vw, 52px); color: var(--ink-soft); }
  .ft-root .cta {
    display: inline-flex; align-items: stretch;
    background: var(--green); color: #fff; text-decoration: none;
    border-radius: 12px; overflow: hidden;
    font-weight: 600; letter-spacing: .02em; text-transform: uppercase;
    font-size: clamp(14px, 1.15vw, 23px);
    height: clamp(48px, 3.6vw, 72px);
    box-shadow: 0 10px 28px rgba(13,128,67,.22);
  }
  .ft-root .cta .icon { background: #F7F3EF; display: grid; place-items: center; width: 3.2em; }
  .ft-root .cta .icon svg { width: 1.5em; height: 1.5em; stroke: var(--green-dark); fill: none; stroke-width: 1.6; }
  .ft-root .cta .label { display: flex; align-items: center; padding: 0 1.4em; }
  .ft-root .cta:hover { background: var(--green-dark); }


  /* building */
  .ft-root .building {
    grid-area: building;
    position: relative; z-index: 1;
    align-self: center; justify-self: end;
    width: 116%;
    max-width: 1250px;
    margin-right: -4vw;
    pointer-events: none; user-select: none;
  }
  .ft-root .building img { width: 100%; height: auto; display: block; }
  @media (max-width: 1000px) {
    .ft-root .hero {
      grid-template-columns: 1fr;
      grid-template-areas: "nav" "content" "building";
      min-height: 0;
    }
    .ft-root .menu { display: none; }
    .ft-root .nav { grid-template-columns: 1fr auto; }
    .ft-root .content { max-width: 640px; }
    .ft-root .building { justify-self: center; margin: clamp(8px, 3vw, 32px) 0 0; max-width: 760px; }
  }
  @media (max-width: 700px) {
    .ft-root .hero { padding: 0 20px; }
    .ft-root .nav { padding-top: 20px; }
    .ft-root h1 { font-size: clamp(38px, 11vw, 56px); }
    .ft-root .lede { font-size: 17px; }
    .ft-root .cta { display: flex; width: 100%; }
    .ft-root .cta .label { flex: 1; justify-content: center; }
  }

  /* sky colour sampled from the top edge of Final-building-v4.png, served as assets/building-front.webp (2560 wide) with a jpg fallback (12 Sep 2026): #F3E1D5 */
  .ft-root { background-color: #F5E4D7; background-image: none; height: auto; min-height: 100%; } /* body must grow with content or the sticky nav stops after one screen */
  /* ---- stacked variant (12 Sep 2026): hero text on top, full-width building below, one column ---- */
  .ft-root .hero {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto;
    grid-template-areas: "content";
    column-gap: 0;
    padding-bottom: 0;
    min-height: 0;                    /* the fly-through section starts right under the button */
  }
  .ft-root .content {
    align-self: start;
    justify-self: center;
    text-align: center;
    padding: clamp(28px, 4vw, 64px) 0 clamp(20px, 2.6vw, 44px);
    max-width: 56em;
  }
  .ft-root h1 { margin-top: 0; }
  .ft-root .lede { margin-left: auto; margin-right: auto; max-width: 40em; }
  .ft-root .building {
    align-self: end; justify-self: center;
    /* full viewport width: break out of the hero's side padding and max-width */
    width: 100vw;
    max-width: none;
    justify-self: start;
    margin: 0 0 0 calc(50% - 50vw);
    /* natural height: the top of the building fills the bottom half of the first screen,
       scrolling reveals the whole picture (the scroll is where the fly-through will start) */
  }
  .ft-root .building img {
    width: 100%; height: auto;
    display: block;
    border-radius: 0;
  }
  .ft-root .after { height: 0; } /* was 40vh of breathing room; the real site puts its footer here instead (17 Sep 2026) */
  @media (max-width: 1000px) {
  }
  @media (max-width: 700px) {
    .ft-root .content { text-align: left; }
    .ft-root .lede { margin-left: 0; }
  }

  /* ---- sticky solid nav (12 Sep 2026; glass removed 15 Sep 2026) ----
     Always the page background colour (#F5E4D7, same as body), no blur, no fade, no border.
     Only the padding tightens once the page has scrolled. */
  .ft-root .hero { overflow: visible; }                 /* overflow:hidden would defeat position:sticky */
  .ft-root .nav {
    position: sticky; top: 0; z-index: 20;        /* lives outside .hero so it sticks for the whole page */
    width: 100%; margin-left: 0;
    padding: clamp(24px, 4.2vw, 58px) clamp(20px, 6.5vw, 158px) 0;
    column-gap: clamp(16px, 2.2vw, 48px);
    background: #F5E4D7;
    transition: padding .35s ease;
  }
  .ft-root .nav.scrolled {
    padding-top: clamp(12px, 1.2vw, 18px);
    padding-bottom: clamp(12px, 1.2vw, 18px);
  }
  @media (max-width: 700px) { .ft-root .nav { padding-left: 20px; padding-right: 20px; } }

  /* ---- scroll-scrubbed fly-through (13 Sep 2026) ----
     The video's first frame IS the building picture. It sits full width right under the hero
     and scrolls normally like any image. The moment its bottom edge reaches the bottom of the
     viewport it sticks, and the next 200vh of scrolling scrubs the video from the building to
     the Word screen. Then the page continues. */
  .ft-root { --fly-h: min(62.5vw, calc(100vh - var(--nav-h, 0px))); --fly-scrub: 200vh; --fly-hold: 70vh; }   /* 16:10 since the v4 flight (15 Sep 2026): the frames are 2560 x 1600. Capped at the space under the nav, or the bottom of the frame (and the red bar) sits below the window on wide, short screens (1400 x 900: 875px frame under a 64px nav) */
  /* --fly-hold is scroll length AFTER the scrub has finished. The frame stays stuck through it
     so the coffee-steam loop has somewhere to play; without it the section released on the same
     pixel the scrub ended and the loop was never on screen. */
  /* --dive-scrub (16 Sep 2026): a SECOND scrub after the hold. The end card at the bottom right of
     the desk recording grows into the whole frame and the dive from the building to Grady at the
     vessel scrubs through it (assets/dive-frames, 241 frames from
     06_storyboard-v4-visuals-higgsfield/05-dive-building-to-vessel-kling3-4k-10s-v1.mp4). */
  .ft-root { --dive-scrub: 260vh; --ipad-hold: 160vh; }   /* the hold was 100vh; 160vh from 16 Sep 2026 so a scroller who keeps going still sees the recording start, like the desk's --fly-hold */
  /* --mv-scrub (16 Sep 2026): a THIRD scrub after the iPad hold, beat 05 to 06. The iPad and hand fade out,
     the vessel fades in and the flight from the mixing vessel through the pipe into the bottle and up to the
     operator's hand scrubs through (assets/mv-frames, 168 frames sampled unevenly from
     06_storyboard-v4-visuals-higgsfield/02mv2bottle/video-frames-for-website-scrolling/mv-to-bottle-stitched-3parts-b.mp4:
     every 2nd frame, the pipe run every 4th to 5th so it travels faster, one stray seam frame dropped). --mv-hold
     keeps the last frame (bottle in the gloved hand) stuck a moment before the section releases to the Joni beat. */
  .ft-root { --mv-scrub: 220vh; --mv-hold: 160vh; }   /* the hold is where the phone and hand rise in and play */
  .ft-root .fly { position: relative; height: calc(var(--fly-h) + var(--fly-scrub) + var(--fly-hold) + var(--dive-scrub) + var(--ipad-hold) + var(--mv-scrub) + var(--mv-hold) + var(--drop-scrub) + var(--drop-hold) + var(--sh-scrub) + var(--sh-hold) + var(--roof-scrub) + var(--roof-hold)); }
  .ft-root .mv-canvas { position: absolute; inset: 0; z-index: 11; display: none; opacity: 0; background: #2a211c; pointer-events: none; }
  .ft-root .mv-canvas.on { display: block; }
  .ft-root .mv-canvas canvas { display: block; width: 100%; height: 100%; }
  /* --drop-scrub (17 Sep 2026): a FOURTH scrub after the phone hold, beat 06 to 07. As it starts the phone, hand and
     veil fade out (iphoneStage multiplies by 1 - dropFade()), leaving the bottle-in-hand still, and the drop through
     the floor slab past the purified water loop to the desktop monitor scrubs through (assets/drop-frames, 109 frames
     from 06_storyboard-v4-visuals-higgsfield/06-07_drop-through-the-floor-slab.mp4: every 2nd frame, the static tail
     from source frame 192 every 4th). */
  .ft-root { --drop-scrub: 200vh; --drop-hold: 160vh; }   /* the hold is where the note stage rises in and plays */
  .ft-root #drop { z-index: 13; }
  /* --sh-scrub (17 Sep 2026): a FIFTH scrub after the note hold, beat 07 to 08. The note stage fades out, a shoulder
     wipes across and the camera lands on Patti's laptop in the lab (assets/shoulder-frames, 96 frames from
     06_storyboard-v4-visuals-higgsfield/03_QA-to-Patti/07-08-two-shoulders-kling3-4k-10s-v1.mp4). The frames are
     sampled by measured motion, not evenly: every 4th where the clip is nearly still (source 0-19, 140-199, 220-240),
     every 2nd through the wipe and the small move at the end. --sh-hold is where the Patti stage rises in and plays. */
  .ft-root { --sh-scrub: 190vh; --sh-hold: 160vh; }
  .ft-root #shoulder { z-index: 15; }
  /* --roof-scrub (17 Sep 2026): the SIXTH and LAST scrub, beats 09 to 10 and the close of the whole home. Patti's
     stage fades out, her hands close the laptop lid and the closed lid match-cuts to the building's rooftop seen from
     above (assets/roof-frames, 91 frames from 06_storyboard-v4-visuals-higgsfield/04_Patti-to-rooftop/
     09-10-laptop-lid-to-rooftop-matchcut-kling3-4k-10s-v3.mp4, 3852 x 2152 cropped to 16:10 at x 204, width 3443).
     Sampled by measured motion: every 3rd frame through the approach, every 2nd through the close and the match cut
     where motion peaks, every 5th once it has settled. --roof-hold is where the closing still and its copy rest. */
  .ft-root { --roof-scrub: 180vh; --roof-hold: 160vh; }
  .ft-root #roof { z-index: 17; }
  /* the closing still (assets/rooftop-16x10.webp, from rooftop-match-laptop-16x9.png) is the same shot as the clip's
     last frame to within 0.2 % but at full render quality, so the copy rests on a clean image. The pale rooftop fills
     the left 77 % of the frame and is deliberately empty: that is the text area. */
  .ft-root #roofStage { position: absolute; inset: 0; z-index: 18; display: none; pointer-events: none;
    background: #2a211c center / cover no-repeat; opacity: 0; transition: opacity 1.1s ease; }
  .ft-root #roofStage.on { display: block; }
  .ft-root #roofStage.in { opacity: 1; }
  .ft-root #roofStage.live { pointer-events: auto; }
  .ft-root .roof-copy {
    position: absolute; left: 6%; top: 50%; transform: translateY(-50%) translateY(14px); width: 58%;
    color: var(--ink); opacity: 0; transition: opacity 1s ease .35s, transform 1s ease .35s;
  }
  .ft-root #roofStage.in .roof-copy { opacity: 1; transform: translateY(-50%); }
  .ft-root .roof-copy h2 { margin: 0 0 .5em; font-size: clamp(26px, 3.4vw, 62px); line-height: 1.05; letter-spacing: -.02em; }
  .ft-root .roof-copy p { margin: 0 0 1.1em; font-size: clamp(15px, 1.35vw, 26px); line-height: 1.45; max-width: 26em; }
  .ft-root .roof-copy .cta { font-size: clamp(13px, 1.05vw, 20px); height: clamp(44px, 3.2vw, 64px); }
  /* Closing faceplate (17 Sep 2026, Vincent: "below the start 14 day trial we could have the face and
     6 usb c and to the right the text that explains what each is"). A two column grid: column 1 holds the
     six ports, column 2 the labels, so a taller label row keeps its port aligned automatically. The plate
     is a separate element spanning column 1 behind the ports. All six labels are plain text nodes, ready
     to be lifted into the docufen_web i18n bundle at port time. */
  .ft-root .roof-ports {
    --rp-pw: clamp(30px, 2.5vw, 46px); --rp-padx: clamp(9px, .8vw, 15px); --rp-pady: clamp(9px, .8vw, 15px);
    --rp-aw: clamp(44px, 3.6vw, 66px); --rp-gap: clamp(14px, 1.2vw, 24px);
    display: grid; grid-template-columns: var(--rp-aw) var(--rp-pw) 1fr; column-gap: var(--rp-gap);
    row-gap: clamp(8px, .7vw, 15px); align-items: center;
    /* top margin clears the antenna, which sticks out above the adaptor and otherwise hits the CTA */
    margin-top: clamp(52px, 4.4vw, 82px);
    max-width: 30em; position: relative;
  }
  /* the plate is taken OUT of grid flow: as a grid item spanning column 1 it pushed every port into an
     implicit row below it (17 Sep 2026). Absolute inside the relatively positioned .roof-ports instead. */
  .ft-root .rp-plate {
    position: absolute; z-index: 0; pointer-events: none;
    left: calc(var(--rp-aw) + var(--rp-gap) - var(--rp-padx)); width: calc(var(--rp-pw) + 2 * var(--rp-padx));
    top: calc(-1 * var(--rp-pady)); bottom: calc(-1 * var(--rp-pady));
    border: 1px solid var(--card-border); border-radius: clamp(10px, .9vw, 16px);
    background: rgba(255, 255, 255, .28); box-shadow: 0 1px 0 rgba(255, 255, 255, .5) inset;
  }
  .ft-root .rp-port {
    grid-column: 2; position: relative; z-index: 2; display: block; justify-self: center;
    width: var(--rp-pw); height: clamp(11px, .95vw, 17px); border-radius: 999px;
    background: var(--ink-soft); border: 1.5px solid var(--green);
    box-shadow: 0 0 6px rgba(13, 128, 67, .5), 0 1px 2px rgba(0, 0, 0, .3) inset;
  }
  .ft-root .rp-label { grid-column: 3; font-size: clamp(13px, 1.05vw, 20px); line-height: 1.3; }
  /* the lead from the adaptor into each port. Drawn as the port's own ::before so it always sits on the
     row's centre line, whatever height a translated label makes that row (17 Sep 2026). */
  .ft-root .rp-port::before {
    content: ''; position: absolute; top: 50%; right: 100%; width: calc(var(--rp-gap) + var(--rp-padx));
    height: 2.5px; margin-top: -1.25px; border-radius: 2px; background: var(--ink-soft); opacity: .85;
  }
  /* the Docufen adaptor: one device every port is plugged into, transmitting from the roof. */
  .ft-root .rp-adaptor {
    position: absolute; z-index: 1; left: 0; width: var(--rp-aw);
    top: calc(-1 * var(--rp-pady)); bottom: calc(-1 * var(--rp-pady));
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .5em;
    border-radius: clamp(10px, .9vw, 16px); background: var(--ink);
    box-shadow: 0 6px 18px rgba(28, 22, 20, .28), 0 0 0 1px rgba(255, 255, 255, .06) inset;
  }
  .ft-root .rp-adaptor img { width: 58%; max-width: 30px; display: block; }
  .ft-root .rp-adaptor .rp-word {
    writing-mode: vertical-rl; transform: rotate(180deg); color: #F5E4D7;
    font-size: clamp(11px, .85vw, 15px); font-weight: 600; letter-spacing: .06em;
  }
  .ft-root .rp-ant { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); width: clamp(34px, 2.8vw, 52px); display: block; }
  .ft-root .rp-ant .rp-wave { animation: rp-pulse 2.6s ease-in-out infinite; transform-origin: 50% 100%; }
  .ft-root .rp-ant .rp-wave:nth-of-type(2) { animation-delay: .45s; }
  @keyframes rp-pulse { 0%, 100% { opacity: .25; } 50% { opacity: 1; } }
  @media (prefers-reduced-motion: reduce) { .ft-root .rp-ant .rp-wave { animation: none; opacity: .7; } }
  .ft-root .rp-sub { display: block; margin-top: .25em; font-size: .78em; line-height: 1.35; opacity: .72; }
  @media (max-width: 700px) {
    /* the closing beat takes the whole window on a phone, with the same --bleed the device beats use
       (defined on .fly-sticky). The rooftop still is a background-image with cover, so it simply
       re-crops to portrait, and the copy then has room for the headline, the line, the button AND the
       adaptor at a readable size - in the 62.5vw band none of them fit. The one thing to look at is the
       cross-fade from the last scrub frame, which is still in the band, to this still, which is not: on
       a phone it reads as a slow push in rather than a straight dissolve. */
    /* 26em, not a bare 88% (17 Sep 2026). The headline is at its maximum size from about 490 px of
       viewport upward while the copy box keeps widening, so between roughly 500 and 700 px it fitted on
       one line and ran off the pale left side of the rooftop into the trees, where it cannot be read.
       Capping the measure makes it wrap instead. Under about 470 px the 88% is still the smaller of the
       two, so nothing changes on a phone. */
    .ft-root .roof-copy { left: 6%; width: min(88%, 26em); top: 50%; }
    .ft-root .roof-copy h2 { font-size: clamp(24px, 7vw, 34px); }
    .ft-root .roof-copy p { font-size: clamp(14px, 3.8vw, 17px); }
    .ft-root .roof-ports { margin-top: clamp(34px, 9vw, 52px); }
  }
  /* iPad in the frame (16 Sep 2026, Vincent: "that's when the iPad should appear"): the iPad no longer
     has its own section. It lives in #ipadStage inside the sticky box, above the dive canvas, fades in
     over the dive's last frames and holds for --ipad-hold of scroll before the section releases. The
     .beat-ipad rules still size it (the stage carries that class). */
  .ft-root .ipad-stage {
    position: absolute; inset: 0; z-index: 9; display: none;
    align-items: center; justify-content: flex-start;
    opacity: 0; pointer-events: none;
  }
  .ft-root .ipad-stage.on { display: flex; }
  .ft-root .ipad-stage.live { pointer-events: auto; }
  .ft-root .ipad-stage .fly-hint { z-index: 10; }
  /* the gloved hand, take 2 of the composite (16 Sep 2026): the CSS iPad is back (correct 3:4 screen, even thin
     bezel, exactly as before the glove) and the hand is a separate keyed layer, assets/glove-hand.webp
     (1792 x 2400), which is the generated left hand with its OWN tablet removed: green, magenta and the
     tablet's dark pixels inside its rectangle are transparent, the glove and sleeve remain. The CSS iPad is
     placed where that tablet was (centre x 56.95 %, top 11.42 %, height 63.92 % of the image), so the thumb
     lands on our bezel and the sleeve leaves the frame bottom left. Layer order: iPad bezel, recording,
     hand, then the click glass, bar and glyph on top. The stretched glove-ipad.webp is gone. */
  .ft-root .ipad-stage .ipad-hand { position: absolute; left: -3.3%; top: -8.4%; height: 135%; aspect-ratio: 1792 / 2400; }
  .ft-root .ipad-stage .ipad-hand .ipad { position: absolute; left: 25.67%; top: 11.42%; height: calc(63.92% - var(--fly-h) * 0.048); margin: 0; }   /* left edge on the drawn tablet's left edge, so the thumb sits on the bezel; ours is a touch wider to the right */
  .ft-root .ipad-stage .ipad-hand img.hand { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 2; pointer-events: none; display: block; }
  .ft-root .ipad-stage .ipad-hand .screen video, .ft-root .ipad-stage .ipad-hand #gradyVideo { position: relative; z-index: 1; }
  .ft-root .ipad-stage .ipad-hand .screen .fly-seek, .ft-root .ipad-stage .ipad-hand .screen .fly-progress { z-index: 6; }
  .ft-root .ipad-stage .ipad-hand .screen .fly-glass { z-index: 5; }   /* UNDER the seek strip: at the same z the glass, later in the DOM, took every click on the bar (Vincent, 16 Sep 2026: "couldn't fast forward") */
  .ft-root .dive-canvas {
    position: absolute; z-index: 8; display: none; overflow: hidden;
    box-shadow: 0 16px 60px rgba(0,0,0,.47); background: #2a211c;
    pointer-events: none;
  }
  .ft-root .dive-canvas.on { display: block; }
  .ft-root .dive-canvas::after { content: ""; position: absolute; inset: 0; border-radius: inherit; box-shadow: inset 0 0 0 2px #ECEAE7; opacity: var(--dive-border, 1); }
  .ft-root .dive-canvas canvas { display: block; width: 100%; height: 100%; }
  .ft-root .fly-sticky {
    position: sticky;
    /* centred in the space under the solid nav (15 Sep 2026). --nav-h is set by script from the
       nav's live height. Was bottom-aligned (top: 100vh - fly-h); that left an empty band above the
       frame on tall windows and let the nav clip the top of the picture on very wide ones. */
    top: max(var(--nav-h, 0px), calc((100vh + var(--nav-h, 0px) - var(--fly-h)) / 2));
    height: var(--fly-h);
    /* the box keeps the frames' 16:10 shape (15 Sep 2026): when the height is capped by a short
       window the box narrows and centres, page colour either side, rather than cropping the frame
       top and bottom, which hid the Word menu bar under the nav */
    width: min(100%, calc(var(--fly-h) * 1.6));
    margin: 0 auto;
    overflow: hidden;
    background: #F5E4D7;
  }
  .ft-root .fly-sticky video, .ft-root .fly-sticky canvas.fly-canvas {
    display: block; width: 100%; height: 100%;
    object-fit: cover; object-position: center bottom;
    pointer-events: none;
  }
  /* canvas scrub (15 Sep 2026): the poster shows until frame 0 has loaded, drawn with the same
     cover / centre-bottom rule the frames use, so the hand-over is invisible */
  .ft-root .fly-sticky canvas.fly-canvas { background: #F5E4D7; }   /* the poster url was dropped on 17 Sep 2026: flythrough-02-poster.jpg has never existed, in the port or the prototype, so it 404d on every load. Frame 0 arrives quickly and is drawn with the same rule. */
  .ft-root .after { height: 0; }
  /* 0, not 60vh (17 Sep 2026). In the standalone prototype the page ended here, so .after was empty
     scroll room below the film. docufen_web has a real footer, and 60vh of it read as a band of empty
     cream between the rooftop and the footer. At 0 the frame butts onto the footer. Nothing in the
     film's timing reads .after: every length comes from .fly and the sticky box. */

  /* ---- hero-to-video blend (13 Sep 2026) ----
     The video frames' sky is a touch greyer than the page and darkens quickly below the top edge,
     so a hard join shows as a line. Two layers: the hero fades toward the frame's top colour from
     the button downward, and a soft veil in the page colour sits over the top of the frame. */
  .ft-root .hero { background: #F5E4D7; }
  .ft-root .content { padding-bottom: clamp(56px, 7vw, 120px); }
  .ft-root .fly-sticky::before {
    content: ""; position: absolute; left: 0; right: 0; top: 0; z-index: 2; pointer-events: none;
    height: 7%;
    opacity: var(--veil, 1); transition: opacity .15s linear;   /* fades out once the fly-through starts, so it never fogs the frame mid-flight */
    background: linear-gradient(180deg, #F5E4D7 0%, rgba(245,228,215,.7) 35%, rgba(245,228,215,0) 100%);
  }
  .ft-root .fly-sticky video, .ft-root .fly-sticky canvas.fly-canvas { position: relative; z-index: 1; }

  /* ---- coffee-steam hold (14 Sep 2026) ----
     The scrub ends on the reviewer's desk. Rather than freeze on a dead still, a short
     silent loop of the same shot plays on top, so only the steam moves. It sits directly
     over the last frame and cross-fades in once the scrub is finished. */
  /* NB the selector must out-specify `.fly-sticky video` above (0,1,1), or that rule's
     position:relative wins and this layer drops out of the sticky box entirely. */
  .ft-root .fly-sticky video.fly-steam {
    position: absolute; inset: 0; z-index: 2;
    width: 100%; height: 100%; object-fit: cover; display: block;
    opacity: 0; transition: opacity .45s ease;
    pointer-events: none;
  }
  .ft-root .fly-sticky video.fly-steam.on { opacity: 1; }

  /* ---- Word screen recording on the monitor (15 Sep 2026) ----
     Once the scrub has landed on the desk, the real screen recording plays inside the monitor.
     Box measured on the last frame of flythrough-01.mp4 (1920 x 1080): screen glass at
     x 363..1556, y 90..822, i.e. the percentages below. The frame is object-fit: cover with
     object-position: center bottom, so the percentages hold as long as the box keeps 16:9;
     --fly-h is min(56.25vw, 100vh), which it does on every window wider than it is tall. */
  .ft-root .fly-sticky video.fly-screen {
    position: absolute; z-index: 3;
    /* left/top/width/height are set by layoutScreen() in the script from the frame's real drawn
       area, because object-fit: cover crops the fly-through whenever the sticky box is not 16:9 */
    object-fit: cover; display: block;
    opacity: 0; transition: opacity .35s ease;
    pointer-events: none;
  }
  .ft-root .fly-sticky video.fly-screen.on { opacity: 1; }

  /* ---- progress line + replay control for the recording (15 Sep 2026) ----
     Both are absolutely positioned by layoutScreen() to the same glass box as the video.
     The line runs along the bottom edge of the glass while the clip plays, so the reader can
     see that something is playing and how long is left. The replay button appears on hover
     over the glass once the clip has started, and a click restarts it. */
  .ft-root .fly-glass {
    position: absolute; z-index: 4; display: block;
    opacity: 0; transition: opacity .35s ease;
    pointer-events: none;
  }
  .ft-root .fly-glass.on { opacity: 1; pointer-events: auto; }
  /* progress line moved to the bottom edge of the whole frame, under the keyboard, and made
     Docufen green since 16 Sep 2026 (was YouTube red) */
  /* YouTube placement (15 Sep 2026): lifted off the bottom edge and inset from the sides, on a
     soft dark gradient so it reads over the bright desk, with a round playhead at the end. */
  .ft-root .fly-progress {
    position: absolute; z-index: 5;
    left: clamp(14px, 1.6vw, 28px); right: clamp(14px, 1.6vw, 28px); bottom: clamp(18px, 2.4vw, 42px);
    height: 4px; border-radius: 2px;
    background: rgba(255,255,255,.32);
    opacity: 0; transition: opacity .35s ease;
    pointer-events: none;
  }
  .ft-root .fly-progress.on { opacity: 1; }
  .ft-root .fly-progress i {
    position: absolute; left: 0; top: 0; bottom: 0; width: 0;
    background: var(--green); border-radius: 2px;   /* Docufen green, was YouTube red (Vincent, 16 Sep 2026) */
    will-change: width;
  }
  .ft-root .fly-progress i::after {                       /* the playhead */
    content: ""; position: absolute; right: -7px; top: 50%; transform: translateY(-50%);
    width: 14px; height: 14px; border-radius: 50%; background: var(--green);
  }
  .ft-root .fly-progress::before {                        /* dark gradient under the bar, like a player's control strip */
    content: ""; position: absolute; z-index: -1; pointer-events: none;
    left: calc(-1 * clamp(14px, 1.6vw, 28px)); right: calc(-1 * clamp(14px, 1.6vw, 28px));
    bottom: calc(-1 * clamp(18px, 2.4vw, 42px)); height: clamp(80px, 12vh, 140px);
    background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.45) 100%);
  }
  /* player controls (15 Sep 2026, Vincent): the always-visible replay arrow is gone. Click anywhere on
     the recording to pause / play (a glyph flashes in the centre, YouTube style, and stays while
     paused or ended); click or drag the red bar to seek; space toggles, left / right arrows skip 5 s. */
  .ft-root .fly-glass { cursor: pointer; }
  .ft-root .fly-flash {
    position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%) scale(.9);
    width: clamp(56px, 6vw, 104px); height: clamp(56px, 6vw, 104px);
    border-radius: 50%; background: rgba(28,22,20,.72);
    display: grid; place-items: center; pointer-events: none;
    opacity: 0; transition: opacity .35s ease, transform .35s ease;
  }
  .ft-root .fly-flash svg { width: 46%; height: 46%; fill: #fff; }
  .ft-root .fly-flash .ic-play, .ft-root .fly-flash .ic-pause { display: none; }
  .ft-root .fly-flash.play .ic-play, .ft-root .fly-flash.pause .ic-pause { display: block; }
  .ft-root .fly-flash.show { opacity: 1; transform: translate(-50%, -50%) scale(1); }   /* held while paused or ended */
  .ft-root .fly-flash.blip { animation: flyblip .7s ease forwards; }                    /* one-off pulse on resume */
  @keyframes flyblip { 0% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.25); } }
  /* scroll hint (16 Sep 2026, Vincent): when the recording ends the centre glyph says "click me", not
     "scroll on", so a small pill appears bottom centre one second after the end and goes the moment
     the page scrolls. Never shown before the end; hidden under 700 px with the card. */
  .ft-root .fly-hint {
    position: absolute; z-index: 6; left: 50%; bottom: calc(clamp(18px, 2.4vw, 42px) + 22px);
    transform: translate(-50%, 8px);
    display: inline-flex; align-items: center; gap: 8px;
    padding: 9px 16px 9px 18px; border-radius: 999px;
    background: #fff; color: #1C1614; box-shadow: 0 0 0 2px #ECEAE7, 0 6px 22px rgba(0,0,0,.28);
    font-weight: 600; font-size: 14px; line-height: 1; white-space: nowrap;
    opacity: 0; transition: opacity .35s ease-out, transform .35s ease-out; pointer-events: none;
  }
  .ft-root .fly-hint.on { opacity: 1; transform: translate(-50%, 0); }
  .ft-root .fly-hint svg { width: 16px; height: 16px; stroke: var(--green); fill: none; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
  .ft-root .fly-hint.on svg { animation: flyhint 1.4s ease-in-out infinite; }
  @keyframes flyhint { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(4px); } }
  @media (max-width: 700px) { .ft-root .fly-hint { display: none; } }
  .ft-root .fly-seek {                                    /* invisible grab strip over the red bar, easier to hit than a 4px line */
    position: absolute; z-index: 6; cursor: pointer; touch-action: none;
    left: clamp(14px, 1.6vw, 28px); right: clamp(14px, 1.6vw, 28px);
    bottom: calc(clamp(18px, 2.4vw, 42px) - 14px); height: 32px;
    opacity: 0; pointer-events: none;
  }
  .ft-root .fly-seek.on { pointer-events: auto; }
  .ft-root .fly-seek:hover ~ .fly-progress i::after, .ft-root .fly-seek.drag ~ .fly-progress i::after { transform: translateY(-50%) scale(1.3); }
  .ft-root .fly-progress i::after { transition: transform .15s ease; }

  /* ---- beat 4 picture-in-picture card (16 Sep 2026) ----
     While the Add Participants dialog is open in the desk recording, a card with the front-on
     building sits bottom right of the frame and a white pin with the Docufen icon drops onto each
     person when their name gets its tick. HTML over the video, driven by the recording's own
     currentTime (see pipCues in the script). Geometry, crop, anchors and timings from the handoff
     01_brain/sessions/claude-code/2026-09-16-beat4-pip-card-handoff.md; card image from
     06_storyboard-v4-visuals-higgsfield/04-pip-mockups/. The recording covers the whole frame, so
     the card's percentages are of the sticky box. */
  .ft-root .pip {
    position: absolute; z-index: 4;
    left: 67.34%; top: 53.42%; width: 31.61%; height: 44.92%;
    border-radius: 4.28%/4.82%;                 /* 26 px of 607 x 539 */
    overflow: hidden;
    box-shadow: 0 16px 60px rgba(0,0,0,.47);
    opacity: 0; transition: opacity .35s ease-out; pointer-events: none;
  }
  .ft-root .pip.on { opacity: 1; }
  .ft-root .pip img { display: block; width: 100%; height: 100%; object-fit: cover; }
  .ft-root .pip::after {                                 /* 2 px warm border inside the rounded edge */
    content: ""; position: absolute; inset: 0; border-radius: inherit;
    box-shadow: inset 0 0 0 2px #ECEAE7; pointer-events: none;
  }
  .ft-root .pip svg.pins { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
  .ft-root .pip .pin { opacity: 0; transform-box: fill-box; transform-origin: 50% 100%; }
  .ft-root .pip .pin.on { opacity: 1; animation: pipdrop .42s cubic-bezier(.22,1,.36,1) both; }
  .ft-root .pip .pin .chip { opacity: 0; }
  .ft-root .pip .pin.on .chip { animation: pipchip .15s ease-out .12s both; }
  @keyframes pipdrop { from { transform: translateY(-18px) scale(.85); } to { transform: none; } }
  @keyframes pipchip { from { opacity: 0; } to { opacity: 1; } }
  .ft-root .pip .pin text { font-family: "Manrope", "Open Sans", system-ui, sans-serif; font-weight: 600; font-size: 14.8px; fill: #1C1614; }
  /* end card (16 Sep 2026): a second card at the end of Segment A, the building with Grady on the
     platform by the mixing vessel, pins baked into the image. 1920 x 1200 frame: x 1140, y 705,
     w 760, h 475 (20 px from the right and bottom edges), 16:10. Same treatment as the beat 4 card;
     no pin animation, it fades in as one picture and stays until the clip ends. Image from
     04-pip-mockups/pip-end-card-16x10-grady-on-stairs-pins@2x.png, geometry in pip-end-card-16x10.json. */
  .ft-root .pip.pip-end {
    left: 59.38%; top: 58.75%; width: 39.58%; height: 39.58%;
    border-radius: 3.42%/5.47%;                 /* 26 px of 760 x 475 */
  }
  @media (max-width: 700px) { .ft-root .pip { display: none; } }

  /* ---- theatre mode (15 Sep 2026) ----
     The lights go down while the Word recording plays: page, nav, and the band around the frame
     fade to the site's warm near-black, nav text and dots flip to light, and everything fades
     back the moment the reader scrolls up out of the desk. Same trigger as the recording, via
     the `theatre` class on <html>. Cue for "something is playing", and it makes the red bar pop. */
  .ft-root, .ft-root .nav, .ft-root .fly-sticky, .ft-root .hero { transition: background-color .55s ease; }
  .ft-root .logo, .ft-root .menu a, .ft-root .lang, .ft-root .signin, .ft-root .tools svg { transition: color .55s ease, stroke .55s ease, filter .55s ease; }
  html.theatre .ft-root, html.theatre .ft-root .hero, html.theatre .ft-root .nav, html.theatre .ft-root .fly-sticky { background-color: var(--ink); }
  html.theatre .ft-root .nav { color: #F5E4D7; }
  html.theatre .ft-root .logo, html.theatre .ft-root .menu a, html.theatre .ft-root .lang { color: #F5E4D7; }
  html.theatre .ft-root .tools svg { stroke: #F5E4D7; }
  html.theatre .ft-root .logo .word { filter: brightness(0) invert(1) sepia(.15) saturate(.6) brightness(1.05); }  /* logo word is a dark svg */
  html.theatre .ft-root .menu li + li::before { color: #F5E4D7; }
  html.theatre .ft-root .fly-sticky::before { opacity: 0 !important; }
  html.theatre .ft-root .beats button { border-color: rgba(245,228,215,.55); }
  html.theatre .ft-root .beats button:hover { border-color: #F5E4D7; }
  html.theatre .ft-root .beats span { background: var(--ink); color: #F5E4D7; }
  /* ---- the site header on the home page (17 Sep 2026) ----
     docufen_web's header is transparent until you scroll and then takes the page background with a
     shadow. On the home page the film owns the colour instead: the header is the film's own cream, and
     it goes dark with the rest of the page while a recording is playing, which is what the prototype's
     own .nav did. The rules above cannot do it, because the header is docufen_web's and sits outside
     .ft-root, so these are matched on <html>. They are gated on .fly-page, which FlyThrough.tsx adds
     while the film is mounted and takes off again when it is not, so no other page is touched. */
  html.fly-page header { background-color: #F5E4D7; box-shadow: none; }
  html.fly-page.theatre header { background-color: #1C1614; }
  html.fly-page.theatre header a, html.fly-page.theatre header button, html.fly-page.theatre header span { color: #F5E4D7; }
  html.fly-page.theatre header img[alt="Docufen Logo"] { filter: brightness(0) invert(1) sepia(.15) saturate(.6) brightness(1.05); }

  /* ---- beat dots (15 Sep 2026) ----
     Small fixed dots on the right edge, one per beat of the story. Click jumps to that beat with
     a smooth scroll, so a reader can go back to the hero or forward to the desk without riding
     the whole fly-through. The list of beats is built by script (beats[] below) so new sections
     only need a new entry there. Hidden on narrow screens. */
  .ft-root .beats {
    position: fixed; right: clamp(14px, 1.6vw, 28px); top: 50%; transform: translateY(-50%);
    z-index: 30; margin: 0; padding: 0; list-style: none;
    display: flex; flex-direction: column; gap: 14px;
  }
  .ft-root .beats li { position: relative; }
  .ft-root .beats button {
    display: block; width: 10px; height: 10px; border-radius: 50%;
    border: 1.5px solid rgba(28,22,20,.45); background: transparent;
    padding: 0; cursor: pointer;
    transition: background .2s ease, border-color .2s ease, transform .2s ease;
  }
  .ft-root .beats button:hover { border-color: var(--ink); transform: scale(1.25); }
  .ft-root .beats li.active button { background: var(--green); border-color: var(--green); }
  .ft-root .beats span {
    position: absolute; right: 22px; top: 50%; transform: translateY(-50%);
    white-space: nowrap; font-size: 12px; font-weight: 500; color: var(--ink);
    background: #F5E4D7; padding: 3px 8px; border-radius: 6px;
    opacity: 0; pointer-events: none; transition: opacity .15s ease;
  }
  .ft-root .beats li:hover span { opacity: 1; }
  @media (max-width: 700px) { .ft-root .beats { display: none; } }
  /* ---- story beats after the desk (15 Sep 2026) ----
     .beat is one full-height section per story beat. .beat-blank is a placeholder for the next
     scrolling animation (not built yet). .beat-video centres a portrait screen recording that
     plays, muted and looping, only while it is on screen (script below). Theatre mode stays on
     from the desk onwards, so these sit on the dark ground. */
  .ft-root .beat { position: relative; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
  .ft-root .beat-blank { min-height: 100vh; }
  .ft-root .beat-video video { display: block; width: min(90vw, 1600px); max-height: 86vh; height: auto; border-radius: 18px; background: #000; box-shadow: 0 30px 80px rgba(0,0,0,.45); }
  .ft-root .beat-video video.portrait, .ft-root #gradyVideo { height: min(86vh, 1600px); max-width: 90vw; width: auto; }
  @media (max-width: 700px) { .ft-root .beat-video video, .ft-root .beat-video video.portrait { height: auto; width: 90vw; max-height: none; } }
  /* #gradyVideo was in the rule above until 17 Sep 2026. It is the iPad recording, and it has not been a
     .beat-video since the iPad moved into the fly frame: 90vw made it wider than the iPad holding it. */
  /* ---- Grady iPad beat (16 Sep 2026, first cut) ----
     The beat is a 16:10 box, same sizing rule as the fly-through frame, with the operator's view of the
     open mixing vessel as the background (assets/vessel-pov-16x10.jpg, cut from
     06_storyboard-v4-visuals-higgsfield/05-END-frame-pov-vessel-manhole-open-v4; the picture still carries
     Vincent's red mark-ups, to be replaced when the final frame lands). An iPad drawn in CSS sits on the
     left, as tall as the box allows, with the Segment B recording (1200 x 1600, 3:4, exactly an iPad
     screen) inside it. Under 700 px the iPad is centred and the background stays behind it. */
  .ft-root .beat-ipad {
    --ipad-h: min(calc(100vh - var(--nav-h, 64px)), calc(100vw / 1.6));
    height: var(--ipad-h); min-height: 0; width: min(100%, calc(var(--ipad-h) * 1.6)); margin: 0 auto;
    background: #2a211c center / cover no-repeat;
    display: flex; align-items: center; justify-content: flex-start;
  }
  .ft-root .ipad-stage.beat-ipad { height: auto; width: auto; margin: 0; background: none; overflow: visible; --ipad-h: var(--fly-h); display: none; }
  .ft-root .ipad-stage.beat-ipad.on { display: flex; }
  .ft-root .beat-ipad { overflow: hidden; }
  .ft-root .beat-ipad .ipad {
    position: relative; height: 93%; box-sizing: content-box; display: block;   /* the SCREEN is 93 % of the box, bezel on top of that fits inside; the bezel adds outside it (16 Sep 2026: "even bigger") */
    margin-left: 12%;                            /* off centre to the left, but not hugging the edge (Vincent, 16 Sep 2026) */
    padding: calc(var(--ipad-h) * 0.024);         /* bezel: an even band in px, so the screen keeps exactly 3:4 */
    border-radius: 5.2% / 3.9%;
    background: #14110F;
    box-shadow: 0 0 0 1.5px #3a3532, 0 40px 90px rgba(0,0,0,.55), inset 0 0 0 1px rgba(255,255,255,.08);
  }
  .ft-root .beat-ipad .ipad::before {                      /* front camera */
    content: ""; position: absolute; left: 50%; top: 1.6%; width: 1.4%; aspect-ratio: 1; border-radius: 50%;
    transform: translateX(-50%); background: #0a0908; box-shadow: inset 0 0 0 1px #2a2624;
  }
  .ft-root .beat-ipad .screen { position: relative; height: 100%; aspect-ratio: 3 / 4; border-radius: 12px; overflow: hidden; background: #000; }
  /* the iPad recording has the desk recording's controls (16 Sep 2026): click to play / pause with the
     centre glyph, a green bar along the bottom of the screen that takes click and drag, space / k,
     left / right arrows, and the scroll pill when it ends. Same elements, scoped to the screen. */
  .ft-root .beat-ipad .screen .fly-glass { inset: 0; opacity: 1; pointer-events: auto; }
  .ft-root .beat-ipad .screen .fly-progress { left: 6%; right: 6%; bottom: 4%; opacity: 1; }
  .ft-root .beat-ipad .screen .fly-progress::before { left: -7%; right: -7%; bottom: -5%; height: 22%; }
  .ft-root .beat-ipad .screen .fly-seek { left: 6%; right: 6%; bottom: calc(4% - 14px); pointer-events: auto; }
  .ft-root .beat-ipad .fly-hint { bottom: 26px; left: auto; right: 4%; transform: translate(0, 8px); }   /* bottom right, over the room, clear of the iPad */
  .ft-root .beat-ipad .fly-hint.on { transform: none; }
  /* ---- Joni iPhone XS beat (16 Sep 2026) ----
     Same box as the iPad beat (class beat-ipad carries the sizing and the scoped controls), background
     assets/bottle-pov-16x10.jpg (operator holding the bottle at the filling line, centre crop of
     02_storyboard-v3-visuals-higgsfield/06-START-frame-bottle-in-hand-no-phone.png). An iPhone XS drawn
     in CSS sits dead centre in front of the bottle: black band, big corner radius, the notch, screen
     738 x 1600 (the Segment C recording, which is the XS's own 1125 x 2436 ratio). */
  .ft-root .beat-iphone { justify-content: center; }
  .ft-root #iphoneStage { z-index: 12; }   /* the phone beat lives in the fly frame over the vessel-to-bottle scrub (16 Sep 2026), like the iPad over the dive */
  .ft-root #iphoneStage::before {           /* a dim veil between the still and the phone, so the reader sees they have entered viewing mode (Vincent, 16 Sep 2026) */
    content: ""; position: absolute; inset: 0; background: rgba(22, 15, 10, .42); opacity: 0; transition: opacity .9s ease; pointer-events: none;
  }
  .ft-root #iphoneStage.in::before { opacity: 1; }
  .ft-root .beat-iphone .iphone {
    position: relative; height: 88%; box-sizing: content-box; display: block; margin: 0;
    padding: calc(var(--ipad-h) * 0.014);          /* the XS band is thin and even all round */
    border-radius: calc(var(--ipad-h) * 0.075);
    background: #0B0A09;
    box-shadow: 0 0 0 1.5px #4a4441, 0 40px 90px rgba(0,0,0,.55), inset 0 0 0 1px rgba(255,255,255,.1);
  }
  .ft-root .beat-iphone .screen { aspect-ratio: 738 / 1600; border-radius: calc(var(--ipad-h) * 0.062); }
  .ft-root .beat-iphone .screen::before {                  /* the notch */
    content: ""; position: absolute; z-index: 7; left: 50%; top: 0; transform: translateX(-50%);
    width: 56%; height: 3.6%; background: #0B0A09; border-radius: 0 0 20px 20px; pointer-events: none;
  }
  .ft-root .beat-iphone .screen .fly-progress { left: 7%; right: 7%; bottom: 5%; }
  .ft-root .beat-iphone .screen .fly-seek { left: 7%; right: 7%; bottom: calc(5% - 14px); }
  .ft-root .beat-iphone .fly-hint { left: auto; right: 4%; }
  /* Gloved left hand holding the phone (16 Sep 2026): assets/glove-phone-frame.webp is the drawn phone bezel AND the hand
     (1792 x 2400, only green and the magenta screen keyed out in Python); the CSS band and notch are switched off here. In that image the phone screen sits at x 613..1173,
     y 321..1545, so with the screen at 0.72 * --ipad-h high the hand is 1.9608 * that tall and is placed so its
     screen hole lands on our CSS iPhone (band 0.014 * --ipad-h all round). The hand sits ABOVE the phone
     (fingertips on the left edge, thumb on the right) and never takes clicks. */
  .ft-root .beat-iphone .iphone-hand { position: relative; height: 72%; aspect-ratio: 738 / 1600; box-sizing: content-box; padding: calc(var(--ipad-h) * 0.014);
    opacity: 0; transform: translateY(5%); transition: opacity .9s ease, transform .9s ease; }   /* phone + hand arrive after the background, like the iPad */
  .ft-root .beat-iphone.in .iphone-hand { opacity: 1; transform: none; }
  .ft-root .beat-iphone .iphone-hand .iphone { position: absolute; height: calc(var(--ipad-h) * 0.7200); width: calc(var(--ipad-h) * 0.3294);
    left: calc(var(--ipad-h) * 0.0140); top: calc(var(--ipad-h) * 0.0140); padding: 0; background: none; box-shadow: none; border-radius: 0; }
    /* the drawn bezel is the phone now: this box is the drawn screen hole (image px 615..1171 x 323..1543, corner radius
       ~60 px, k = 1.4118 / 2400 --ipad-h per px) grown by 2 px so no hairline shows; the video is clipped by the
       rounded .screen (overflow hidden). No mask-image here on purpose: Safari draws <video> in its own layer and a
       mask on an ancestor blanked it (16 Sep 2026). */
  .ft-root .beat-iphone .iphone-hand .screen { height: 100%; aspect-ratio: auto; width: 100%; border-radius: calc(var(--ipad-h) * 0.036); overflow: hidden; }
  .ft-root .beat-iphone .iphone-hand .screen::before { display: none; }   /* the drawn phone has its own notch */
  .ft-root .beat-iphone .iphone-hand #joniVideo { object-fit: cover; width: 100%; height: 100%; }
  .ft-root .beat-iphone .iphone-hand img.hand { position: absolute; z-index: 2; pointer-events: none; display: block;
    height: calc(var(--ipad-h) * 1.4118); width: auto; max-width: none; left: calc(var(--ipad-h) * -0.3466); top: calc(var(--ipad-h) * -0.1748); }
    /* max-width: none is the port's doing (17 Sep 2026). The standalone prototype had no global image
       rule, but docufen_web's Tailwind preflight sets img { max-width: 100% }, which clamped this image
       to its containing block (301 px instead of its natural 881 px) while the height stayed keyed to
       --ipad-h. The drawn phone and hand were squashed horizontally and sat well left of the recording.
       With this line the drawn screen hole lands on the video rect to within 1 px. */
  .ft-root .beat-iphone .iphone-hand .screen video, .ft-root .beat-iphone .iphone-hand #joniVideo { position: relative; z-index: 1; }
  .ft-root .beat-iphone .iphone-hand .screen .fly-seek, .ft-root .beat-iphone .iphone-hand .screen .fly-progress { z-index: 6; }
  /* same scrim fix as the note beat (17 Sep 2026): the bar's own ::before pulled back by a viewport clamp and
     stopped 2 px short of the screen on each side, so the strip is the screen's ::after instead and is clipped to
     the phone's rounded corners. */
  .ft-root #iphoneStage .screen .fly-progress::before { display: none; }
  .ft-root #iphoneStage .screen::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 16%; z-index: 4; pointer-events: none;
    background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.45) 100%);
  }
  .ft-root .beat-iphone .iphone-hand .screen .fly-glass { z-index: 5; }
  @media (max-width: 700px) {
    /* The phone comes out of the film band on a narrow screen: see the note over the iPad's own mobile
       block below. The drawn gloved hand goes, because it is about as wide as the whole window and it
       would leave the recording a finger wide, so the CSS phone and its notch do the job again. The
       width is capped against the window HEIGHT as well, or a short window clips the phone's feet. */
    .ft-root .beat-iphone .iphone-hand {
      position: relative; height: auto; aspect-ratio: auto; padding: 0;
      width: min(60vw, calc((100vh - var(--nav-h, 64px)) * 0.40));
    }
    .ft-root .beat-iphone .iphone-hand img.hand { display: none; }
    .ft-root .beat-iphone .iphone-hand .iphone {
      position: relative; left: auto; top: auto; width: auto; height: auto;
      padding: clamp(6px, 2vw, 10px); background: #0B0A09;
      border-radius: clamp(28px, 9vw, 44px); box-shadow: 0 0 0 1.5px #4a4441, 0 24px 60px rgba(0, 0, 0, .5);
    }
    .ft-root .beat-iphone .iphone-hand .screen {
      aspect-ratio: 738 / 1600; height: auto; width: 100%; border-radius: clamp(22px, 7.5vw, 36px);
    }
    .ft-root .beat-iphone .iphone-hand .screen::before { display: block; }   /* the CSS notch is back */
  }
  /* ---- the QA manager note, inside the fly frame (17 Sep 2026) ----
     Vincent: "07 should be a seamless transition from the last frame of the scrolly flythrough". So the note beat is
     NOT its own section with its own picture: it is a stage inside the fly sticky box, over the drop's LAST FRAME,
     which already shows the monitor on the desk. Nothing about the picture changes when the beat arrives; the room
     dims a little and the recording fades up inside the monitor. The screen rectangle was measured off the bezel in
     assets/drop-frames/p-108.webp (2560 x 1600: right edge 2094, top 46, bottom 1399, and the screen runs 70 px off
     the left edge of the 16:10 crop) = left -2.734 %, top 2.875 %, 84.531 % wide, 84.563 % tall, which is 1.600,
     exactly the 1920 x 1200 recording. The frames are 16:10 and so is the box, so the canvas draws them 1:1 and
     these percentages hold at every size. */
  .ft-root #noteStage { position: absolute; inset: 0; z-index: 14; display: none; pointer-events: none; }
  .ft-root #noteStage.on { display: block; }
  .ft-root #noteStage.live { pointer-events: auto; }
  .ft-root #noteStage::before {          /* dims the room around the monitor, never the screen itself */
    content: ""; position: absolute; inset: 0; z-index: 0; background: rgba(22, 15, 10, .42);
    opacity: 0; transition: opacity .9s ease; pointer-events: none;
  }
  .ft-root #noteStage.in::before { opacity: 1; }
  .ft-root #noteStage .screen {
    position: absolute; left: -2.734%; top: 2.875%; width: 84.531%; height: 84.563%;
    z-index: 1; overflow: hidden; background: #000; opacity: 0; transition: opacity .9s ease;
  }
  .ft-root #noteStage.in .screen { opacity: 1; }
  .ft-root #noteStage .screen video, .ft-root #noteStage #henriettaVideo {
    display: block; width: 100%; height: 100%; object-fit: cover; position: relative; z-index: 1;
    border-radius: 0; box-shadow: none; max-width: none; max-height: none;
  }
  .ft-root #noteStage .screen .fly-glass { inset: 0; opacity: 1; pointer-events: auto; z-index: 5; }
  .ft-root #noteStage .screen .fly-progress { left: 4%; right: 4%; bottom: 4%; opacity: 1; z-index: 6; }
  /* the dark strip under the bar is the SCREEN's own ::after here, not the bar's ::before (17 Sep 2026, Vincent:
     "make it the correct width so that it has the same width as the video"). The base ::before pulls back by a
     viewport-sized clamp, which cannot cancel a percentage inset, so it stopped 22 px short of the video on each
     side. On the screen it spans the video edge to edge by construction. */
  .ft-root #noteStage .screen .fly-progress::before { display: none; }
  .ft-root #noteStage .screen::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 16%; z-index: 4; pointer-events: none;
    background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.45) 100%);
  }
  .ft-root #noteStage .screen .fly-seek { left: 4%; right: 4%; bottom: calc(4% - 14px); pointer-events: auto; z-index: 6; }
  .ft-root #noteStage .fly-hint { z-index: 10; }
  /* ---- Patti's review, inside the fly frame (17 Sep 2026) ----
     Same build as the note beat, one frame further on: the recording goes into the laptop that is already in the
     shoulder clip's last frame, so nothing about the picture changes when the beat arrives. Screen rectangle measured
     off the bezel in assets/shoulder-frames/s-095.webp (2560 x 1600: right 2128, top 30, bottom 1413, running 85 px
     off the left of the 16:10 crop) = left -3.320 %, top 1.875 %, 86.445 % wide, 86.438 % tall, which is 1.600,
     exactly the 1920 x 1200 recording. */
  .ft-root #pattiStage { position: absolute; inset: 0; z-index: 16; display: none; pointer-events: none; }
  .ft-root #pattiStage.on { display: block; }
  .ft-root #pattiStage.live { pointer-events: auto; }
  .ft-root #pattiStage::before {
    content: ""; position: absolute; inset: 0; z-index: 0; background: rgba(22, 15, 10, .42);
    opacity: 0; transition: opacity .9s ease; pointer-events: none;
  }
  .ft-root #pattiStage.in::before { opacity: 1; }
  .ft-root #pattiStage .screen {
    position: absolute; left: -3.320%; top: 1.875%; width: 86.445%; height: 86.438%;
    z-index: 1; overflow: hidden; background: #000; opacity: 0; transition: opacity .9s ease;
  }
  .ft-root #pattiStage.in .screen { opacity: 1; }
  .ft-root #pattiStage .screen video, .ft-root #pattiStage #pattiVideo {
    display: block; width: 100%; height: 100%; object-fit: cover; position: relative; z-index: 1;
    border-radius: 0; box-shadow: none; max-width: none; max-height: none;
  }
  .ft-root #pattiStage .screen .fly-glass { inset: 0; opacity: 1; pointer-events: auto; z-index: 5; }
  .ft-root #pattiStage .screen .fly-progress { left: 4%; right: 4%; bottom: 4%; opacity: 1; z-index: 6; }
  .ft-root #pattiStage .screen .fly-progress::before { display: none; }
  .ft-root #pattiStage .screen::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 16%; z-index: 4; pointer-events: none;
    background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.45) 100%);
  }
  .ft-root #pattiStage .screen .fly-seek { left: 4%; right: 4%; bottom: calc(4% - 14px); pointer-events: auto; z-index: 6; }
  .ft-root #pattiStage .fly-hint { z-index: 10; }
  /* ---- Henrietta note beat with controls (16 Sep 2026), superseded by #noteStage above; kept for reference ----
     The desktop recording (1920 x 1200) stays a plain rounded panel on the dark ground, but it gets the
     device beats' controls: the video sits in a .screen wrapper sized like the old bare video, and
     devicePlayer('henrietta', ...) drives it. No autoplay. */
  .ft-root .beat-plain .screen { position: relative; width: min(90vw, 1600px); aspect-ratio: 1920 / 1200; max-height: 86vh; border-radius: 18px; overflow: hidden; background: #000; box-shadow: 0 30px 80px rgba(0,0,0,.45); }
  .ft-root .beat-plain .screen video, .ft-root .beat-plain #henriettaVideo { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 0; box-shadow: none; max-width: none; max-height: none; }
  .ft-root .beat-plain .screen .fly-glass { inset: 0; opacity: 1; pointer-events: auto; }
  .ft-root .beat-plain .screen .fly-progress { opacity: 1; }
  .ft-root .beat-plain .screen .fly-seek { pointer-events: auto; }
  .ft-root .beat-plain .fly-hint { bottom: 4vh; }
  .ft-root .beat-ipad .screen video, .ft-root .beat-ipad #gradyVideo, .ft-root .beat-ipad #joniVideo {      /* the ids win over .beat-video's sizing */
    display: block; height: 100%; width: 100%; aspect-ratio: 3 / 4; object-fit: cover;
    border-radius: 12px; background: #000; box-shadow: none; max-width: none; max-height: none;
  }
  /* ---- the film on a narrow screen (17 Sep 2026) ----
     The frame is always exactly 16:10 - the box is width = 1.6 * height by construction - so on a phone
     it is a band about 62.5vw tall across the middle of the window. The beats that are MEASURED OFF THE
     PICTURE stay inside that band and are simply small: the desk screen, the note and the review. Their
     rectangles come from where the monitor is in the frame, so growing the band would crop the frame and
     cut the document in half, which is the one thing worth reading.
     The iPad and the phone are not measured off the picture - they are drawn objects standing on a dimmed
     still - so on a narrow screen they leave the band and use the window. Three things make that work:
     the sticky box stops clipping, the drawn gloved hand goes (it is about as wide as the window and it
     is what makes the device tiny), and the device is capped against the window's width AND its height so
     it always lands whole. The rules that used to be here sized the device at 82vw / 60vw while the hand
     stayed keyed to --ipad-h, so the hand and the device came apart and the device ran off the edge. */
  @media (max-width: 700px) {
    /* --bleed is how far the film band falls short of the window above and below, so a negative top and
       bottom of that size stretches a layer out from the band to the whole window. Only the closing beat
       uses it. The picture behind the iPad and the phone was stretched the same way for a while on
       17 Sep 2026 and was taken out again: Vincent, "it does not look good, keep it as it was before so
       it's consistent with the rest". The picture stays in the band everywhere the film plays, and the
       two devices are the only things that come out of it. */
    .ft-root .fly-sticky { overflow: visible; --bleed: calc((var(--fly-h) - 100vh + var(--nav-h, 64px)) / 2); }
    .ft-root .beat-ipad { justify-content: center; overflow: visible; }
    .ft-root .ipad-stage .ipad-hand {
      position: relative; left: auto; top: auto; height: auto; aspect-ratio: auto;
      width: min(82vw, calc((100vh - var(--nav-h, 64px)) * 0.62));
    }
    .ft-root .ipad-stage .ipad-hand img.hand { display: none; }
    .ft-root .ipad-stage .ipad-hand .ipad {
      position: relative; left: auto; top: auto; height: auto; width: 100%; margin: 0;
      padding: clamp(8px, 2.5vw, 14px);
      box-sizing: border-box;   /* content-box put the bezel outside the width and pushed the iPad right */
    }
    .ft-root .beat-ipad .screen { height: auto; width: 100%; }
  }
  /* ---- site footer (16 Sep 2026): same rules as site.css so the sub-pages and the home match ---- */
.ft-root .site-footer { background: #150F0D; color: rgba(245,228,215,.8); padding: clamp(36px, 4vw, 64px) 0 24px; font-size: 14px; }
.ft-root .site-footer .wrap { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1fr; gap: 28px; }
@media (max-width: 800px) { .ft-root .site-footer .wrap { grid-template-columns: 1fr 1fr; } }
.ft-root .site-footer .logo { color: #fff; }
.ft-root .site-footer .logo .word { filter: brightness(0) invert(1) sepia(.15) saturate(.6) brightness(1.05); }
.ft-root .site-footer h4 { color: #fff; margin: 0 0 12px; font-size: 13px; letter-spacing: .08em; text-transform: uppercase; }
.ft-root .site-footer ul { list-style: none; margin: 0; padding: 0; }
.ft-root .site-footer li { margin: 0 0 8px; }
.ft-root .site-footer a { color: inherit; text-decoration: none; }
.ft-root .site-footer a:hover { color: #fff; }
.ft-root .foot-brand p { max-width: 30em; margin: 14px 0; }
.ft-root .linkedin svg { width: 22px; height: 22px; fill: currentColor; }
.ft-root .foot-legal { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; border-top: 1px solid rgba(255,255,255,.1); margin-top: 28px; padding-top: 18px; font-size: 13px; opacity: .8; }
.ft-root .foot-legal.wrap { display: flex; }
  .ft-root .site-footer .wrap { width: min(1180px, 100% - 2 * clamp(20px, 7.8vw, 158px)); margin: 0 auto; }