/* home.css — the home page only. Header, footer, buttons and tokens come from
   shared.css / tokens.css, which every other page already uses.

   The story track (13 Sep 2026, Phase 7, Albert's decision, recorded in
   design-references/july-background-decision.md): a still of the ribbons scene
   under four slides that pin for a 900vh track on desktop, each slide's title
   written on letter by letter as it becomes active, the proof block and the
   closing CTA in normal flow below. The pin costs Lighthouse nothing measurable;
   the live scene it replaces cost everything (the Phase 7 report, same file).

   Stacked by default, pinned as the enhancement, at every width (17 Sep 2026,
   Albert: phones get the same story as desktop; the 721px minimum is gone).
   Without JavaScript the four slides sit in normal flow over the poster and
   every title is plain text: an inline script in <head> adds html.js-story
   before first paint and removes it again if home.js has not run within four
   seconds, so a failed script gives the stacked page rather than a blank one.
   The pinned rules below all start with .js-story.

   Motion: transform and opacity only (design.md §6); the 6 Sep letter blur is
   gone. Under prefers-reduced-motion every transition is off; the pin itself
   stays, a judgment call recorded in the decision file. */

/* ---------- story: shared by both layouts ---------- */

.story-scroll {
    position: relative;
    /* the sticky header sits over the top of it, as it did over the hero */
    margin-top: -82px;
}

.story-sticky {
    position: relative;
    min-height: 100svh;
    overflow: hidden;
}

.hero-poster,
.hero-poster img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 1;
}
/* The live WebGL background (src/live/live-bg2.js), when the capability gate passes.
   It sits on the poster's own layer, inserted after it, so it paints over the still and
   under the overlay and the copy. Invisible until its first frame has been drawn, then
   faded in; every gated visitor and every failure keeps the still, which is the page.
   Hidden outright under prefers-reduced-motion, which is also a gate condition, so this
   rule is the second of two independent guards, not the only one. */
.hero-live {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 1;
    opacity: 0;
    transition: opacity 900ms ease;
}
.hero-live.is-live { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
    .hero-live { display: none; }
}

/* The poster is the LCP element. */

.hero-overlay {
    position: absolute;
    inset: 0;
    z-index: 2;
    background: linear-gradient(180deg,
        rgba(5, 7, 13, 0.55) 0%,
        rgba(5, 7, 13, 0.25) 38%,
        rgba(5, 7, 13, 0.88) 100%);
}

/* The palette shift the live scene used to make: a cyan wash whose opacity home.js
   sets from story progress (0 at the top, 0.22 at the end). Opacity only, so it is
   a compositor change, never a repaint. */
.story-tint {
    position: absolute;
    inset: 0;
    z-index: 2;
    background: radial-gradient(ellipse at 68% 42%, var(--cyan) 0%, var(--blue) 55%, transparent 100%);
    mix-blend-mode: screen;
    opacity: 0;
    pointer-events: none;
}

.story-frame {
    position: relative;
    z-index: 3;
    width: 100%;
    max-width: 1180px;
    margin: 0 auto;
    padding: 82px 40px 40px;
}

.slide { padding: 0 0 56px; }

.slide-title { color: var(--fg-title); }

h1.slide-title {
    font-family: 'Italiana', serif;
    font-size: clamp(38px, 6vw, 72px);
    line-height: 1.05;
    letter-spacing: 0.5px;
    margin-bottom: 22px;
    max-width: 15ch;
}

h2.slide-title {
    font-family: 'Geist', system-ui, sans-serif;
    font-size: clamp(30px, 4vw, 52px);
    line-height: 1.1;
    margin-bottom: 16px;
}

.slide-desc {
    font-size: 17px;
    line-height: 1.6;
    color: var(--fg-muted);
    max-width: 58ch;
}

.slide-desc.hero-lede {
    font-size: 19px;
    color: var(--fg);
    letter-spacing: 0.3px;
    max-width: 46ch;
}

.story-dashes { display: none; }

