/* ==========================================================================
   Never Forgotten — 9/11 25th Anniversary Memorial
   Design: Luke Sohl, "20260722 - September 11 - Web Build", direction 2a
   ("quiet rest, photo bloom" — his designated primary build).

   Palette is deliberately warm/muted and does NOT use the main site's
   #062160 navy or #FFD700 gold. The deck lists "warm the palette / add
   navy & gold" as an *unexplored* option, so the restraint is intentional.
   Do not "correct" this to site chrome without asking Luke.

   The source deck frames are 288px-wide scaled mockups, so its px values
   are miniature. Type here is scaled to real reading sizes that preserve
   the deck's proportions and hierarchy.
   ========================================================================== */

:root {
    --nf-ground: #1b1e24; /* primary dark ground */
    --nf-ground-2: #23262d; /* raised panel */
    --nf-ground-3: #2a2e36; /* hover panel */
    --nf-ink: #111318; /* deepest */
    --nf-gold: #c9a24b; /* antique gold — muted, not #FFD700 */
    --nf-cream: #f5f3ec; /* light ground / text on dark */
    --nf-cream-2: #f0ede6;
    --nf-paper: #fbfaf7; /* column rest ground */
    --nf-muted: #8a8578; /* eyebrow / secondary */
    --nf-muted-2: #948f83; /* meta / byline on dark */
    /* Raised from #6f6a5e, which computed to 3.10:1 on the dark ground and
       failed WCAG 1.4.3 at all seven of its uses there — including the landing
       colophon and the only line telling visitors the portraits are clickable.
       #a6a294 clears 4.5:1 against every memorial ground, including the worst
       case where a photograph shows through the scrim, and stays in the warm
       palette. De-emphasis is carried by size and weight instead. */
    --nf-muted-3: #a6a294;
    --nf-line: #cdc9c0; /* body text on dark */
    --nf-line-2: #b9b6ae; /* secondary body on dark */
    --nf-line-3: #9a968d;
    --nf-chip-ink: #e2dfd7; /* text on a raised dark chip */
    --nf-hairline: #4a4e57; /* border on dark */
    --nf-burnt: #8a5a1e; /* article section headings */
    --nf-rule: rgba(0, 0, 0, .12); /* hairline divider */
    --nf-navy: #14245a; /* chrome navy — lighter than site #062160 */
    --nf-navy-ink: #3a4258;
    --nf-navy-soft: #aeb8d6;
    --nf-serif: var(--memorial-serif); /* declared in site.css, which both layouts load */
    --nf-sans: 'Inter', system-ui, -apple-system, sans-serif;
    /*  The landing's hover slideshows.

        --nf-slide-fade is the cross-fade between one picture (or clip) and the
        next. The client, 2026-09-02: "add slight motion and a good fade between
        them". At .9s it is slow enough to read as a dissolve rather than a cut,
        and it overlaps the hold rather than following it.

        --nf-drift is how long the Ken Burns push runs before it settles. It is
        NOT the cycling interval: that is data-hold in Index.cshtml, 4000ms on
        Faces and Stories and 3000ms on Voices, and it is written once, there.
        This is deliberately longer than the longest of them, because a drift
        that finishes early stops dead under a picture still on screen and the
        stop is the thing the eye catches. Running long means every image is
        still moving, barely, when it is replaced.  */
    --nf-slide-fade: .9s;
    --nf-drift: 7s;
    /* The article's single measure. Everything on a story page — breadcrumb,
       kicker, headline, subtitle, byline and body copy — is centred on this one
       width so they share a left edge.

       They did not before: the reading column was a centred 680px while the
       hero content was full-bleed with a small padding, so on a 2560px monitor
       the headline sat 908px to the left of the paragraph it introduced. The
       gutter keeps the same clamp the padding used, so nothing changes on a
       handset. */
    /* 64rem is Tailwind's max-w-5xl — the same step the obituary and the In
       Memoriam grid use, so the article column lines up with the rest of the
       site instead of sitting in a narrower box of its own. Centred on the page,
       its left edge lands on the nav's Home link.

       It was 760px, which put the article's edge ~120px inside the chrome and
       read as misaligned. Wider than the classic 66-75 character measure — a
       deliberate trade for alignment with the site's grid. */
    /*  The default content box: the memorial's listing pages, the player column,
        the back link. The ARTICLE overrides this to a reading measure of its own
        — see .nf-story-hero, .nf-read in never-forgotten-interior.css — because
        64rem measured at 137 characters per line, against the 80 that WCAG 2.2
        SC 1.4.8 allows and the 66 Bringhurst puts at the centre of 45–75. */
    --nf-measure: 64rem;
    --nf-gutter: clamp(1rem, 4vw, 2.5rem);
    /* The page box. It was 1536px paired with the clamp() gutter above, on the
       reasoning that 1536px is the largest step of the Tailwind .container the
       shared header and footer use — right number, wrong shape. .container is
       not a clamp: its max-width STEPS at 640/768/1024/1280/1536 and it adds
       px-4 (lg:px-8) of padding inside that. Approximating a step function with
       a smooth one lines up at exactly one window width and nowhere else, which
       is why at 1400px the memorial's content still began 52px left of the
       chrome above it.

       --page-content in site.css restates the real steps, and everything in
       this section now sits on it, so the memorial shares one bounding box with
       the rest of the site by construction rather than by arithmetic. Derrick,
       2026-09-01: "that's going to be our sort of bounding box across the
       board." --nf-page is kept only as the ceiling that box tops out at. */
    --nf-page: 1536px;
}