/* ---------- story: the pinned layout, with JavaScript, every width ---------- */

    .js-story .story-scroll { min-height: 900vh; }

    .js-story .story-sticky {
        position: sticky;
        top: 0;
        height: 100vh;
        height: 100svh;
        min-height: 0;
    }

    .js-story .story-frame {
        position: absolute;
        left: 50%;
        bottom: 0;
        transform: translateX(-50%);
        padding: 0 40px 96px;
    }

    /* every slide sits in the same place, the copy space left of the tower */
    .js-story .slide {
        position: absolute;
        left: 40px;
        right: 40px;
        bottom: 96px;
        padding: 0;
        pointer-events: none;
    }
    .js-story .slide.is-active { pointer-events: auto; }

    /* a title never paints as raw text: hidden until home.js has split it */
    .js-story .slide-title:not(.is-split) { opacity: 0; }

    /* a word is one unbreakable unit, so per-letter spans never break mid-word */
    .js-story .slide-title .word { display: inline-block; white-space: nowrap; }

    .js-story .slide-title .char {
        display: inline-block;
        opacity: 0;
        transform: translateY(0.45em);
        transition: opacity 0.8s cubic-bezier(0.25, 1, 0.5, 1),
                    transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
    }
    /* on the way in, letters follow each other 35 ms apart; on the way out, together */
    .js-story .slide.is-active .slide-title .char {
        opacity: 1;
        transform: none;
        transition-delay: calc(var(--i, 0) * 35ms);
    }

    .js-story .slide-desc {
        opacity: 0;
        transform: translateY(24px);
        transition: opacity 0.8s cubic-bezier(0.25, 1, 0.5, 1),
                    transform 0.8s cubic-bezier(0.25, 1, 0.5, 1);
    }
    .js-story .slide.is-active .slide-desc {
        opacity: 1;
        transform: none;
        transition-delay: 0.4s;
    }

    /* On the way out, faster than on the way in (Albert, 13 Sep 2026, 19:26): 0.3 s
       against 0.8 s. Measured at constant scroll speeds on the preview: with 0.8 s both
       ways the old heading was still at 0.17 to 0.32 opacity under the next one's first
       letters at 1200 to 1800 px/s, the pace of a flick past the hero; at 0.3 s out the
       two are never visible together at any pace. Invisible at reading pace. */
    .js-story .slide:not(.is-active) .slide-title .char,
    .js-story .slide:not(.is-active) .slide-desc { transition-duration: 0.3s; }

    /* where the visitor is in the four-part track */
    .js-story .story-dashes {
        position: absolute;
        right: 40px;
        top: 50%;
        transform: translateY(-50%);
        z-index: 3;
        display: flex;
        flex-direction: column;
        gap: 12px;
    }
    .js-story .story-dash {
        display: block;
        width: 2px;
        height: 40px;
        border-radius: 2px;
        /* The UNFILLED track. At 0.14 it vanished against the near-black sky at the top of the
           scroll, so the first slide looked like it had no progress indicator at all while the
           later ones showed white marks. The fills are correct (they are scaleY(0) until you
           descend); it was the empty track that could not be seen. 0.32 reads at the darkest
           point without competing with the fill at the brightest. */
        background: rgba(150, 180, 255, 0.32);
        /* A translucent light track cannot read against BOTH a near-black sky at the top and the
           bright aurora at late-mid; centring the block moved it over the bright region, where it
           measured 1.14:1. The dark hairline gives it an edge to sit against at either extreme
           without making it heavier. */
        box-shadow: 0 0 0 1px rgba(0, 10, 30, 0.55);
        overflow: hidden;
    }
    .js-story .story-dash-fill {
        display: block;
        width: 100%;
        height: 100%;
        background: var(--fg-title);
        transform: scaleY(0);
        transform-origin: top;
    }

    @media (prefers-reduced-motion: reduce) {
        .js-story .slide-title .char,
        .js-story .slide-desc,
        .js-story .slide:not(.is-active) .slide-title .char,
        .js-story .slide:not(.is-active) .slide-desc { transition: none; }
    }

/* ---------- sections below the track ---------- */

.proof,
.home-closing-cta {
    max-width: 1180px;
    margin: 0 auto;
    padding: 110px 40px;
}

.proof figure { margin: 0; max-width: 24ch; }
.proof blockquote {
    font-family: 'Geist', system-ui, sans-serif;
    font-size: clamp(28px, 4vw, 46px);
    line-height: 1.25;
    color: var(--fg-title);
    margin: 0 0 22px;
    max-width: 20ch;
}
.proof figcaption {
    font-size: 13px;
    color: var(--fg-faint);
    letter-spacing: 0.6px;
}
.proof-link,
.proof figure { max-width: none; }
.proof-link {
    display: inline-block;
    margin-top: 28px;
    color: var(--cyan);
    font-size: 14px;
    font-weight: 600;
    text-decoration: none;
}
.proof-link:hover { text-decoration: underline; }

.home-closing-cta { padding-bottom: 130px; }
/* The map variant ends 100 px under the reach line at every width, the same padding-bottom the final
   .page-section carries on About, Pricing and Portfolio (measured 18 Sep 2026: 220 px from the last line
   to the footer rule on those three pages at 390 and 1440). With the heading, button and proof line gone
   the 80 / 130 of .home-closing-cta left 200 / 250 px of empty dark under one sentence. */
.home-closing-reach { padding-bottom: 100px; }
/* The map block (17 Sep 2026): centred. The map and the sentence are styled in shared.css (they came
   from About and may go back). The two h2 rules below match nothing on a public page since 18 Sep 2026:
   the homepage closing block (heading, quote button, proof line) was removed at Albert's request, and
   only the noindexed test copies gl-test.html and live-test.html still carry that markup. Kept, not
   deleted, until those pages go. */
.home-closing-reach { text-align: center; }
.home-closing-reach h2 { margin-top: 48px; }
.home-closing-cta h2 {
    font-family: 'Geist', system-ui, sans-serif;
    font-size: clamp(28px, 4vw, 44px);
    color: var(--fg-title);
    margin-bottom: 26px;
}


/* ---------- reveal on entry ---------- */
/* Once, on entering the viewport. Not a scroll-linked animation: the section
   does not move back when the visitor scrolls up. */

/* Progressive enhancement, deliberately. The hidden state is scoped to
   .js-reveal, which an inline script in <head> sets before first paint. Without
   JavaScript — a failed script, a parse error, a blocked request — the class is
   never set, the rule never applies, and every section renders normally. The
   earlier version hid these unconditionally and relied on JS to reveal them,
   which meant one script failure blanked the whole page below the hero. */
.js-reveal .reveal {
    opacity: 0;
    transform: translateY(24px);
    transition: opacity 0.4s ease, transform 0.4s ease;
}
.js-reveal .reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
    .js-reveal .reveal { opacity: 1; transform: none; transition: none; }
}

/* ---------- phone ---------- */