/* The memorial's page box. Full-bleed grounds and photographs stay full-bleed —
   this bounds CONTENT only. */
.nf-page {
    width: var(--page-content);
    margin-inline: auto;
}

/* Only the memorial's type and ink — layout, background and the chrome come
   from the shared Tailwind classes, exactly as on _Layout. */
.nf-body {
    color: var(--nf-ground-2);
    font-family: var(--nf-sans);
}

/*  The section's footer used to be defined here as .nf-colophon. It is now the
    SITE's footer — .site-colophon in site.css, rendered by _SiteFooter on every
    page of the magazine, memorial or not. Derrick, 2026-09-01: "I made this new
    footer here to match the Never Forgotten... that will be consistent across
    everything."

    Its colours came from this file's tokens and are restated over there as
    --colophon-*, because site.css loads on pages that never see this sheet. The
    values are unchanged, so Never Forgotten looks exactly as it did; what
    changed is that everything else now looks like it. */

/* Full-viewport sections (landing, Faces) fill the space between the shared
   header and footer; the article scrolls instead. */
.nf-main {
    display: flex;
    flex-direction: column;
    min-height: 0;
}

    .nf-main:has(.nf-read) {
        display: block;
    }

/* Available to assistive tech and crawlers, invisible in the design. */
.nf-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ------------------------------------------------------------- masthead  */

.nf-mast {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: clamp(.85rem, 2.4vw, 1.75rem);
    /* Shorter than the deck. The masthead is a nameplate over the three doors,
       not a hero — at the old height it pushed the columns down far enough that
       the gateway stopped reading as a single view on a laptop. */
    padding: clamp(.9rem, 2.2vw, 1.6rem) clamp(1rem, 4vw, 2rem);
    flex: none;
    position: relative;
    transform-origin: top center;
    background: linear-gradient(rgba(20,22,28,.92), rgba(20,22,28,.95)), url("/images/911/bg-memorial-field.jpg") center/cover;
    background-color: var(--nf-ground);
    background-blend-mode: normal, luminosity;
}

.nf-mast-emblem {
    height: clamp(40px, 5.5vw, 60px);
    width: auto;
    flex: none;
    display: block;
}

.nf-mast-txt {
    text-align: left;
}

.nf-kick {
    font: 600 clamp(9px, 1vw, 11px) var(--nf-sans);
    letter-spacing: .28em;
    text-transform: uppercase;
    color: var(--nf-line);
}

.nf-mast-title {
    font: 400 clamp(1.85rem, 4.2vw, 2.9rem)/1.05 var(--nf-serif);
    color: var(--nf-cream);
    margin: .15rem 0;
}

/*  The .15rem the title carries is the gap between two lines of one nameplate,
    and this is not that: "Never Forgotten" is the name and this line is the fact
    underneath it. Derrick, 2026-09-08: "add some padding above USNA alumni lost -
    September 11, 2001." Set on the sub rather than the title so the space belongs
    to the line it separates, and margins collapse to whichever is larger anyway. */
.nf-mast-sub {
    margin-top: clamp(.5rem, 1.1vw, .8rem);
    font: italic 400 clamp(.9rem, 1.3vw, 1.05rem) var(--nf-serif);
    color: var(--nf-line);
}

.nf-mast-rule {
    height: 2px;
    width: 52px;
    margin-top: .45rem;
    border-radius: 1px;
    background: var(--nf-gold);
    border: 0;
}

/* Masthead collapses away when a column is entered (deck: .mast-lift/.gone) */
.nf-mast {
    /* Ceiling for the collapse transition, so it has to clear the masthead's
       real height — which is now ~180px at the largest step. */
    max-height: 300px;
    overflow: hidden;
    transition: max-height .55s cubic-bezier(.4,0,.2,1), opacity .4s ease, padding .55s ease;
}

    .nf-mast.is-gone {
        max-height: 0;
        opacity: 0;
        padding-top: 0;
        padding-bottom: 0;
    }

/* -------------------------------------------------------------- columns  */

.nf-cols {
    display: flex;
    flex: 1;
    min-height: 0;
}

.nf-col {
    flex: 1;
    border: 0;
    border-right: 1px solid var(--nf-rule);
    display: flex;
    flex-direction: column;
    position: relative;
    overflow: hidden;
    background: var(--nf-paper);
    cursor: pointer;
    outline: none;
    text-align: left;
    /* On the element, not on :hover — a transition declared only in the hovered
       rule animates the way in and then snaps back out, because the element
       stops matching the rule (and so loses the transition) the instant the
       pointer leaves. It also has to live here to cover .is-entered, which is a
       different state entirely: that is the click-to-enter swipe. */
    transition: flex .35s ease;
    font: inherit;
    padding: 0;
    color: inherit;
    -webkit-appearance: none;
    appearance: none;
}

    .nf-col:last-child {
        border-right: 0;
    }

/* Fills its own column, framed as the deck intended.

   An earlier attempt pinned these to 100vw so the bitmap would never re-raster
   during the width tween. It did fix the paint cost and it wrecked the picture:
   a 2560px-wide image inside an 854px column shows a small central crop, so
   every photograph looked heavily zoomed. The framing is left alone here and
   the paint cost is dealt with at its actual source instead.

   NOTHING here animates a filter, and that is the fix for the flicker. The
   resting state used to carry blur(1.5px) and the hover took it to blur(0)
   through a .35s transition — so every frame of every hover re-rastered a
   full-column bitmap through a blur kernel on the main thread. That is the
   stutter, and it is the one thing on this page the compositor cannot absorb.
   grayscale is now constant, blur is gone, and only opacity moves: at .12 the
   blur was doing nothing a human could see anyway. */
.nf-col-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: .12; /* "barely-there duotone" at rest */
    filter: grayscale(1);
    transition: opacity .35s ease;
    will-change: opacity;
}

.nf-col-img[data-voices] {
    object-position: center 20%;
    transform: scale(1.35);
    filter: grayscale(1) brightness(1.18);
}