@media (max-width: 720px) {
    /* Phones, since 17 Sep 2026: the story pins here too (the .js-story rules above
       apply at every width and win on specificity). The rules below are the stacked
       no-JavaScript layout plus the phone scrim; the pinned phone layout is step 5
       of the mobile build and is not settled yet. */
    /* 82px clears the sticky header, the rest is air above the headline */
    .story-frame { padding: 130px 20px 24px; }
    .slide { padding: 0 0 48px; }
    .slide-desc.hero-lede { font-size: 17px; }
    /* the stacked slides run over the tower and its lamp on the portrait poster */
    .slide-title, .slide-desc { text-shadow: 0 2px 16px rgba(5, 7, 13, 0.85); }
    /* A scrim behind the text column (Albert, 13 Sep 2026, 17:29, option A of three
       measured): the page background at 82% on the left fading to 35% on the right,
       under the copy and above the poster and its overlay. Takes the worst line ("Sites
       get watched", on the beam) from 1.4:1 to 3.4:1 against the brightest pixel behind
       it, text shadow not counted. Still under AA there; accepted knowingly, recorded in
       CHANGELOG. Dimming the whole still passed but removed the design on phones. */
    .story-frame { isolation: isolate; }
    .story-frame::before {
        content: "";
        position: absolute;
        inset: 0;
        z-index: -1;
        pointer-events: none;
        background: linear-gradient(90deg, rgba(5, 7, 13, 0.82) 0%, rgba(5, 7, 13, 0.72) 60%, rgba(5, 7, 13, 0.35) 100%);
    }
    /* The pinned story on phones (17 Sep 2026). The copy sits over the lower third of the
       scene, and at the end of the scroll the tower's lit flank passes behind it: measured at
       390 and 430, EN and FR, scene frozen, glyph pixels against the pixels behind them, body
       text fell to 2.76:1 on slide 4, with or without the camera push. A second darkening from
       the page background, phones only, clear at 48% of the frame (below the lantern), 0.6 by
       60%, held to 90% (the copy ends at 89 to 90% of the frame at 390 and 430, both languages),
       then released to 0.15 at the bottom: under the copy and the dashes, over the scene and the
       overlay. The release is Albert's 22:01 decision: at 0.7 to the bottom the tower body below
       the copy went to page-background black (slide 4 read 104 px of nothing-visible at 390); at
       0.15 the same rows read 0 px, and the copy contrast above stays where the 0.6 puts it.
       Slide 1 stays dark below the copy under every camera (scroll 0 has the key light on the far
       side of the tower, no lit surface below the lantern): accepted for tonight, queued with 4c.
       The frame scrim above stays for the stacked no-JS layout; this one is .js-story only, and
       it sits inside the phone media query, so the desktop overlay is untouched. */
    .js-story .story-sticky::after {
        content: "";
        position: absolute;
        inset: 0;
        z-index: 2;
        pointer-events: none;
        background: linear-gradient(180deg, rgba(5, 7, 13, 0) 48%, rgba(5, 7, 13, 0.6) 60%, rgba(5, 7, 13, 0.6) 90%, rgba(5, 7, 13, 0.15) 100%);
    }
    /* The pinned frame follows the dynamic viewport on phones (Albert, 22:01). The shared rule
       above is 100svh, the smallest viewport, chosen on 13 Sep so the frame never resizes while
       scrolling; on a phone that leaves a strip of page background below the frame once the
       browser bar collapses. dvh tracks the bar, so the frame fills the screen in both states and
       everything anchored to its bottom (the copy, the dash centre) moves by the bar's travel when
       it collapses, once per direction. svh stays as the fallback for browsers without dvh. */
    .js-story .story-sticky { height: 100svh; height: 100dvh; }
    /* The copy block 48 px above the bottom edge on phones, not the desktop 96 (Albert, 17 Sep
       2026, 22:52: the region below the copy on his phone read as empty space; the frame is
       already full height, so the copy moves, the camera does not). Measured before: the copy
       ended 96 px above the bottom at 390x844 and 430x932, EN and FR, all four slides. After:
       48 px, the last line at 94.3% (390) and 94.9% (430) of the frame, still inside the scrim's
       0.6 hold to 90% plus its release; glyph contrast worst 7.7 to 10.4:1, zero pixels under
       4.5, all sixteen contexts; extending the hold to 95% changed nothing measurable, so the
       scrim stays as it is. Bonus at slide 1: the headline now starts below the tower base
       (640 vs 629 at 390). The frame padding and the slide offset move together: the slides sit
       at bottom: <padding> of the frame's padding box, so both must carry the same value. */
    .js-story .story-frame { padding-bottom: 48px; }
    .js-story .slide { bottom: 48px; }
    /* The progress dashes at 20 px with 10 px gaps on phones, not the desktop 40 and 12 (Albert,
       same message: too big on mobile; 24 first, then 20). Block 110 px tall against 196, still centred on the frame
       (359 to 485 at 844, 403 to 529 at 932), clear of the copy at every point (lowest dash bottom
       529 against the earliest headline top 691 at 430 FR). Legibility unchanged in kind: the mark
       as a whole reads 3.08 to 3.48:1 at every unfilled dash, both widths, both languages (3.04 to
       3.51 at 40 px), caps excluded. Width stays 2 px: the hairline needs a core to sit against. */
    .js-story .story-dashes { gap: 10px; }
    .js-story .story-dash { height: 20px; }   /* 24 first, 20 on Albert's second look, 17 Sep 23:25 */
    /* The unfilled progress track at 0.5 on phones, 0.32 on desktop (Albert, 17 Sep 2026). The
       14 Sep decision was that an empty track should READ empty, and 0.32 was accepted at 1.97:1
       because it still read at desktop size; at DPR 3 the same alpha did not read at all
       (1.9 to 2.7:1, scene frozen, track and hairline against what is behind them, rounded caps
       excluded). 0.5 reads 3.12 to 3.58:1 at every unfilled dash at 390 and 430; 0.65 read 4.5 to
       5.7 and competes with the fill, which is the thing that carries the information. */
    .js-story .story-dash { background: rgba(150, 180, 255, 0.5); }
    .proof, .home-closing-cta { padding: 70px 20px 80px; }
    .home-closing-reach { padding-bottom: 100px; }
}