/*  ------------------------------------------------- the hover slideshows

    Luke, 2026-09-01: "On load, can it randomly select a profile picture of any
    of the people that are listed in that section." Derrick: "do we want when
    you hover to slideshow it also?" Alex: "We can slideshow images from all the
    different stories."

    All three doors now carry one. The client, 2026-09-02: "the slideshow of
    faces is WAY TOO FAST ... i want to make a slideshow of voices VIDEO that is
    like 3 seconds of each video clip fading between and looping. i want stories
    to do a slideshow of the images from the stories but always have the same
    image when not hovered." Faces walks the roster's portraits, Stories walks
    the published stories' hero images, and Voices plays the films.

    Faces' load half is server-side and already here — IndexModel draws
    FacesPortrait at random per request, so the door onto the fourteen is a
    different face each time rather than a fixed graphic of whoever sorts first.
    Stories is pointedly the opposite: one constant resting image, because that
    is what was asked for.

    Built as its own layer stacked OVER .nf-col-img rather than by swapping that
    element's src. .nf-col-img's opacity is the column's rest/hover state — .12
    at rest, 1 on hover — so animating it for a cross-fade would fight the hover
    it is already carrying, and swapping src on a single element gives a hard
    cut, not a fade. Here the WRAPPER carries the rest/hover visibility and the
    layers inside it cross-fade against each other, so the two motions are
    independent and neither has to know about the other. That layering is also
    what makes "the same image when not hovered" free rather than something to
    implement: the resting picture is never touched, and stopping is a fade back
    down to it.

    No filter transitions, for the reason spelled out on .nf-col-img above: a
    filter that animates re-rasters a full-column bitmap every frame. grayscale
    is constant here too; only opacity and transform move, and both are
    composited.

    The grayscale on this wrapper now sits over playing video as well as
    photographs. That is deliberate: the whole landing is duotone, and a colour
    film cross-fading into a grey column would read as a mistake. It is a
    constant filter, never a transitioned one.

    Voices' films are muted in the markup and again in the script before every
    play(), carry no controls, and sit under a pointer-events:none wrapper.
    Nothing on this page can make a sound. */
.nf-col-slides {
    position: absolute;
    inset: 0;
    pointer-events: none;
    opacity: 0;
    filter: grayscale(1);
    transition: opacity .35s ease;
    will-change: opacity;
}

    .nf-col:hover .nf-col-slides,
    .nf-col:focus-visible .nf-col-slides {
        opacity: 1;
    }

.nf-col-slide {
    position: absolute;
    inset: 0;
    /*  Two kinds of layer wear this one class. Faces and Stories paint
        photographs as a background image; Voices holds a <video> per film. The
        background-* pair is inert on a <video> and the object-* pair is inert on
        a <span>, so both can be stated unconditionally — and object-fit is what
        makes a film crop to the column the way .nf-col-img does instead of
        letterboxing inside it.  */
    background-position: center;
    background-size: cover;
    object-fit: cover;
    object-position: center;
    width: 100%;
    height: 100%;
    opacity: 0;
    /*  Base scale sits midway between the two ends of the drift below, so when
        .is-on comes off and the animation stops applying, the layer has at most
        3% to give back — and it gives it back under an opacity that is on its
        way to 0, where nobody can see it.

        There is deliberately NO transition on transform, and that is not an
        omission. It was written that way first, to ease the hand-back, and
        measured on 2026-09-02: getAnimations() showed the CSSTransition and the
        drift both on the element with the drift pinned at currentTime 0 for over
        two seconds before it began to advance. Chrome will not start a
        compositor animation on a property a transition is still holding. The
        cost of the transition was therefore the first two seconds of every
        four-second image — the drift simply did not happen — to smooth a 3% snap
        on an invisible layer. Removing it is what makes the motion real.  */
    transform: scale(1.04);
    transition: opacity var(--nf-slide-fade) ease;
    will-change: opacity, transform;
}

/*  ------------------------------------------------------------- the drift

    The client, 2026-09-02: "add slight motion and a good fade between them like
    zoom/transform motion to give the site a little more style."

    "Slight" is the operative word. This is a 9/11 memorial, so the push is 6%
    over seven seconds — motion you notice at the edge of the frame rather than
    watch — and nothing pans. A translate on top of the scale is what turns a
    memorial into a carousel, and there is none here.

    transform ONLY, and the warnings on .nf-col-img and .nf-col-slides about
    animating a filter apply with more force: a transform is composited and costs
    nothing per frame, a filter re-rasters a full-column bitmap on the main
    thread. That is the stutter those comments were written about.

    Declared on .is-on rather than on the layer so that re-adding the class
    restarts the push from the top for each incoming picture. The two stacked
    photo layers drift in OPPOSITE directions, which alternates per image and is
    the difference between a slideshow that breathes and one that marches.  */
@keyframes nf-drift-in {
    from {
        transform: scale(1.01);
    }

    to {
        transform: scale(1.07);
    }
}

@keyframes nf-drift-out {
    from {
        transform: scale(1.07);
    }

    to {
        transform: scale(1.01);
    }
}

    .nf-col-slide.is-on {
        opacity: 1;
        animation: nf-drift-in var(--nf-drift) ease-out both;
    }

    .nf-col-slide:nth-child(even).is-on {
        animation-name: nf-drift-out;
    }

.nf-col-scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(transparent, rgba(20,22,28,.78));
    opacity: 0;
    transition: opacity .45s cubic-bezier(.22, .61, .36, 1);
    will-change: opacity;
    z-index: 1;
}

.nf-col-label {
    position: relative;
    z-index: 2;
    margin-top: auto;
    padding: clamp(1.25rem, 3vw, 2.25rem);
    color: var(--nf-ground-2);
    transition: color .35s ease;
}

.nf-col-word {
    display: block;
    font: 400 clamp(2rem, 4vw, 3.25rem)/1.05 var(--nf-serif);
    color: inherit;
}

.nf-col-sub {
    display: block;
    font: 400 clamp(12px, 1.1vw, 14px)/1.4 var(--nf-sans);
    color: inherit;
    margin-top: .5rem;
    max-width: 22ch;
    opacity: 0;
    max-height: 0;
    overflow: hidden;
    transition: opacity .3s ease, max-height .3s ease;
}

    .nf-col:hover .nf-col-sub,
    .nf-col:focus-visible .nf-col-sub {
        opacity: 1;
        max-height: 60px;
    }

/*  Hover / focus — photo blooms, column widens, label inverts. The deck's own
    rules, restored.

    A long detour lived here: the hover was rebuilt onto a two-layer cross-fade
    and a GSAP flex-grow tween to cure choppiness that turned out not to exist.
    The machine it was diagnosed on has Windows "Animation effects" switched off,
    so SPI_GETCLIENTAREAANIMATION reports false, Chrome reports
    prefers-reduced-motion: reduce, and the reduced-motion block near the end of
    this file suppressed every transition — the hover jumped straight to its end
    state. That reads exactly like jank and is not. On any machine with
    animations enabled these four rules were always smooth: one flex-grow
    animating re-solves its siblings' widths every frame as a consequence, so
    they never needed transitions of their own.  */
.nf-col:hover,
.nf-col:focus-visible {
    flex: 1.5;
}

    /* Opacity only. Setting a filter here is what re-introduces the per-frame
       raster — the resting filter is already correct and must stay untouched. */
    .nf-col:hover .nf-col-img,
    .nf-col:focus-visible .nf-col-img {
        opacity: 1;
    }

    .nf-col:hover .nf-col-scrim,
    .nf-col:focus-visible .nf-col-scrim {
        opacity: 1;
    }

    .nf-col:hover .nf-col-label,
    .nf-col:focus-visible .nf-col-label {
        color: #fff;
    }

    /* The sub-label reveal moved up to the .is-tweened fallback, and to the
       GSAP tween when the motion module is running. */

/* Entered — selected column goes full-bleed, siblings collapse */
.nf-cols.is-entered .nf-col {
    flex: 0;
    opacity: 0;
    pointer-events: none;
}

.nf-cols.is-entered .nf-col.is-active {
    flex: 1;
    opacity: 1;
    pointer-events: auto;
}

.nf-cols.is-entered .nf-col.is-active .nf-col-img {
    opacity: 1;
}

.nf-cols.is-entered .nf-col.is-active .nf-col-scrim {
    opacity: 1;
}

.nf-cols.is-entered .nf-col-label {
    opacity: 0;
    transition: opacity .3s ease;
}

.nf-col-panel {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: .75rem;
    padding: clamp(1.5rem, 5vw, 4rem);
    opacity: 0;
    transform: translateY(8px);
    transition: opacity .45s ease .2s, transform .45s ease .2s;
    pointer-events: none;
}

.nf-cols.is-entered .nf-col.is-active .nf-col-panel {
    opacity: 1;
    transform: none;
    pointer-events: auto;
}

.nf-col-panel-word {
    font: 400 clamp(2.5rem, 5vw, 4rem)/1.05 var(--nf-serif);
    color: var(--nf-cream);
}

.nf-col-panel-desc {
    font: 400 clamp(13px, 1.2vw, 15px)/1.5 var(--nf-sans);
    color: var(--nf-line);
    max-width: 34ch;
}

/* ----------------------------------------------------------- breadcrumb  */

.nf-crumb {
    display: flex;
    align-items: center;
    gap: .5rem;
    font: 500 12px var(--nf-sans);
    color: var(--nf-muted);
}

    /*  The 24px minimum lives HERE, in the base sheet, not in the handset one.

        It was added to never-forgotten-handset.css, which is inside a max-width media query, so
        the breadcrumb met the target size on a phone and missed it everywhere else — measured
        93x14 at 1440. WCAG 2.2 SC 2.5.8 has no viewport condition: a 14px-tall link is as hard
        to hit with an unsteady hand on a desktop as with a thumb on a phone, and pointer size is
        not something a breakpoint can tell you.

        Vertical padding only, so the row keeps its height and nothing moves; the breadcrumb's
        own .5rem gap keeps the enlarged boxes from overlapping each other. */
    .nf-crumb a,
    .nf-crumb button {
        display: inline-flex;
        align-items: center;
        min-height: 24px;
        color: var(--nf-muted);
        text-decoration: none;
        background: none;
        border: 0;
        padding: 0;
        font: inherit;
        cursor: pointer;
        transition: color .2s ease;
    }

        .nf-crumb a:hover,
        .nf-crumb button:hover {
            color: var(--nf-cream);
        }

    .nf-crumb .sep {
        color: var(--nf-hairline);
    }

    .nf-crumb .here {
        color: var(--nf-gold);
    }

/*  Luke, 2026-09-01: "We also standardized the spacing of the back button."
    Same .5rem/.85rem box as .site-back in site.css, so the memorial's back link
    and the article's are the same control at the same size — and so the target
    clears the 24x24 that WCAG 2.2 SC 2.5.8 asks for, which bare 13px text did
    not.

    The negative inline-start margin is what keeps it standardised WITHOUT
    moving it: the padding would otherwise inset the arrow 13.6px from the
    content edge, off the left edge the breadcrumb above it sits on. The box
    grows leftwards into the gutter; the glyph does not move. */
.nf-back {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    min-height: 24px;
    margin-top: 1.5rem;
    margin-inline-start: -.85rem;
    padding: .5rem .85rem;
    border-radius: 9999px;
    font: 500 13px var(--nf-sans);
    color: var(--nf-gold);
    text-decoration: none;
    align-self: flex-start;
    transition: opacity .2s ease, background-color .2s ease;
}

    .nf-back:hover {
        opacity: .75;
        background: rgba(245, 243, 236, .06);
    }

    /* On the article's light reading column, gold on cream is too faint —
       the burnt orange the section headings already use reads correctly. */
    .nf-back--article {
        color: var(--nf-burnt);
    }

/* ---------------------------------------------------------- handset chrome */

/* Mobile — the three columns stack rather than squeeze.
   (Luke's action item: "make mobile-friendly versions".) */
@media (max-width: 820px) {
    .nf-cols {
        flex-direction: column;
    }

    .nf-col {
        border-right: 0;
        border-bottom: 1px solid var(--nf-rule);
        min-height: 34vh;
    }

        .nf-col:hover,
        .nf-col:focus-visible {
            flex: 1;
        }

        /* Always open on a phone — there is no hover to open it. */
        .nf-col .nf-col-sub {
            opacity: 1;
            max-height: 60px;
        }

    /*  Entered, on a phone.

        min-height above is what broke this. flex:0 does not shrink a box below
        its own min-height, so the two "collapsed" columns still held 34vh each —
        invisible at opacity 0, and occupying two thirds of the screen. That is
        the band of white under the chosen photograph: not a sizing bug on the
        active column, dead space reserved by its hidden siblings. */
    .nf-cols.is-entered .nf-col {
        min-height: 0;
    }

    /*  The chosen column then takes the whole screen before we navigate.

        Fixed rather than flex:1, so it covers the site header and footer too and
        genuinely fills the viewport. Nothing is lost by leaving flow here: the
        stacked phone layout has no width tween to preserve, which is the only
        thing flex positioning was buying. dvh rather than vh so it tracks the
        browser chrome as it hides on scroll instead of sitting a toolbar short.

        The photograph inside is already inset:0 with object-fit:cover, so it
        fills this box by cropping, never by stretching. */
    .nf-cols.is-entered .nf-col.is-active {
        position: fixed;
        inset: 0;
        width: 100vw;
        height: 100dvh;
        min-height: 0;
        z-index: 60;
    }

    .nf-mast {
        flex-direction: column;
        text-align: center;
        gap: 1rem;
    }

    .nf-mast-txt {
        text-align: center;
    }

    .nf-mast-rule {
        margin-left: auto;
        margin-right: auto;
    }
}

/*  Reduced motion means less MOVEMENT, not less everything.

    This used to be "transition: none !important" on all of them, which is why
    the landing appeared to snap straight to its final state for anyone whose
    system asks for reduced motion — the columns jumped to full width and the
    photographs to full opacity with no intermediate frame at all. That is not
    what the preference is for: it exists to prevent vestibular triggers, which
    are movement, scaling and parallax. A cross-fade is none of those.

    So the fades are kept, and only the things that actually move are stopped:
    the scale on the photographs, and the column widening (dropped in the rule
    further up, and skipped by the motion module).  */
/*  Reduced motion means less MOVEMENT, not less everything.

    This was a blanket "transition: none !important" on all of these, which is
    what makes the landing appear broken to anyone whose system asks for reduced
    motion: the columns jump to full width and the photographs to full opacity
    with no intermediate frame. Windows users hit it whenever "Animation effects"
    is off, which is not rare.

    The preference exists to prevent vestibular triggers — movement, scaling,
    parallax. A cross-fade is none of those, so the photo bloom and the scrim and
    label fades are kept. What is removed is the movement: the column widen,
    which takes a column edge and its label roughly 350px across a large screen.
    That is precisely the "movement of a large object in the field of view" the
    guidance names, and it is why the widen is dropped rather than merely
    shortened.  */
@media (prefers-reduced-motion: reduce) {
    .nf-col-img,
    .nf-col-scrim,
    .nf-col-label,
    .nf-col-sub {
        transition: opacity .5s ease, color .5s ease !important;
    }

    /* No widen, and no transition to animate one. */
    .nf-col:hover,
    .nf-col:focus-visible {
        flex: 1;
    }

    .nf-col, .nf-col-panel, .nf-mast {
        transition: none !important;
    }

    /*  ALL THREE hover slideshows are inert here — Faces, Stories and Voices —
        and inert means gone rather than instant: with the transitions above
        suppressed it would be a sequence of hard cuts under the pointer, which
        is a worse experience than the still it replaces, not a gentler one. The
        JS bails on the same media query, so nothing is scheduled either — this
        is the belt to its braces, because the query can flip after load (a
        Windows user turning "Animation effects" off mid-session) and CSS
        re-evaluates where a one-shot script does not.

        One selector covers all three because they share the class. That is the
        reason the Stories and Voices decks were built onto .nf-col-slides rather
        than given markup of their own: a second class here is a second place to
        forget.

        The resting image underneath is untouched and still shows on every
        column — Faces' randomly-chosen portrait included. Picking a different
        face per page load is not motion. */
    .nf-col-slides {
        display: none;
    }

    /*  And no drift, on any of them.

        display:none above already stops every deck from rendering, so this is
        redundant by construction — and it is stated anyway, because the drift is
        the one thing added here that is unambiguously the vestibular trigger the
        preference exists to prevent, and it should not depend on a rule three
        lines up for its suppression. If .nf-col-slides ever becomes visible
        under reduced motion for some reason nobody has thought of yet, it will
        not be scaling.

        The Voices films stop with it. A <video> in a display:none subtree paints
        nothing, the script never calls play() under this query, and they are
        muted regardless — so "no video playback" holds three ways over. */
    .nf-col-slide,
    .nf-col-slide.is-on {
        animation: none !important;
        transform: none !important;
    }
}

/* ------------------------------------------- Luke's notes, 2026-08-10 ----- */

/*  Every Never Forgotten page used to open its own <main> INSIDE the layout's
    <main>. That is invalid HTML, and it also broke the flex chain: the inner
    element was a plain block, so `.nf-cols { flex: 1 }` had no definite parent
    height to fill and the columns collapsed to content height, leaving the
    white gap above the footer in the 2026-08-10 markup. They are now .nf-view
    and carry the chain through, so the columns reach the top of the footer.
    (Luke's note supersedes the older "hide the footer" markup - the footer
    stays, the columns just meet it.)  */
.nf-view {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}

/*  The reading view scrolls rather than filling, so it opts out.  */
.nf-view:has(.nf-read) {
    display: block;
    flex: 0 1 auto;
}

/*  "Center content vertically & horizontally to window" - portrait detail,
    Listen and Watch. These pages hold a single panel, so it centres instead of
    stacking from the top.

    The centring has to be applied to .nf-dark-inner, not to .nf-view. The inner
    carries flex:1 and therefore stretches to the full height of the view, so
    justify-content on the view has a single full-height child and nothing to
    move; the content still sat at the top. Centring the inner's own children is
    what actually does it.  */
.nf-view.is-centered > .nf-dark-inner {
    justify-content: center;
}

/*  "Move the back button to the bottom of the page." It sat inline directly under
    the content. In the markup the destination box keeps the SAME left edge and
    only drops down, so it stays left-aligned rather than centring with the panel.

    It is taken out of flow to get there. margin-top:auto was the obvious move and
    does not work: .nf-back sits inside .nf-player-wrap, which is sized by its
    content, so there is no free space for the auto margin to absorb and it
    resolved to 0. Making the wrap fill the height instead would let the auto
    margin win over justify-content and drag the centred content back to the top -
    the two requests in this markup fight each other in normal flow. Absolute
    positioning satisfies both: the content stays centred, the button pins to the
    foot of the view.  */
/*  Positioned on the INNER, not the view. The view spans the full window, so
    anchoring there dropped the button at the far left edge, detached from the
    centred column it belongs to. The inner is that column, and it already fills
    the view's height, so it gives both the correct left edge and the page foot. */
.nf-view.is-centered > .nf-dark-inner {
    position: relative;
}

    /*  Stretched across the inner and constrained to the same 1080px column as
        .nf-player-wrap, so the link lands on the content's left edge rather than
        the window's. Anchoring to the inner alone put it at x=48 - the inner is
        full-width; it is the wrap inside it that is the centred column.  */
    /* Its positioned ancestor is .nf-dark-inner, which IS the page box — so it
       simply fills it. Applying the page width again here inset it by a second
       gutter and left the link 40px right of the breadcrumb above it. */
    /*  width:auto, not 100%. It was full-width so that text-align:left would
        pin the label to the content edge, which also made the whole empty width
        of the page a click target for "back". Now the padded box IS the target
        and it is only as wide as the label, with the same -.85rem pull as the
        in-flow version so the arrow still lands on the content edge. */
    .nf-view.is-centered .nf-back {
        position: absolute;
        left: 0;
        right: auto;
        bottom: clamp(1.25rem, 3vw, 2.5rem);
        width: auto;
        margin: 0 0 0 -.85rem;
        text-align: left;
    }

        /* The link above is out of flow, so nothing stops the content growing
           underneath it. It did: a two-line film description — which every
           delivered film has, the placeholders were all one line — ran straight
           through the link. Reserve its band.

           .nf-detail is here for the same reason and was missed the first time:
           the Faces portrait view is also is-centered, so its bio ran under the
           back link on a phone. It showed up as the link tangled in the photo
           strip that used to sit at the foot of that column. */
        .nf-view.is-centered .nf-player-wrap,
        .nf-view.is-centered .nf-detail {
            padding-bottom: calc(clamp(1.25rem, 3vw, 2.5rem) + 2.25rem);
        }
