/* =========================================================================
   quizflix.mobile.css — the phone presentation.

   SCOPE, and it is absolute: every rule in this file lives inside one
   `@media (max-width: 767px)` block. 767 and not 768 because 768 is the app's
   existing breakpoint and it FIRES ON IPAD PORTRAIT — a tablet must load this
   file and get nothing from it. Loaded last in every <head> so it wins on
   order; where a base rule is id-scoped (#media-modal …) the override is
   id-scoped too, because order does not beat specificity.

   THE FOUR RULES THIS FILE IS BUILT ON (owner, 2026-08-19):

     1. DENSE, NOT BIG. No full-width slabs, no screen-filling controls. Where a
        control has to be 44px for Google's tap-target audit, the 44px is the
        HIT AREA and the ink stays small — padding and min-height, never a
        bigger box with a bigger fill.
     2. PAGED, NOT SCROLLED. Long prose folds behind a control. "I rather remove
        stuff and hide it in buttons than have it all cluttered up."
     3. SOLID CHROME. Nothing transparent over a photograph, nothing floating
        over a heading.
     4. CUT FOR MOBILE. Decoration is deleted here, not reflowed: ad slots,
        the Ken Burns pan, the hover transforms.

   Priority order of the sections below is the owner's: the public front door
   (lander) and the Featured blog come first — they are what a stranger sees.
   ========================================================================= */

@media (max-width: 767px) {

    /* ── 0. GROUND ───────────────────────────────────────────────────────── */

    html {
        /* Stops iOS inflating text in a rotated column, which is what makes a
           carefully sized line suddenly wrap on a phone that is lying down. */
        -webkit-text-size-adjust: 100%;

        /* The public navbar is `position: fixed` and 52px tall (section 1), so
           it covers the top 52px of everything. A snapped section is aligned to
           `scroll-padding-top: 52px` — its start edge lands UNDER the bar — so
           a section that is 100svh tall has its last 52px below the fold and is
           not "one screen" at all. Caught on an SE, where row 3's photograph was
           cut by the viewport edge in exactly that band.

           Every full-view section below is `calc(100svh - var(--m-bar))`. One
           number, because the bar's height and this subtraction must never
           drift apart. */
        --m-bar: 52px;

        /* The poster on a paged portal screen (section 7.1). Height, because
           that is what has to fit the screen; the width is this times the
           card's own 132:240 ratio so the crop never changes. `svh` so a tall
           phone gets a bigger card instead of more empty page, capped so it
           cannot run away on a tablet-sized phone. */
        --m-poster: min(52svh, 400px);

        /* 🛑 A JOIN GRADIENT IS A TRANSITION, NOT A TEXT PLATE. Owner,
           2026-08-19: "The gradient is a transition not the thing that sits
           behind the text they should not be related."

           This is how deep the header image dissolves into the page at its
           BOTTOM edge, and it is the only thing that number describes. It is
           not sized to a heading, it does not move when a heading moves, and
           nothing is allowed to derive its own legibility from it — text
           carries a `text-shadow` instead.

           The film page has had exactly this shape since 2026-08-17
           (`--film-fade-h` in theme/quizflix/film.css: a fixed band at the
           seam, with the logo held clear of it by a derived padding). This is
           the same idea for the `.hero-section` screens, kept as its own token
           because a hero band can be shorter than a film header. */
        --m-join: 120px;
    }

    body {
        /* Google's mobile-friendly test fails a page whose content is wider
           than the viewport. Several rows here are 240px cards and a 400px
           trailer box; they are each fixed below, and this is the backstop so
           one missed rule cannot make the whole page slide sideways. */
        overflow-x: hidden;
    }

    img,
    video,
    iframe {
        max-width: 100%;
    }

    /* Motion that costs battery and buys nothing at this size. */
    .hero-bg-anim {
        animation: none !important;
    }

    /* Decoration that is not content. Cut, per rule 4. */
    .ad-slot {
        display: none !important;
    }


    /* ── 1. THE PUBLIC NAVBAR ────────────────────────────────────────────
       Owner: "The transparency in the header looks like shit when an image is
       behind it."

       It is worse than a phone problem. `.navbar.scrolled` — the only rule that
       makes the bar opaque — is applied by assets/script/landing/script.js,
       and that file is loaded by index.html ALONE. So on /blog/, /blog/feature
       and /info/ the header is permanently the transparent gradient, at every
       width. This makes it solid on phones outright rather than depending on a
       scroll handler that four of the five public pages never load. */

    .navbar,
    .navbar.scrolled {
        height: 52px;
        padding: 0 14px;
        background: #0b0b0b;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        border-bottom: 1px solid rgba(255, 255, 255, 0.09);
    }

    /* The wordmark is a link home on every public page, so it is a tap target:
       61x23 measured, which fails the audit. The ink stays 1.45rem; the box
       grows. */
    .navbar .logo,
    .navbar a.logo {
        font-size: 1.45rem;
        line-height: 1;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }

    .nav-right {
        gap: 8px;
    }

    /* Small ink, legal hit area: the 44px is min-height, the type stays 0.8rem
       and the fill stays the size of the words. */
    .nav-right .nav-link {
        font-size: 0.85rem;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        padding: 0 6px;
    }

    /* BETA FIX 1 — there is ONE control here now and it is a plain link, so
       the `.nav-right .btn-sm` rule that sized the red Sign Up pill is gone
       with it. `.nav-auth` is styled in landing.css at every width; this only
       buys back the tap target. */
    .nav-right .nav-auth {
        font-size: 0.85rem;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        padding: 0 4px;
    }


    /* ── 2. THE LANDER ──────────────────────────────────────────────────── */

    /* BETA MOBILE FIX 2 (owner, 2026-08-19): "on mobile the header is the
       background it should be the full page. The text should be below it and
       scroll up over the image. The text background should be gradient
       transparent top to solid black at the very bottom so the image slowly
       fades away behind the text as it scrolls down."

       Three parts, and only one of them is new markup-free luck:

         - `.hero-bg-anim` is ALREADY `position: fixed` (landing.css). So the
           photograph does not move and the text passing over it is a
           consequence of scrolling, not of a parallax script. Nothing to add.
         - The text is pushed down by 56svh of padding INSIDE .hero-content, so
           the first thing on the screen is a clean, uninterrupted frame of the
           picture and the copy begins below it.
         - That padding is inside the gradient box, not above it, which is what
           makes the fade continuous: transparent where the image is clean,
           solid black by the bottom edge, and the section under the hero is
           black too, so the seam does not exist.

       Rule 3 (solid chrome) is not violated. That rule is about a BAR or a
       CONTROL floating on a photo. This is the opposite: the copy brings its
       own ground with it.

       `100svh` not `100vh`: on iOS Safari `vh` is the height with the address
       bar retracted, so 100vh puts the call-to-action under the browser chrome
       on first paint. */
    .hero {
        height: auto;
        min-height: 100svh;
        padding: 0;
        display: flex;
        align-items: flex-end;
    }

    /* 🔴 `.hero .hero-content`, NEVER BARE `.hero-content`. This rule is the
       LANDER's text plate — a gradient that runs to solid black and carries the
       copy over a fixed photograph. `.hero-content` is also the class every
       PORTAL hero puts its heading in (home.js, arcade.js, search.js,
       profile.js, quizflix.render.logic.js), and this stylesheet loads on those
       pages too. Unscoped, the plate travelled: a black-to-transparent box
       wrapped around the title of every portal screen, sized to the text and
       moving with it.

       That is the defect the owner reported on 2026-08-19 — "behind the text is
       that black gradient that is the top of the content for blending not a
       shadow to make text visible… That gradient needs to be where it belongs
       not following text around" — and it is why the last three rounds of
       hero work kept finding a black band no hero rule accounted for. The
       lander's hero is `.hero`; the portals' is `.hero-section`, so one
       ancestor separates them for good.

       ⚠️ quizflix.responsive.css already carries the mirror-image warning at
       `.hero-section:not(.exercise-hero) .hero-content` — a bare `.hero-*` rule
       here restyles the lander. It is the same trap from the other side. */
    .hero .hero-content {
        margin-top: 0;
        max-width: none;
        width: 100%;
        padding: 56svh 18px 28px;
        background: linear-gradient(180deg,
                rgba(0, 0, 0, 0) 0%,
                rgba(0, 0, 0, 0.15) 34%,
                rgba(0, 0, 0, 0.55) 52%,
                rgba(0, 0, 0, 0.86) 74%,
                #000 100%);
    }

    /* The desktop vignette is a radial that reaches rgba(5,5,5,1) at 90% — on a
       390px frame that is most of the picture painted out, and the owner asked
       for the picture. The gradient above is doing the legibility work now, so
       this only keeps the corners from glaring. */
    .vignette {
        background: radial-gradient(circle at center,
                rgba(0, 0, 0, 0) 0%,
                rgba(0, 0, 0, 0.35) 100%);
    }

    /* 12px floor, here and everywhere below. Google's mobile-friendly test
       fails a page for "text too small to read" under it, and the desktop
       0.9rem-of-a-scaled-root landed at 10.56px measured. */
    .hero-tags {
        font-size: 0.75rem;
        letter-spacing: 1.4px;
        gap: 7px;
        margin-bottom: 10px;
    }

    .hero h1,
    .hero .hero-headline {
        /* 5rem is 80px — two words a line on a 390px screen. Trimmed again for
           BETA FIX 4 ("all the headers" are too big); still the largest type on
           the page, which is the one place that is correct. */
        font-size: 2.2rem;
        line-height: 1.02;
        margin-bottom: 10px;
        text-shadow: none;
    }

    .hero-sub {
        font-size: 0.95rem;
        line-height: 1.45;
        margin-bottom: 22px;
        /* Four lines, then it stops. The paragraph is 42 words; unclamped it is
           a third of the screen and it pushes both buttons below the fold. */
        display: -webkit-box;
        -webkit-line-clamp: 3;
        -webkit-box-orient: vertical;
        overflow: hidden;
        color: #cfcfcf;
    }

    /* Two controls on ONE line. Stacked full-width they are the "huge buttons"
       the owner rejected, and they cost 60px of the first screen. */
    .cta-group {
        display: flex;
        flex-direction: row;
        gap: 10px;
    }

    /* BETA FIX 3 — smaller and squarer than before. The 44px stays because it
       is the audit's hit area; what shrank is the ink and the radius, which is
       rule 1 exactly. The flat/no-glow treatment itself lives in landing.css so
       desktop gets it too. */
    .cta-group .btn,
    .cta-group .btn-lg {
        flex: 0 1 auto;
        min-width: 0;
        min-height: 44px;
        padding: 0 16px;
        font-size: 0.75rem;
        letter-spacing: 0.05em;
        border-radius: 2px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 7px;
        white-space: nowrap;
    }

    .cta-group .btn i {
        font-size: 0.8em;
    }

    /* ROUND 2 (owner, 2026-08-19): "make all the sections in the imersion
       method also snap to screens… Perperation and evaluation can have the
       image below it, you can hide the video above it. Study on the go same
       thing keep the image from below the text but snap that screen to view."

       So each `.feature-row` is its own screen. Stacked they were the "scroll
       for miles"; a screen each with the prose folded is the paged reading the
       brief asks for. The order is already text-then-picture — the rows are
       `flex-direction: column` and `.reverse` is neutralised — which is the
       "image below it" the owner describes, so nothing moves. */
    .feature-row,
    .feature-row.reverse {
        display: flex;
        flex-direction: column;
        justify-content: center;
        gap: 14px;
        margin-bottom: 0;
        min-height: calc(100svh - var(--m-bar));
        /* Tight, because the screen is the budget. An iPhone SE is 667px and
           row 2 is a nine-line paragraph plus a three-item checklist plus a
           picture; at 56/40 it ran 71px past the fold and the photo was cut by
           the viewport edge. Measured on 375x667, not assumed. */
        padding: 40px 0 24px;
        scroll-snap-align: start;
    }

    /* Screen 1 shares its screen with "The Immersion Method" — the h2 is the
       snap point for the pair, so the row does not declare a second one 26px
       further down. The 62px subtraction IS that heading block — 22 padding +
       26 line + 14 margin — and it is set with those numbers at #methodology
       below. Change one, change the other. Measured on an SE: row 1 is
       content-driven at 531px, so the pair has to fit 615px of usable screen. */
    .feature-row:first-of-type {
        min-height: calc(100svh - var(--m-bar) - 62px);
        padding-top: 0;
        scroll-snap-align: none;
    }

    /* 🛑 The video is cut, on the owner's word: "you can hide the video above
       it." It is the only motion on the page, it is the tallest thing in the
       three rows, and screen 1 is the one carrying three paragraphs. Rule 4 —
       cut for mobile, do not reflow. */
    .feature-visual.visual-1 {
        display: none;
    }

    .feature-visual {
        width: 100%;
        margin: 0;
        /* The base is a fixed 350px box. Inside a screen that also holds a
           heading, a paragraph and a checklist, 350px is what pushes the last
           list item under the fold. */
        height: auto;
        /* svh, so the cap tracks the screen it has to fit inside: 160px on an
           SE, 202px on a 390x844. A fixed px would fit one phone and break the
           other. */
        max-height: 24svh;
        overflow: hidden;
    }

    .feature-visual img,
    .feature-visual video,
    .feature-visual .ui-player,
    .feature-visual .ui-doc {
        width: 100%;
        height: auto;
        max-height: 24svh;
        object-fit: cover;
        display: block;
        border-radius: 8px;
    }

    .section-dark,
    .row-dark {
        padding-left: 0;
        padding-right: 0;
    }

    /* Row 2's paragraph is nine lines on a 375x667 SE and it is a single <p>,
       so `foldLandingProse()` leaves it alone — correctly: "a Read more that
       reveals nothing is worse than the scroll it saves." That put the row 25px
       past its screen on the short phone only. The picture gives the 25px back,
       and only where it is needed — a 390x844 has the room and keeps the
       larger crop. */
    @media (max-height: 700px) {

        .feature-visual,
        .feature-visual img,
        .feature-visual video,
        .feature-visual .ui-player,
        .feature-visual .ui-doc {
            max-height: 19svh;
        }
    }

    /* The section's own 80px top/bottom is dead space once every row inside it
       is a full screen with its own padding. The heading keeps a band clear of
       the 52px bar, and that band is what .feature-row:first-of-type takes off
       its own height: 22 + 26 + 14 = 62.

       ROUND 3 (owner, 2026-08-19): "The immersion method header sits to far
       from the top of the screen when it is snapped into place." The 44px was
       measured against the top of the SECTION, but the section's start edge is
       already parked 52px down by scroll-padding-top, so the gap on screen was
       96px. 22px reads as a margin under the bar; the 52 is not ours to spend
       twice. Same number at #featured-media below. */
    #methodology.section-dark {
        padding-top: 0;
        padding-bottom: 0;
    }

    #methodology .section-header {
        padding-top: 22px;
        margin-bottom: 14px;
        scroll-snap-align: start;
    }

    .container {
        padding-left: 16px;
        padding-right: 16px;
    }

    /* BETA FIX 4/5 (owner, 2026-08-19): "The text on the header featured
       attraction header is too big as are all the headers… The immersion method
       part also the headers and text are all too big."

       One step down across the board. .strip-title inherits this — it is
       .section-header plus a text-align, so the size lives here only. */
    .section-header,
    .strip-title {
        font-size: 1.35rem;
        letter-spacing: 0.01em;
        margin-bottom: 16px;
    }

    /* Ragged-centre body copy is a desktop conceit that survives because a
       desktop line is 60 characters. Here it is 34, so eight centred lines is
       eight different left edges and the eye loses the column on every one. */
    .feature-text,
    .feature-text h3,
    .feature-text p,
    .feature-list li {
        text-align: left;
    }

    .feature-text p {
        font-size: 0.88rem;
        line-height: 1.5;
        color: #b9b9b9;
    }

    .feature-text h3 {
        font-size: 1.05rem;
        line-height: 1.25;
        margin: 0 0 6px;
    }

    .step-badge {
        font-size: 0.78rem;
        margin-bottom: 5px;
    }

    /* 🔴 SCOPED TO THE LANDER, AND THAT IS THE WHOLE OF ROUND 5 ITEM 6.
       Owner, 2026-08-19: "the vocabulary words have made the sentences appear
       to be a table they are supposed to be inline with the rest of the article
       words they are vocabulary words not some sort of normal control."

       `.feature-list` is TWO DIFFERENT LISTS with one class name. On the lander
       it is the Immersion Method's tick list, where every <li> is an icon and a
       phrase. In a Featured article it is a prose list, and its items are
       sentences with `<a class="vocab-word">` glossary links inside them.

       The rule below makes an <li> a FLEX CONTAINER (it had to — see the tick
       note). A flex container turns every child into a flex item, and an
       anonymous text node is a child: a sentence with one vocab link in it
       becomes three columns — the words before, the word itself, the words
       after — which is the "table" the owner is looking at. Nothing was done to
       the vocabulary link at all; it was collateral from a fix two rounds ago
       on a different page.

       `body.page-lander` is the fence. The article's lists are left exactly as
       blog.css draws them.

       ⚠️ Anything added here must be asked the same question: is this class the
       lander's, or is it shared? `.feature-row`, `.feature-text` and
       `.feature-visual` above are lander-only markup; `.feature-list`,
       `.feature-card` and `.feature-title` are not. */
    body.page-lander .feature-list {
        margin: 12px 0 0;
        padding: 0;
        list-style: none;
    }

    /* 🔴 THE TICKS SAT OUTSIDE THE PAGE. Owner, on an iPhone SE (375px):
       "the checkmarks in the list sit outside the page."

       The cause is the hanging indent this replaces — `padding-left: 22px` with
       `text-indent: -22px`. A hanging indent is a guess about how wide the
       thing that hangs is, and Font Awesome's `<i>` is not 22px: the glyph box
       is the icon's own advance width PLUS the 10px `margin-right` that
       landing.css:463 gives it. Guess low and the icon is pulled past the
       container's left padding and off the page. It is width-dependent, which
       is why a 390px frame nearly hid it and a 375px one did not.

       A flex row cannot do this. The icon is a real, measured first item and
       the text is the second; nothing hangs, nothing overhangs, and the wrapped
       second line still aligns under the first because the text is its own flex
       item. `margin-right: 0` because the gap owns the spacing now. */
    body.page-lander .feature-list li {
        font-size: 0.82rem;
        line-height: 1.35;
        padding: 5px 0;
        text-indent: 0;
        display: flex;
        align-items: flex-start;
        gap: 9px;
    }

    body.page-lander .feature-list li i {
        flex: 0 0 auto;
        margin-right: 0;
        line-height: inherit;
        /* Optical: the tick's ink is smaller than a cap, so it reads high
           against the first word without this. */
        margin-top: 0.15em;
    }

    /* BETA FIX 6 (owner, 2026-08-19): "The counter section should be a snap to
       section and the counters can be displayed vertically instead of
       horizontally."

       This reverses the earlier "three boxes across, not three screens down" —
       which was the right call while the section was a 26px-tall strip among
       ten screens of scroll. It owns a screen now, so three across in 362px is
       three cramped columns for no reason. The snap is declared in section 2.9
       with the other two. */
    /* 🔴 …and it needs a background of its own. `.row-light` (landing.css:1063)
       wins over `.stats-section`'s opaque gradient on source order, and it is
       `--hover-overlay → --text-muted` — BOTH TRANSLUCENT. The lander's
       `.hero-bg-anim` is `position: fixed`, so what shows through is the cinema
       photograph, greyed. At 60px tall nobody saw it; at a full screen it is
       rule 3 broken in the most literal way there is.
       ⚠️ Pre-existing and NOT phone-only — it is transparent at every width.
       Fixed here for the phone; the desktop row is a separate call. */
    .stats-section,
    .stats-section.row-light {
        background: #0c0c0c;
        border-top: 1px solid rgba(255, 255, 255, 0.07);
        border-bottom: 1px solid rgba(255, 255, 255, 0.07);
    }

    .stats-section {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        gap: 34px;
        min-height: calc(100svh - var(--m-bar));
        padding: 64px 24px;
        text-align: center;
    }

    .stat-box {
        flex: 0 0 auto;
        width: 100%;
        max-width: 260px;
        padding: 0;
        opacity: 1;
    }

    .stat-box i {
        font-size: 1.2rem;
        margin-bottom: 8px;
    }

    .stat-number {
        font-size: 2.6rem;
        line-height: 1;
    }

    .stat-label {
        font-size: 0.72rem;
        line-height: 1.2;
        letter-spacing: 0.14em;
        margin-top: 6px;
    }

    /* BETA FIX 7 — "high value high impact section text is ok but this section
       again is 1 view so once its close it should snap into place." The copy is
       untouched; it is given the screen and the snap. */
    #outro {
        min-height: calc(100svh - var(--m-bar));
        display: flex;
        align-items: center;
        padding: 56px 0;
    }

    .outro-container {
        padding: 0 22px;
    }

    .outro-container h3 {
        font-size: 1.35rem;
        margin-bottom: 14px;
    }

    .outro-container p {
        font-size: 0.9rem;
        line-height: 1.55;
        color: #b9b9b9;
        margin-bottom: 26px;
    }

    /* Not full width: an inline button reads as a button, a 358px red bar reads
       as a banner. */
    .outro-container .btn-lg {
        min-height: 44px;
        padding: 0 22px;
        font-size: 0.75rem;
        letter-spacing: 0.05em;
        border-radius: 2px;
        display: inline-flex;
        align-items: center;
    }


    /* ── 2.9 THE FULL-VIEW SNAP SECTIONS ─────────────────────────────────
       Owner, 2026-08-19: the Featured Presentation, the counters and the
       closing pitch are each "1 view", and "once its close it should snap into
       place before scrolling more again."

       🛑 `proximity`, never `mandatory`. That word is the owner's brief read
       literally — mandatory hijacks every flick, including one aimed at the
       three-essay Immersion Method between two snap points, and a section
       taller than the phone becomes unreachable in the middle. Proximity only
       finishes a scroll that already stopped near an edge.

       🛑 Scoped to the LANDER. `html:has(body.page-lander)` and not bare `html`
       because this file is loaded by nineteen pages and a snap axis on the
       document affects every one of them. The selector is on <html> and not
       <body> because scroll-snap-type does NOT propagate body → viewport the
       way overflow does; on <body> alone it silently does nothing. Browsers
       without :has() get the page exactly as it scrolls today. */
    html:has(body.page-lander) {
        scroll-snap-type: y proximity;
        scroll-padding-top: 52px;
        /* 52px = the mobile navbar in section 1. Without it a snapped heading
           lands underneath the fixed bar. */
    }

    body.page-lander #featured-media,
    body.page-lander .stats-section,
    body.page-lander #outro {
        scroll-snap-align: start;
        scroll-snap-stop: normal;
    }

    /* The Featured Presentation takes the screen it snaps to.

       ROUND 3 (owner, 2026-08-19): "the text should be at the top of the view
       and the cards larger, they should fill the view not look like a little
       band across the center." So the column starts at the top instead of
       centring, and the strip — still a strip — takes every pixel the title
       does not. 22px of top padding, the same as the Immersion heading: the
       52px bar is already paid for by scroll-padding-top. */
    body.page-lander #featured-media {
        min-height: calc(100svh - var(--m-bar));
        display: flex;
        flex-direction: column;
        justify-content: flex-start;
        padding-top: 22px;
        padding-bottom: 24px;
        /* The 50px is desktop clearance under a hero that ends in a photograph.
           The mobile hero ends in solid #000 (FIX 2), so it is a black band
           between two black things AND it pushes the snap point off by 50px. */
        margin-top: 0;
    }

    /* 🔴 THE GRADIENT THAT WAS FADING THE HERO BUTTONS. Owner, round 3:
       "the two buttons that still look childish are now under a gradient so
       they look faded down to the bottom." Not the buttons — computed style
       says solid #e50914 at opacity 1. `.content-strip::before` is a 150px
       transparent→black panel at `top: -150px` on a `z-index: 10` section, so
       it paints OVER the bottom 150px of the hero, which since margin-top: 0
       is exactly where the call-to-action pair sits. Desktop keeps it (it
       feathers a hero that ends in photograph); the mobile hero already ends
       in solid #000, so here it only greys the controls. */
    .content-strip::before {
        display: none;
    }

    /* 🛑 THE STRIP STAYS A FILM STRIP. Owner, 2026-08-19: "featured
       presentations as a film strip was fine you were not told to change that
       just the header text." An earlier pass turned it into a 2-column grid —
       scope the owner did not give, and reverted. The horizontal scrolling row
       IS the design; round 3 only makes the cards bigger inside it, which is
       what "the cards larger, they should fill the view" asks for. The card is
       one poster wide (78vw leaves a slice of the next one showing, which is
       what tells a thumb the row scrolls) and as tall as the section leaves. */
    #featured-media .strip-title {
        flex: 0 0 auto;
        margin-bottom: 14px;
    }

    /* 🔴 ROUND 5 — THE ROW WAS SCROLLING UP AND DOWN. Owner, 2026-08-19: "the
       featured attraction row is smaller than the items causing the text to be
       cut in the bottom and the row to have a small scroll up down effect that
       needs to be fixed."

       Two causes, both here:

       1. `overflow-x: auto` (landing.css) computes `overflow-y: auto` as well —
          the spec turns a `visible` axis into `auto` when the other one is not
          visible. Measured, the card is 529.09px inside a 529px row: a ninth of
          a pixel of overflow is enough for the browser to make the row
          scrollable, and what it cuts is the bottom of the caption bar, which is
          the one thing pinned to the card's bottom edge.
       2. `.strip-card:hover { transform: scale(1.1) }` — on a touch screen a tap
          latches :hover, and a 529px card at 1.1 is 582px in a 529px row. That
          is a real 53px of vertical scroll, and it is why the effect showed up on
          the owner's phone and not in a desktop window.

       So: the row never scrolls vertically, and nothing inside it grows. Hover is
       a pointer idea; there is no pointer here. */
    #featured-media .strip-container {
        flex: 1 1 auto;
        min-height: 0;
        align-items: stretch;
        justify-content: flex-start;
        gap: 12px;
        padding-bottom: 0;
        overflow-y: hidden;
    }

    #featured-media .strip-card {
        min-width: 78vw;
        height: auto;
        align-self: stretch;
        border-radius: 8px;
    }

    .strip-card:hover,
    .strip-card:focus,
    .strip-card:active {
        transform: none;
        z-index: auto;
        box-shadow: none;
    }

    /* The caption was 0.8rem on a 200px card; on a full-height poster it is the
       only text in the frame and it was reading as a footnote. */
    /* The caption sits ON the clipped bottom edge, so it is the first thing a
       sub-pixel overflow eats. The extra bottom padding is the margin of error. */
    #featured-media .strip-card span {
        font-size: 0.9rem;
        font-weight: 600;
        padding: 11px 12px 14px;
        line-height: 1.25;
        background: rgba(0, 0, 0, 0.78);
    }

    .main-footer {
        padding: 26px 16px;
    }

    .footer-col {
        margin-bottom: 18px;
    }

    /* Footer links measured 358x23 — full-bleed rows of text, two lines apart.
       A 40px row is both a legal target and easier to hit than a 23px one
       sitting 6px from its neighbour. */
    .footer-col a,
    .footer-bottom a {
        display: inline-flex;
        align-items: center;
        min-height: 40px;
    }


    /* ── 3. THE FEATURED BLOG ───────────────────────────────────────────
       Owner, first complaint of the whole review: "the menu in featured is
       above the blog listing 13 other blogs right under the title."

       The cause is source order — <aside class="blog-rail"> is the FIRST child
       of .feature-layout, so when blog.css collapses the grid to one column at
       1180px the rail lands above the piece. Two `order` declarations move it
       under, and quizflix.mobile.js folds the 13 names into one button. */

    .blog-shell {
        padding: 64px 12px 28px;
    }

    .feature-layout {
        gap: 1.25rem;
    }

    .feature-panel {
        order: 1;
        padding: 1.25rem 1rem 1.5rem;
        border-radius: 10px;
        box-shadow: none;
    }

    .blog-sidebar {
        order: 2;
    }

    .blog-rail {
        order: 3;
        padding-bottom: 0;
        padding-top: 1.25rem;
        border-bottom: 0;
        border-top: 1px solid rgba(255, 255, 255, 0.08);
    }

    /* Folded (quizflix.mobile.js adds .is-folded and the button before the
       list). Left alone if the script never runs: the pills below are the
       1180px band, which is correct-if-long, so a failed script costs length,
       never access. */
    .blog-rail .rail-group.is-folded .rail-list {
        display: none;
    }

    .blog-rail .rail-group.is-folded:has(> .m-disclose[aria-expanded="true"]) .rail-list {
        display: flex;
    }

    .feature-crumb {
        font-size: 0.75rem;
        margin-bottom: 0.75rem;
    }

    /* Article typography. The panel is 358px wide here, so the desktop scale
       puts four words on a headline line. */
    .feature-article h1,
    .feature-title {
        font-size: 1.75rem;
        line-height: 1.08;
    }

    .feature-article h2 {
        font-size: 1.25rem;
        line-height: 1.2;
    }

    .feature-article h3 {
        font-size: 1.05rem;
    }

    .feature-body p,
    .feature-article p {
        font-size: 1rem;
        line-height: 1.62;
    }

    .feature-article figure {
        margin-left: 0;
        margin-right: 0;
    }

    .feature-article img {
        height: auto;
        border-radius: 8px;
    }

    /* The sign-up card stays — it is the reason the blog is public. The rest of
       the rail's furniture goes quiet. */
    .blog-sidebar {
        gap: 1rem;
    }

    .sidebar-card {
        padding: 1rem;
        border-radius: 10px;
    }

    .sidebar-card h3,
    .sidebar-card h2 {
        font-size: 1.05rem;
        margin: 0 0 0.5rem;
    }

    .sidebar-card p {
        font-size: 0.88rem;
        line-height: 1.45;
    }

    .sidebar-card .btn {
        min-height: 40px;
        padding: 0 16px;
        display: inline-flex;
        align-items: center;
        gap: 6px;
    }

    /* ── THE BLOG INDEX ────────────────────────────────────────────────
       Measured at 8,517px — ten screens for thirteen articles, because one
       card is a full-bleed 16:9 photograph, a two-line headline and a
       forty-word standfirst: 1.2 screens each. Owner: "those cards are huge
       they wont all fit on one page with other information."

       The card becomes a ROW: a 104px thumbnail beside the kicker, the title
       and the date. Six to a screen instead of four-fifths of one, and it is
       the same markup — no second renderer to keep in step with
       blog.index.js. */

    .blog-grid,
    .article-grid {
        grid-template-columns: 1fr !important;
        gap: 14px;
    }

    /* clamp(320px, 42vh, 460px) plus 8.5rem of top padding is 460px of
       photograph before the first article. */
    .featured-hero {
        min-height: 0;
        padding: 5.5rem 1rem 1.5rem;
    }

    .featured-hero-inner {
        padding: 0;
    }

    .blog-masthead h1 {
        font-size: 2rem;
        line-height: 1;
        margin-bottom: 8px;
    }

    .blog-masthead p {
        font-size: 0.9rem;
        line-height: 1.45;
        margin: 0;
    }

    .feature-stream {
        gap: 10px;
    }

    .feature-card {
        display: grid;
        grid-template-columns: 88px minmax(0, 1fr);
        gap: 12px;
        align-items: center;
        padding: 10px;
        border-radius: 10px;
    }

    /* 🔴 `min-height: 11rem` on the base rule is what makes the thumbnail 176px
       tall however narrow the column is — the row is only as short as this
       lets it be. */
    .feature-card-hero {
        margin: 0;
        min-height: 0;
        height: 88px;
        border-radius: 6px;
        overflow: hidden;
    }

    .feature-card-hero img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }

    .feature-card-body {
        padding: 0;
        display: flex;
        flex-direction: column;
        gap: 4px;
        min-width: 0;
    }

    .feature-card-kicker {
        font-size: 0.7rem;
        margin: 0;
    }

    .feature-card-title {
        font-size: 1.05rem;
        line-height: 1.12;
        margin: 0;
        /* Three lines is enough for every title in the manifest; the longest
           ("Bruce Campbell: The King of the B Movie") is two. */
        display: -webkit-box;
        -webkit-line-clamp: 3;
        -webkit-box-orient: vertical;
        overflow: hidden;
    }

    .feature-card-meta {
        font-size: 0.75rem;
        margin: 0;
    }

    /* Cut, per rule 4. The standfirst is two thirds of the card's height and
       the reader is one tap from the whole piece. */
    .feature-card-summary {
        display: none;
    }


    /* ── 4. THE PORTAL APP BAR ──────────────────────────────────────────
       Owner: "The menu button looks like its stuck ontop of the top of the
       screen, the effects and look of it do not match the rest of the app and
       it covers the quizflix logo."

       All three are one cause: `.mobile-menu-btn` is `position: fixed; top:
       15px; left: 15px` — a control floating on the page with nothing behind
       it, landing on the first letter of whatever heading is underneath.

       quizflix.mobile.js builds a real bar and moves that same button into it
       (it does not clone it — quizflix.navigation.js owns the click). The bar
       is opaque and 52px, the same bar as the public navbar in section 1, so
       the two halves of the product finally look related. */

    .m-appbar {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: 1200;
        height: 52px;
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 0 8px;
        background: #0b0b0b;
        border-bottom: 1px solid rgba(255, 255, 255, 0.09);
    }

    /* Restyled in place, not re-created: same element, same id, same listener. */
    .m-appbar .mobile-menu-btn,
    .mobile-menu-btn {
        position: static;
        top: auto;
        left: auto;
        z-index: auto;
        width: 44px;
        height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        padding: 0;
        margin: 0;
        font-size: 1.15rem;
        line-height: 1;
        color: #fff;
        background: transparent;
        border: 0;
        border-radius: 6px;
    }

    .m-appbar .m-appbar-logo {
        font-family: 'Bebas Neue', sans-serif;
        font-size: 1.4rem;
        line-height: 1;
        letter-spacing: 0.03em;
        color: var(--brand-red, #e50914);
        text-decoration: none;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }

    /* The bar is fixed, so the page needs the 52px back. */
    body.m-has-appbar .main-content {
        padding-top: 52px;
    }

    /* 🛑 THE DRAWER IS THE WHOLE SCREEN. Owner, 2026-08-19: "the nav on mobile
       should be full screen." It was `min(78vw, 300px)` — a drawer with a strip
       of the page showing beside it, which is a tablet pattern on a phone.

       Full screen means it also covers `.m-appbar` (z-index 1200), so the ☰
       that opened it is no longer there to close it. `buildNavClose()` in
       quizflix.mobile.js puts a ✕ in the same corner — same 44px target, same
       thumb position — and closes by clicking the same #menuToggle, because
       quizflix.navigation.js owns that state and a second owner is how a drawer
       gets stuck open. */
    .sidebar {
        width: 100vw;
        max-width: 100vw;
        z-index: 1300;
        padding-top: 8px;
    }

    /* Top RIGHT, not left: the ✕ sits opposite the QUIZFLIX wordmark the drawer
       already opens with, so neither has to move for the other. */
    .m-navclose {
        position: absolute;
        top: 4px;
        right: 8px;
        width: 44px;
        height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        padding: 0;
        border: 0;
        border-radius: 6px;
        background: transparent;
        color: #fff;
        font-size: 1.5rem;
        line-height: 1;
        cursor: pointer;
    }

    /* The scrim stays exactly as it is: at z-index 1250 it is now completely
       behind the drawer, so its dimming never paints — but it is still the
       Escape handler and the thing that catches a tap during the slide. */

    .m-scrim {
        position: fixed;
        inset: 0;
        z-index: 1250;
        border: 0;
        padding: 0;
        background: rgba(0, 0, 0, 0.6);
        opacity: 0;
        pointer-events: none;
        transition: opacity 0.2s ease;
    }

    .m-scrim.is-open {
        opacity: 1;
        pointer-events: auto;
    }


    /* ── 5. THE MEDIA MODAL ─────────────────────────────────────────────
       Owner: "the movies are completely broken the modal wont allow anyone to
       enter the movie."

       Exactly right, and it is two rules colliding. `.modal-content` sets
       `max-height: 90vh; overflow-y: auto` — but `#media-modal .modal-content`
       (quizflix.css:861) then sets `overflow: hidden`, and an id beats a class.
       So the box is capped at 90vh with no way to reach the rest of it, and
       #modal-go-button — "Go to Movie" — is below the cut on every phone.

       Both overrides here are id-scoped for that reason. */

    #media-modal .modal-content {
        max-height: 92svh;
        overflow-y: auto;
        overflow-x: hidden;
        margin: 4svh auto;
        width: calc(100vw - 20px);
        max-width: none;
        padding: 14px;
        box-sizing: border-box;
    }

    /* One column, and the fixed 50%/303px/340px boxes let go. */
    #media-modal .modal-header {
        display: flex;
        flex-direction: column;
        gap: 12px;
    }

    #media-modal .modal-header>div,
    #media-modal .modal-header>div:first-of-type {
        width: 100%;
        max-width: none;
        max-height: none;
    }

    #media-modal #modal-profile {
        width: 100%;
        height: auto;
        aspect-ratio: 16 / 9;
        object-fit: cover;
        border-radius: 8px;
    }

    /* The two actions side by side and small — 28px tall was the old size, and
       it fails Google's tap-target audit; 40px passes without becoming a slab. */
    #media-modal #modal-go-button,
    #media-modal #modal-play-button {
        width: calc(50% - 5px);
        height: auto;
        min-height: 40px;
        font-size: 0.8rem;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        margin: 8px 5px 0 0;
    }

    #media-modal #modal-play-button {
        margin-right: 0;
    }

    #media-modal #modal-title {
        font-size: 1.25rem;
        line-height: 1.15;
        margin: 0 0 4px;
    }

    /* 🔴 A 155px box with `overflow-y: scroll` and a hidden scrollbar: on a
       phone it reads as a paragraph that simply stops mid-sentence. Clamped
       instead, so it is visibly truncated and the modal stays one screen. */
    #media-modal .modal-summary,
    #media-modal #modal-summary {
        max-height: none;
        overflow: hidden;
        font-size: 0.88rem;
        line-height: 1.5;
        display: -webkit-box;
        -webkit-line-clamp: 5;
        -webkit-box-orient: vertical;
    }

    /* Lessons / Exercises / Activities were three 30%-wide columns with their
       own 125px scrollers — three ~110px columns on a 358px screen. One column
       each, full width, no nested scroller. */
    #media-modal .lessons-container,
    #media-modal .exercises-container,
    #media-modal .activities-container {
        width: 100%;
    }

    #media-modal .deck-list {
        width: 100%;
        max-height: none;
        overflow: visible;
    }

    #media-modal #modal-list-container {
        display: flex;
        flex-direction: column;
        gap: 10px;
        width: 100%;
    }

    /* Measured 220x28 with 10px type: a link that is neither readable nor
       reliably hittable. Full width, 44px, 0.8rem/12.8px. */
    #media-modal .deck-list a,
    #media-modal .deck-list-item {
        display: flex;
        flex-direction: column;
        justify-content: center;
        width: 100%;
        box-sizing: border-box;
        min-height: 44px;
        padding: 8px 10px;
        font-size: 0.8rem;
        line-height: 1.3;
    }

    #media-modal .deck-list h3,
    #media-modal #modal-list-container h3 {
        font-size: 0.95rem;
        margin: 0 0 6px;
    }

    /* The trailer iframe carries width="303" as an attribute. */
    #media-modal #modal-trailer-container,
    #media-modal #trailer-preview {
        width: 100% !important;
        max-width: 100%;
        aspect-ratio: 16 / 9;
        height: auto;
    }


    /* ── 6. FOLDS ───────────────────────────────────────────────────────
       The shared shape behind "hide it in a button". quizflix.mobile.js wraps
       the long thing in `.m-fold` and puts a `.m-disclose` before it; nothing
       here is page-specific, which is why the lander's essays and the blog's
       rail can use the same two classes. */

    .m-disclose {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        min-height: 40px;
        padding: 0 2px;
        margin: 4px 0 2px;
        border: 0;
        background: none;
        color: var(--brand-red, #e50914);
        font: inherit;
        font-size: 0.85rem;
        font-weight: 600;
        cursor: pointer;
    }

    .m-disclose::after {
        content: "▾";
        font-size: 0.7em;
        transition: transform 0.15s ease;
    }

    .m-disclose[aria-expanded="true"]::after {
        transform: rotate(180deg);
    }

    .m-fold[hidden] {
        display: none;
    }


    /* ── 7. ROWS AND CARDS ──────────────────────────────────────────────
       Owner: "those section buttons or epsiode cards are huge they wont all fit
       on one page with other information."

       240px cards on a 390px screen is one and a bit per row. 132px is three
       and a bit, which is what makes a row read as a row. Hover transforms are
       cut outright — there is no hover on a phone, and `:hover` sticking after
       a tap is what leaves one card enlarged and overlapping its neighbour. */

    .series-card,
    .episode-card {
        flex: 0 0 132px;
    }

    #library-rows>section.category-row .row-canvas img,
    #library-rows>section.category-row img {
        height: 132px;
    }

    .series-card:hover,
    .episode-card:hover {
        transform: none;
        left: auto;
        top: auto;
        z-index: auto;
    }

    .row-title {
        font-size: 1rem;
        margin-bottom: 8px;
    }

    .category-row {
        margin-bottom: 18px;
    }


    /* ── 7.1 THE PAGED PORTAL SCREENS ───────────────────────────────────
       Owner, 2026-08-19: "when the page loads welcome back and the text below
       it should be the view then scroll down to see recently added for now.
       Each scroll down should just flip to the next view snap with the main and
       library pages."

       The same paging the lander got in section 2.9, now behind the login: the
       hero is screen one, and every row after it is a screen of its own.

       🛑 `proximity`, never `mandatory` — the same ruling as the lander. A row
       is a horizontal scroller; mandatory would fight a thumb that flicks
       slightly off-axis, and any screen taller than the viewport becomes
       unreachable in the middle.

       🛑 Hooked on the ROW CONTAINERS, not a body class. `#home-rows` and
       `#library-rows` exist on exactly these two pages and nowhere else, so
       `:has()` names the two screens the owner named without touching the
       markup. The selector has to sit on <html> because scroll-snap-type does
       not propagate body → viewport; on <body> it silently does nothing.
       Browsers without `:has()` get today's page, unpaged. */
    html:has(#home-rows),
    html:has(#library-rows) {
        scroll-snap-type: y proximity;
        /* The portal's own fixed bar is `.m-appbar`, and it is 52px — the same
           number as the public navbar, which is why one variable covers both. */
        scroll-padding-top: var(--m-bar);
    }

    /* Screen one is the hero. It was a 172px band because the page under it was
       one long scroll; a paged screen means the picture gets the screen.

       🛑 THE HEADLINE SITS AT THE TOP OF IT, NOT IN THE MIDDLE. Owner,
       2026-08-19: "The movies has a logo in the center mid bottom or should
       anyways the other menus can match as far as text location."

       Centre, low, and clear of the join — which is precisely where film.css
       has put the film logo since 2026-08-17: `align-items: flex-end;
       justify-content: center; padding-bottom: calc(var(--film-fade-h) + 4rem)`
       on `.film-logo-layer`. The heading of a menu screen is the same object in
       the same frame, so it is placed by the same rule and its clearance is
       DERIVED from `--m-join` in the same way. Retune the join and the heading
       moves with it; nothing here has to be re-measured by hand.

       🛑 TWO ROUNDS OF WRONG ANSWERS, BOTH RECORDED SO THEY ARE NOT TRIED AGAIN.
       Round 8 put the heading at `center` — poster composition for what is only
       a page label, and dead centre of a stretched scrim as well. Round 10 moved
       it to `flex-start`, which the owner did not ask for and which fixed
       nothing: the black behind the text was never the hero's scrim at all, it
       was the LANDER's text-plate gradient leaking off a bare `.hero-content`
       selector (section 2). That leak is fixed at its source, the scrim is a
       bottom join again, and the heading is placed for its own sake.

       🔴 `top: auto !important` is not tidying. `Helpers.ScrollEffect.Hero`
       writes an inline `top: -Npx` for the sticky hero — 30% of its height,
       clamped to the empty band above `.hero-content`. That was ~0 on a 172px
       band whose content filled it; on a full-screen hero it computes to about
       184px, and on a `position: relative` element a negative top is not a
       sticky offset, it drags the hero up the page and tears a hole under it.
       Inline styles need `!important` to beat. */
    body:has(#home-rows) .hero-section,
    body:has(#library-rows) .hero-section {
        min-height: calc(100svh - var(--m-bar));
        align-items: flex-end;
        top: auto !important;
        scroll-snap-align: start;
        scroll-snap-stop: normal;
    }

    /* The clearance. `--m-join` is the depth of the transition and 1.75rem is
       the gap above it, so the lowest pixel of the sub-line still stops short of
       the point where the gradient has begun to darken at all. On a 615px hero
       that seats the heading block at about three quarters down — "center mid
       bottom" — and it holds at any screen height because both terms are the
       join's, not the text's. */
    body:has(#home-rows) .hero-section .hero-content,
    body:has(#library-rows) .hero-section .hero-content {
        padding-bottom: calc(var(--m-join) + 1.75rem);
    }

    /* "the other menus can match as far as text location" — so every `.main-page`
       hero centres its heading, the full-screen ones and the 147–210px bands
       alike. Scoped to `.main-page`, which is exactly the menu screens (home,
       the libraries, the arcade, search, profile). The directory, the film page
       and the five exercise heroes carry titles and metadata for one title
       rather than a section label, and they are left aligned exactly as they
       were found. */
    .hero-section.main-page .hero-content {
        text-align: center;
    }

    /* ⚠️ `.hero-meta` is `display: flex` (theme/series-listening/quizflix.css),
       so the `text-align` above does nothing to the badge row — it stayed hard
       left under a centred title. Flex items are centred with
       `justify-content`, and `wrap` because the profile hero's meta carries an
       e-mail address. */
    .hero-section.main-page .hero-meta {
        justify-content: center;
    }

    /* Every row is the next screen. Centred rather than top-aligned: the title
       and its posters are one block, and a block pinned to the top of a screen
       it does not fill leaves the empty half at the bottom where it reads as a
       page that failed to load. */
    #home-rows>section.category-row,
    #library-rows>section.category-row {
        min-height: calc(100svh - var(--m-bar));
        display: flex;
        flex-direction: column;
        justify-content: center;
        margin-top: 0;
        margin-bottom: 0;
        scroll-snap-align: start;
        scroll-snap-stop: normal;
    }

    /* The card grows into the screen it now owns — 132x240 in a 615px view is
       the "little band across the center" the owner rejected on the lander in
       round 3. It does NOT grow to one card a screen: two posters and a slice
       of the third is what tells a thumb the row scrolls sideways, and rule 1
       (dense, not big) still holds inside the screen. */
    #home-rows>section.category-row .series-card,
    #library-rows>section.category-row .series-card {
        flex: 0 0 calc(var(--m-poster) * 0.55);
    }

    /* Specificity has to match the base rule, which is an id plus five elements
       (theme/series-listening/quizflix.css) — a short selector here loses and
       the card grows around a 240px picture. ⚠️ The two halves of that base rule
       are NOT symmetrical: its `#home-rows` half also carries `.card-image`, so
       this one must too — without it home kept a 240px poster inside a 190px
       card while the library grew correctly. Measured, not assumed. */
    #home-rows>section.category-row>div.row-canvas>div.scroll-container>div.series-card>img.card-image,
    #library-rows>section.category-row>div.row-canvas>div.scroll-container>div.series-card>img {
        height: var(--m-poster);
    }

    /* The in-app hero. 30vh of photograph with a scrim that starts fully
       transparent at the top is the "text meant for a black background" the
       owner described — the heading sits on the bright part of the still. A
       phone gets a short band, and the picture STILL SHOWS THROUGH it.

       🔴 `position: static` IS WHY THE WHOLE SCREEN WAS GREY. Owner,
       2026-08-19: "the welcome back has no image behind it or its scaled wrong
       and only the black top is there… The title of the rows (recently added)
       needs to be brighter."

       Both of those are one bug and it is this line. The base rule is
       `position: sticky` (theme/series-listening/quizflix.css) and
       `.hero-section::after` — the scrim — is `position: absolute; inset: 0`.
       Turning the hero static took away the containing block that scrim was
       written against, so it resolved against the nearest positioned ancestor
       instead and painted **667px tall over the entire screen**: measured, a
       full-viewport rgba(20,20,20,0.72 → 0.85) sheet laid over the hero's own
       photograph, over "RECENTLY ADDED" and over every card under it. Nothing
       was dim; everything was behind a curtain. `position: relative` restores
       the containing block without bringing the sticky behaviour back — a hero
       that sticks to the top of a phone is what the static was reaching for. */
    .hero-section {
        position: relative;
        min-height: 0;
        padding: 0;
        /* ⚠️ `background-size: cover` and `background-position: center` used to
           be declared here as well, and both were DEAD the day they were
           written. Two rules beat them on specificity at every width:
           `.hero-section.main-page` (theme/series-listening/quizflix.css) for
           the banner screens and `.hero-section:not(.exercise-hero)`
           (quizflix.responsive.css §4.2) for everything else — (0,2,0) against
           this rule's (0,1,0), which source order does not touch. Removed
           rather than left as decoration; the series hero's framing is section
           7.4's, and it is an id selector for exactly this reason. */
    }

    /* And with the scrim lifted the picture is STILL nearly black, because the
       picture is a dim cinema interior and the band is only 172px of it. So
       the photograph
       itself is brightened, and it has to happen on a layer of its own: a
       `filter` on `.hero-section` would take the text with it.

       `background-image: inherit` gives the pseudo the parent's whole layer
       list — one file on home, banner-over-background on the library and arcade
       screens — so nothing has to be repeated here or kept in step. It paints
       over the parent's own copy, under `::after` (later pseudo, later paint)
       and under `.hero-content` (z-index 2).

       ⚠️ `brightness()` and not a `screen` blend, which was tried first: screen
       lifts the blacks with everything else and the band went milky grey by the
       time the seats were visible. brightness is multiplicative — black stays
       black and only what is lit comes up, which is the right shape for this
       picture. Measured over the band above the join: p90 luminance 23.6 → 45.0
       (the seats), mean 19.0 → 20.0 (the dark stays dark). Brightening a
       photograph should move the highlights, not the average.

       🛑 SCOPED TO THE SCREENS THAT ACTUALLY PAINT THE DARK PHOTOGRAPH, and
       measured on each of them rather than assumed. `.main-page` alone was
       wrong: it also covers the banner screens, and 3.2× blew out
       banners/arcade.jpg and banners/series.jpg — both are lit art that was
       never dark. Excluded by name. The directory and film heroes carry poster
       art and no `.main-page` at all, so they were never in scope.

       ⚠️ `search-hero` and `options-hero` ARE in scope, because their banner
       files do not exist yet and those two screens paint the shared background
       (see theme/series-listening/quizflix.css). 🛑 WHEN `banners/search.jpg`
       OR `banners/options.jpg` LANDS, ADD ITS CLASS TO THIS `:not()` CHAIN —
       the screen will otherwise show its new art at 3.2×. */
    .hero-section.main-page:not(.arcade-hero):not(.library-hero-series):not(.library-hero-movie)::before {
        content: "";
        position: absolute;
        inset: 0;
        background-image: inherit;
        background-size: cover;
        background-position: center;
        background-repeat: no-repeat;
        filter: brightness(3.2) contrast(1.08);
    }

    /* 🛑 THE SCRIM IS A JOIN AT THE BOTTOM EDGE — NOT A SHEET OVER THE PICTURE,
       AND NOT A PLATE UNDER THE TEXT. Owner, 2026-08-19: "The gradient is a
       transition not the thing that sits behind the text they should not be
       related… That gradient needs to be where it belongs not following text
       around, the text being visible is another issue."

       The base rule is `top: 0; bottom: 0` (theme/series-listening/quizflix.css)
       — one gradient stretched over the WHOLE hero. On a 172px band that read as
       a join because the band was barely taller than a join. Stretched over the
       full-screen hero of round 8 it became a 615px wash sitting on two thirds
       of the photograph, which is what put a grey lid over the artwork and made
       every attempt to place the heading a negotiation with a gradient.

       So it is unpinned from the top and given a depth of its own: `--m-join`
       deep, sitting on the hero's bottom edge, transparent where it starts and
       full `--bg-color` where the content begins. Above that line the picture is
       untouched at any hero height. The `min()` is for the short band heroes —
       the directory, search and profile screens are 147–210px, where a flat
       120px would be most of the band; there it takes 55% instead and still
       reads as an edge rather than a lid.

       The two things this used to do and must not do again:
         · legibility — that is `text-shadow` on the title and sub-line, below.
           It travels with the text over whatever pixel it lands on.
         · placement — the heading is held CLEAR of this band by a derived
           padding (section 7.1), the same way film.css holds the film logo
           clear of `--film-fade-h`. Neither one is measured from the other's
           content. */
    .hero-section::after {
        top: auto;
        height: min(var(--m-join), 55%);
        background: linear-gradient(to bottom,
                rgba(20, 20, 20, 0) 0%,
                rgba(20, 20, 20, 0.30) 40%,
                rgba(20, 20, 20, 0.72) 74%,
                var(--bg-color, #141414) 100%);
    }

    /* The legibility that the scrim used to buy. A tight shadow for the edge of
       each glyph and a wide soft one for the halo, so red-on-bright and
       grey-on-bright both hold. The sub-line's colour is set inline by
       `home.js` (`--text-secondary`), so lifting it off the picture needs
       `!important` — it is the only way past an inline style. */
    .hero-section .hero-content h1,
    .hero-section .hero-content .hero-title,
    .hero-section .hero-content p {
        text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9), 0 0 16px rgba(0, 0, 0, 0.7);
    }

    .hero-section .hero-content p {
        color: var(--text-color, #fff) !important;
    }

    /* 🛑 "welcome back should be 1 line not 2." Owner, same message — and the
       font size was never the reason. `.hero-content` carries
       `var(--container-padding)` = 32px each side, so on a 375px phone the
       headline was laid out in 279px of a 343px box and "WELCOME BACK" needs
       285px at this size. It wrapped for want of 6px that was padding. */
    .hero-section .hero-content {
        padding: 16px;
    }

    /* Specific enough to beat `.hero-section:not(.exercise-hero) .hero-title`
       in quizflix.responsive.css, which is what actually sets 2rem here — a
       bare `.hero-section h1` lost to it and the rule below it did nothing. */
    .hero-section h1,
    .hero-section h2,
    .hero-section:not(.exercise-hero) .hero-title {
        font-size: 1.5rem;
        line-height: 1.15;
    }

    /* 🛑 Bigger, now that it is a screen and not a band. Owner, 2026-08-19:
       "Welcome back should be larger now that it is a screen and apply a drop
       shadow to it so its easier to see."

       Only on the two paged screens (section 7.1) — the exercise heroes and the
       directory are still bands and 1.5rem is right for them. 1.95rem is the
       measured ceiling, not a taste: "WELCOME BACK" is 285px at 1.5rem inside
       343px of a 375px screen, so it holds one line up to about 1.75rem alone.
       With a name after it the greeting wraps regardless — the name is the
       second line by design, which is why this is allowed past that ceiling,
       and `text-wrap: balance` keeps the break between the greeting and the
       name rather than mid-phrase.

       The drop shadow is doing more work than decoration: it is what lets the
       scrim stay light enough for the photograph to read (§13). Two layers — a
       hard one for the edge of each glyph, a wide soft one to darken whatever
       the glyph is standing on. */
    body:has(#home-rows) .hero-section .hero-title,
    body:has(#library-rows) .hero-section .hero-title {
        font-size: 1.95rem;
        line-height: 1.1;
        text-wrap: balance;
        text-shadow: 0 2px 4px rgba(0, 0, 0, 0.95), 0 0 24px rgba(0, 0, 0, 0.8);
    }

    /* 🛑 MOVIES, SERIES AND GAMES ONLY. Owner, 2026-08-19: "For movies, series
       and games can you reduce the badge sizes by 25% and increase the title
       size by 50%, mobile only of course."

       Scoped by the three hero classes and NOT by `body:has(#library-rows)`,
       which would have been the shorter selector and the wrong one: that hook
       also catches the UNFILTERED library, whose heading is "QUIZFLIX LIBRARY"
       — sixteen characters that wrap to three lines at this size. The three
       named screens read MOVIES, SERIES and ARCADE, which is why they can take
       it. `.library-hero-movie` / `.library-hero-series` are put on by
       quizflix.render.logic.js from the ?type=; `.arcade-hero` is in the markup
       (portals/arcade.html).

       Both numbers are the owner's percentage applied to what was there, so
       the derivation is legible rather than a taste:
         title  1.95rem  x 1.5  = 2.925rem   (the rule above, §7.1)
         badge  0.8rem   x 0.75 = 0.6rem     (.hero-badge, series-listening/quizflix.css)
                24px     x 0.75 = 18px
                2px 5px  x 0.75 = 1.5px 3.75px

       ⚠️ The 1px border is deliberately NOT scaled. 0.75px is below the line
       most phones can draw and lands as either 1px or nothing depending on the
       device pixel ratio, so scaling it buys an inconsistent hairline rather
       than a smaller badge.

       ⚠️ THE `body:has(#library-rows)` PREFIX ON THE TITLE IS LOad-BEARING and is
       not the scoping — the three hero classes are. It is there for
       SPECIFICITY: the 1.95rem rule above is
       `body:has(#library-rows) .hero-section .hero-title`, and `:has()` takes
       the specificity of its argument, so that selector carries an ID. Three
       classes cannot beat it. Measured before the prefix went on: all three
       screens still computed 31.2px. */
    body:has(#library-rows) .hero-section.arcade-hero .hero-title,
    body:has(#library-rows) .hero-section.library-hero-movie .hero-title,
    body:has(#library-rows) .hero-section.library-hero-series .hero-title {
        font-size: 2.925rem;
    }

    .hero-section.arcade-hero .hero-badge,
    .hero-section.library-hero-movie .hero-badge,
    .hero-section.library-hero-series .hero-badge {
        font-size: 0.6rem;
        line-height: 18px;
        padding: 1.5px 3.75px;
    }

    /* 🔴 THE DIRECTORY'S SIDEWAYS SCROLL, measured at 540px of document in a
       390px viewport.

       Two causes, and the second is the one that matters: the 276px trailer box
       carries 16px margins and 16px padding, which is 400px of box on its own;
       and `#inner-block-trailers` is a `display: flex` row holding that box
       BESIDE a 60%-wide #seriesOverview. Sizing the overview to 100% does
       nothing while they are still on one line, so the row is the fix. */
    #inner-block-trailers {
        flex-direction: column;
        justify-content: flex-start;
    }

    #seasonInfoContainer {
        width: 100%;
        box-sizing: border-box;
        padding-left: 12px;
        padding-right: 12px;
        margin-top: 20px;
    }

    .trailer-video-container {
        width: auto;
        margin: 8px 12px 12px;
        padding: 10px;
        height: auto;
        box-sizing: border-box;
    }

    #trailer-video {
        width: 100%;
        height: auto;
        aspect-ratio: 16 / 9;
        background-size: cover;
    }

    #seriesOverview {
        width: auto;
        min-height: 0;
        margin: 14px 12px;
    }

    #seriesOverview p {
        max-height: none;
        overflow: visible;
        font-size: 0.9rem;
        line-height: 1.5;
    }

    #trailer-play-button-container {
        width: 100%;
        height: auto;
        min-height: 40px;
        flex-wrap: wrap;
        gap: 8px;
    }

    /* A 4rem gap and 3rem of padding puts the third tab off the right edge. */
    .tab-strip {
        gap: 1.25rem;
        padding: 0 1rem;
    }

    .tab-strip button,
    .tab-strip a {
        min-height: 44px;
        font-size: 0.85rem;
        white-space: nowrap;
    }

    /* Search: four 180px fields in a 358px row wrap to one per line with half
       the row empty. Two up, and the dates share a line. */
    .search-panel {
        padding: 0 12px 12px;
    }

    .search-fields {
        gap: 8px;
    }

    .search-field {
        flex: 1 1 calc(50% - 4px);
        min-width: 0;
    }

    .search-field:first-child {
        flex-basis: 100%;
    }

    .search-field input,
    .search-field input[type="date"] {
        min-width: 0;
        width: 100%;
        min-height: 44px;
        box-sizing: border-box;
    }

    .search-field label {
        font-size: 0.75rem;
    }

    /* Genre chips measured 29px tall. They sit in a wrapping cloud where the
       rows are 8px apart, which is the exact shape Google's tap-target audit
       reports as "targets too close". */
    .genre-chip,
    .tag-cloud .genre-chip {
        min-height: 40px;
        display: inline-flex;
        align-items: center;
        font-size: 0.8rem;
    }


    /* ── 7.2 THE FILM PAGE'S FULL-SCREEN HEADER ─────────────────────────
       🔴 THE FADE WAS ONE APP BAR CLEAR OF THE CONTENT IT JOINS. Owner,
       2026-08-19: "in the movie section on mobile when scrolling down the black
       line that creates the fade effect is separated from the content container
       creating a gap with a black gradient above it."

       Measured on an SE: `.film-topfade` ended at 667 and `.film-content` began
       at 719. The 52 between them is `.m-appbar`. Section 4 pays the fixed bar
       back with `body.m-has-appbar .main-content { padding-top: 52px }`, so
       `.film-header-space` — a normal-flow spacer — begins BELOW that padding,
       while `.film-topfade` is absolutely positioned and measured from the
       padding BOX, which begins above it. The fade therefore stopped exactly one
       bar short of the seam it exists to dissolve, and the strip in between was
       raw header image ending in the hard black line, with a gradient hanging
       over nothing above it. Every film page, at every scroll position.

       🛑 The band is ONE SCREEN, `100svh - var(--m-bar)`, not `100svh`. film.css
       deliberately keeps the header at a full screen on a phone; a full screen
       under a fixed 52px bar is 100svh minus the bar, the same subtraction every
       other full view in this file makes. At 100svh the seam started below the
       fold, so the join could not be seen even once it was in the right place.

       Both numbers are set through the TOKEN and derived from it, never typed:
       `--film-fade-h` drives the gradient's own offset AND the logo's clearance
       off the seam (film.css), so overriding a height here would strand one of
       them. */
    .film-page {
        --film-header-h: calc(100svh - var(--m-bar));
    }

    /* The three fixed layers start under the bar, not under it. They are pinned
       to the viewport, so nothing in the flow moves them. */
    .film-page .film-bg,
    .film-page .film-scrim,
    .film-page .film-logo-layer {
        top: var(--m-bar);
    }

    /* ...and the fade adds back the padding its containing block ignores. This
       one line is the defect. */
    .film-page .film-topfade {
        top: calc(var(--m-bar) + var(--film-header-h) - var(--film-fade-h));
    }


    /* ── 7.3 THE FILM PAGE IS SIX SCREENS ───────────────────────────────
       Owner, 2026-08-20 (§2-2b, VIEW SNAPS): "The view when opening a movie
       needs to snap into place on each spot to make the experience easier.
       These are how the sections should work down the page.

         1) Background with logo
         2) Trailer and rating
         3) Corpus (should fill the view and scroll inside the view if needed)
         4) Character carousel
         5) Watch Settings displayed with the square profile image on the left
            with the 2 controls Watched and Not Interested to its right. Below
            those two columns full width will be Prewatch Guide, Character Guide
            and Vocabulary Review options
         6) Study parts as a carousel if there are 2 or more parts."

       Six screens, in that order, and each one is `100svh - var(--m-bar)` like
       every other full view in this file.

       🛑 `proximity`, never `mandatory` — the same ruling the lander and the
       portal screens got in 2.9 and 7.1. Screen 3 scrolls INSIDE itself and
       screen 6 scrolls SIDEWAYS; mandatory fights both, and a section that ends
       up taller than the viewport becomes unreachable in the middle.

       🛑 Hooked on `#filmContent` — the film page's own container id, present on
       exactly this page and nowhere else, which is the `#home-rows` idiom from
       7.1. The selector has to sit on <html>: `scroll-snap-type` does not
       propagate body → viewport, and on <body> it silently does nothing.

       🔴 THE SNAP SECTIONS ARE NOT ALL SIBLINGS, and they do not need to be.
       Screens 2 and 3 are the two halves of ONE grid (.film-block); the rest are
       sections of .film-content. A snap area is any descendant of the scroll
       container, so the grid's own children carry their own snap points and the
       block simply stops being a block on a phone. */
    html:has(#filmContent) {
        scroll-snap-type: y proximity;
        scroll-padding-top: var(--m-bar);
    }

    /* 🔴 `clip`, NOT `hidden`, AND THIS ONE LINE IS WHETHER THE PAGE SNAPS AT
       ALL. film.css carries `overflow-x: hidden` on .film-main to stop the
       header layers pushing the page sideways. `overflow-x: hidden` with
       `overflow-y: visible` does not stay visible: the used value blockifies to
       `auto`, so .film-main is a SCROLL CONTAINER — it never scrolls, because
       nothing constrains its height, but it is one.

       And a snap area belongs to its NEAREST ancestor scroll container. Every
       screen below is inside .film-main, so all six were snapping to a box with
       `scroll-snap-type: none` while the viewport, which is what actually
       scrolls, had no snap areas at all. The geometry was perfect and a wheel
       gesture landed wherever it stopped.

       `overflow-x: clip` clips exactly the same and does NOT create a scroll
       container, so the areas belong to the viewport again. The page's own
       horizontal guard is unchanged, and `body { overflow-x: hidden }` in
       section 0 is still behind it.

       ⚠️ This is worth knowing off this page: any full-view snap layout inside a
       container that carries `overflow-x: hidden` for a horizontal guard has the
       same defect, and it presents as "scroll-snap does nothing" with every
       computed value looking right. */
    .film-page .film-main {
        overflow-x: clip;
    }

    /* Screen 1 — the artwork and the logo. The band is already exactly one
       screen (7.2 sets `--film-header-h`), and this is its spacer in the flow,
       so all it needs is the snap point. Its start edge is the top of the page. */
    .film-page .film-header-space {
        scroll-snap-align: start;
        scroll-snap-stop: normal;
    }

    /* The block that used to hold screens 2 and 3 together. Its gap and its
       3rem of lead-in are the space BETWEEN two screens now, which is a strip of
       black at the bottom of the trailer's view and nothing else. */
    .film-page .film-block {
        gap: 0;
        padding-top: 0;
    }

    /* Screens 2, 4, 5 and 6 — same treatment, one rule. Centred rather than
       top-aligned, for the reason 7.1 gives: a block pinned to the top of a
       screen it does not fill leaves the empty half at the bottom, where it
       reads as a page that failed to load.

       `margin: 0` because .film-section carries 3.5rem above itself for the
       long-scroll layout. Between two snapped screens that margin is invisible
       black that the snap then has to skip, and it pushes each section's start
       edge 3.5rem below the point the screen begins. */
    .film-page .film-trailer,
    .film-page #filmCharacterSection,
    .film-page #filmDeck,
    .film-page #filmPartsSection,
    .film-page #filmSuggestionsSection {
        min-height: calc(100svh - var(--m-bar));
        display: flex;
        flex-direction: column;
        justify-content: center;
        margin: 0;
        scroll-snap-align: start;
        scroll-snap-stop: normal;
    }


    /* The trailer is the whole subject of screen 2, so it takes the whole width
       of it. A 16:9 frame inside the content gutters is 197px of picture in a
       792px view; edge to edge it is 219, and more to the point it stops reading
       as a thumbnail with a button under it.

       The height cannot be bought back — a 16:9 box on a 390px screen is 219px
       whatever is done to it, and the rest of the view is black. That is the
       cost of "trailer and rating" being one view on a tall phone, and it is the
       owner's arrangement; the answer is not to fill the screen with something
       that belongs on another one.

       `calc(50% - 50vw)` rather than a negative gutter, because .film-content's
       padding is 5% above 520px and 1.2rem below it — this cancels either
       without knowing which. `body { overflow-x: hidden }` in section 0 is the
       backstop if a scrollbar ever makes 100vw wider than the page. */
    .film-page .film-trailer-frame {
        width: 100vw;
        margin-left: calc(50% - 50vw);
        border-radius: 0;
    }

    /* Screen 3 — the corpus. "should fill the view and scroll inside the view if
       needed", which is the shape it has on a desktop and lost at 820px, where
       the columns stack and both halves of the absolute trick come off.

       It comes back here, and NOT by restoring the absolute scroller: that one
       is height-matched to the trailer column beside it, and on a phone there is
       no column beside it. The screen is the height instead. The scroller keeps
       its own place in the flow and takes a ceiling, so a short summary sits
       centred in its screen and a long one scrolls where it is.

       🛑 NO `overscroll-behavior: contain`. It is the obvious thing to add to a
       scroller inside a paged view and it would be a trap here: on a box whose
       content fits, the containment still applies, so a thumb dragged on a short
       summary would move neither the corpus nor the page. Chaining stays on —
       the corpus scrolls to its end, then the page carries on to screen 4. */
    .film-page .film-corpus {
        min-height: calc(100svh - var(--m-bar));
        display: flex;
        flex-direction: column;
        justify-content: center;
        scroll-snap-align: start;
        scroll-snap-stop: normal;
    }

    .film-page .film-corpus-scroll {
        max-height: calc(100svh - var(--m-bar) - 3rem);
        overflow-x: hidden;
        overflow-y: auto;
        padding-right: 0.75rem;
        -webkit-overflow-scrolling: touch;
    }

    /* The house bar is 20px wide (film.css). That is a pointer's target and a
       fifth of the gutter on a 390px screen — the rail is a hint here, and the
       thumb is the only part of it anyone can see. */
    .film-page .film-corpus-scroll::-webkit-scrollbar {
        width: 6px;
    }

    .film-page .film-corpus-scroll::-webkit-scrollbar-button,
    .film-page .film-corpus-scroll::-webkit-scrollbar-thumb {
        border: 0;
    }

    /* Screen 4 — the character carousel. It is already a sideways scroller
       (film.css §5.2); on its own screen it gets snap points, so a thumb lands
       on a card rather than between two. */
    .film-page .character-strip {
        scroll-snap-type: x proximity;
        -webkit-overflow-scrolling: touch;
    }

    .film-page .character-card {
        scroll-snap-align: start;
    }

    /* Screen 5 — Watch Settings. The owner's arrangement, and it is a different
       one from the three even columns the desktop has:

           Watch Settings
           [ profile ]  Watched
                        Not interested
           Pre-Watch Guide / Character Guide / Vocabulary Overview

       Two columns for the square and the two checkboxes, then the three
       materials full width beneath both of them.

       At 820px this grid collapses to ONE column and stacks all three panels;
       that is what is being overridden. The mobile sheet loads last, so equal
       specificity is enough — but both `grid-template-columns` and `gap` have to
       be restated, not just the tracks, because the 820px rule sets a `gap`
       shorthand and a leftover 1.5rem column gap would push the checkboxes off a
       390px screen.

       🔴 ONE EXPLICIT PLACEMENT, AND ONLY ONE. The materials panel is second in
       the DOM (profile, materials, feedback — the desktop order, which is the
       owner's ruling of 2026-08-17: "put the mark as watched as the last
       column"). Pinning it to row 3 is enough: grid places definite-position
       items first, so the feedback panel then auto-places into the one cell left
       beside the square. Do not add a second `grid-row` here to "match" it — the
       two would then have to be kept in step by hand.

       ⚠️ The cost is that the tab order on a phone reaches the three material
       buttons before the two checkboxes, which is not the order they are drawn
       in. The two rulings genuinely disagree — materials sit BEFORE feedback on
       a desktop and AFTER it here — so one of the two layouts has to be built
       against the source order, and it is this one. Worth revisiting if these
       controls are ever wired to anything; they are inert today. */
    .film-page .film-deck-panels {
        grid-template-columns: minmax(0, 9.5rem) minmax(0, 1fr);
        justify-items: start;
        column-gap: 1.25rem;
        row-gap: 1.5rem;
        align-items: start;
    }

    .film-page .film-deck-prewatch {
        grid-column: 1 / -1;
        grid-row: 3;
        /* "Below those two columns full width" — the AREA is full width. The
           three rows inside it stay left-aligned and stacked, which is what they
           are at every other width; laid out across a 390px screen instead, all
           three labels wrap to two lines. `justify-self` because the grid's
           `justify-items: start` would otherwise shrink this item to the width of
           its longest label and the span would be nominal. */
        justify-self: stretch;
    }

    /* The square is the column now, so it takes the track rather than its own
       12rem ceiling. */
    .film-page .film-deck-profile {
        max-width: none;
    }

    /* Screen 6 — "Study parts as a carousel if there are 2 or more parts."

       On a desktop the row divides into one column per part up to four and only
       becomes a scroller at five (film.css §5.3); at 820px every count under five
       collapses to a single stacked column. The owner's line moves the scroll
       point on a phone from five parts to TWO — four cards on a 390px screen are
       four thumbnails 80px wide, and a stack of four is four screens of scrolling
       inside a screen that is meant to be one.

       One part is still one card. A carousel of one is a card that scrolls
       nowhere, and the owner's condition is explicit about the count. */
    .film-page .film-parts-row[data-parts="2"],
    .film-page .film-parts-row[data-parts="3"],
    .film-page .film-parts-row[data-parts="4"],
    .film-page .film-parts-row[data-parts="many"] {
        display: flex;
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        scroll-snap-type: x proximity;
        -webkit-overflow-scrolling: touch;
    }

    /* 78% of the row, so the next card is always half-visible at the edge —
       the same reasoning as the poster rows in 7.1: a slice of the next one is
       what tells a thumb the row moves sideways. It overrides the 260px basis
       the `many` variant carries at every width, which is narrower than this
       screen and would leave a card and two thirds. */
    .film-page .film-parts-row[data-parts="2"] .film-part-card,
    .film-page .film-parts-row[data-parts="3"] .film-part-card,
    .film-page .film-parts-row[data-parts="4"] .film-part-card,
    .film-page .film-parts-row[data-parts="many"] .film-part-card {
        flex: 0 0 78%;
        width: auto;
        max-width: none;
        scroll-snap-align: start;
    }



    /* ── 7.4 THE SERIES PAGE IS A SCREEN PER SECTION ────────────────────
       portals/directory.html — the page a library card opens. Measured at
       390x844 before any of this: 2.97 continuous screens, an episode row using
       281 of 390px, a 132x424 card tower, 858px of unbroken prose and a hero
       that read as a black band.

       The screens, in DOM order:

         1) the banner, the title block and the trailer still
         2) the overview, scrolling inside its own screen
         3..n) one screen per season

       🛑 One screen PER SEASON and not one for "the seasons". A series here
       runs to eight of them; a single screen holding all eight rows is the
       long-scroll page this section exists to replace.

       🛑 Hooked on `#seasonsContainer` — the directory's own container id,
       which no other page in the tree carries (grep: directory.html,
       quizflix.render.logic.js, series.listening.series.js). Same idiom as
       `#home-rows` in 7.1 and `#filmContent` in 7.3, and for the same reason it
       has to sit on <html>: `scroll-snap-type` does not propagate body →
       viewport and on <body> it silently does nothing.

       ⚠️ The 7.3 trap was checked for and is not present here: nothing between
       an episode card and the viewport carries `overflow-x: hidden`
       (`.seasons-container`, `.main-content` and `.app-container` are all
       `visible`), so the snap areas belong to the viewport. `body`'s guard in
       section 0 propagates to the viewport rather than making body a scroll
       container, which is why the film page only had to fix `.film-main`. */
    html:has(#seasonsContainer) {
        scroll-snap-type: y proximity;
        scroll-padding-top: var(--m-bar);
    }


    /* ── SCREEN 1 ───────────────────────────────────────────────────────
       The hero band, then the trailer still under it, together in one view.

       🛑 The hero is NOT stretched to a full screen the way the library and
       arcade heroes are in 7.1, and the reason is the art. Those screens paint
       a 3:1 banner authored for the job. A series paints `banners/<slug>.jpg`,
       and all 45 of those are 1000x185 — a title strip 72px tall on a 390px
       screen. Stretched to 792px it is 9% picture and 91% black, which is a
       worse screen than the band. The band plus the trailer's picture is one
       full view of real content instead, which is the "denser, not bigger"
       brief rather than an exception to it.

       🔴 AND THE STRIP IS PAINTED AT ITS OWN RATIO, WHICH IS THE THIRD ANSWER
       TO A QUESTION THAT HAS HAD TWO WRONG ONES. The base sheet frames this
       hero `contain` + `left top`; the owner reported that as "looks like it's
       broken" — a strip in the top-left corner of a box that is otherwise
       unpainted — and quizflix.responsive.css §4.2 answered it with
       `cover` + `center` below 768px. But `cover` on a 5.41:1 strip in a
       1.49:1 box scales it up 3.6x and throws 73% of it away, which is the
       smear of dark pixels that reads as "the hero is a black band".

       Neither is a framing problem. A 1000x185 title strip has no honest
       rendering inside a 1.49:1 box, so the box stops being 1.49:1: `100% auto`
       paints it edge to edge at its native ratio, and `padding-top` reserves
       exactly that band so the title sits below the artwork instead of on it.
       Both terms come from the ratio the files actually have — measured across
       all 45 of them — rather than being typed, so this holds at any width
       below 767px and on any series.

       (0,1,0) is not enough to reach it: this has to beat
       `.hero-section:not(.exercise-hero)` at (0,2,0), hence the id. */
    html:has(#seasonsContainer) #heroSection {
        background-size: 100% auto;
        background-position: center top;
        background-repeat: no-repeat;
        padding-top: calc(100vw * 0.185 + 10px);
        scroll-snap-align: start;
        scroll-snap-stop: normal;
        /* 🔴 Not tidying — the same inline `top: -Npx` that 7.1 documents.
           `Helpers.ScrollEffect.Hero` runs on this page (render.logic.js, the
           `case "series"` branch) and writes a negative sticky offset that, on
           a `position: relative` hero, drags the band up and tears a hole under
           it. Inline styles need `!important` to beat. */
        top: auto !important;
    }

    /* The trailer still and the WATCH TRAILER button belong to screen 1; the
       overview is screen 2. In the DOM they are not siblings —
       `#inner-block-trailers` wraps the still and the overview together while
       the button is appended after that wrapper (render.logic.js), so source
       order is still / overview / button.

       `display: contents` drops the wrapper out of the box tree, which makes
       all three direct flex items of `#seasonInfoContainer` and lets `order`
       put the button back where it belongs. It does not undo the fix above it
       in this file: that one exists to stop the still and the overview sitting
       on ONE LINE, and as column flex items they still do not. */
    html:has(#seasonsContainer) #seasonInfoContainer {
        display: flex;
        flex-direction: column;
        margin-top: 0;
        gap: 0;
    }

    html:has(#seasonsContainer) #inner-block-trailers {
        display: contents;
    }

    /* 🔴 THE STILL IS WHAT SHRINKS WHEN SCREEN 1 IS SHORT, and it has to be
       measured rather than guessed. Swept across all 34 series at both phone
       sizes: at 390x844 every one of them fitted at the still's natural 3:2,
       and at 375x667 the ten series that HAVE a trailer to play came to 620px
       of content in a 615px screen — leaving the WATCH TRAILER button 5-7px
       under the fold. A control cut off by the viewport edge, which is what
       round 1's modal defect was.

       So the still takes the screen minus the fixed part of it:
       `--m-series-head` is the hero plus the button row, published by
       quizflix.mobile.js because a "Starring:" line runs from one row to six
       and no typed number covers both phones and every cast. The 10px is this
       box's own margin, above. `background-size: contain` on the picture inside
       means shrinking the frame letterboxes the art rather than cropping it —
       nothing is lost when the cap bites. */
    html:has(#seasonsContainer) .trailer-video-container {
        order: -2;
        margin: 10px 0 0;
        max-height: calc(100svh - var(--m-bar) - var(--m-series-head, 344px) - 10px);
        box-sizing: border-box;
        display: flex;
        flex-direction: column;
        min-height: 0;
    }

    /* ⚠️ A `max-height` on the frame alone does NOT shrink the picture — the
       still is a fixed-ratio box and would simply overflow the frame it was
       given. It has to be a flex item that is allowed to lose its ratio:
       `flex: 1 1 auto` takes the 3:2 content height as its basis, and
       `min-height: 0` is what lets shrinking past that basis actually happen
       (a flex item's automatic minimum size would otherwise hold it at 3:2 and
       overflow anyway — the same rule that makes `min-width: 0` necessary on a
       card that must ellipsis). */
    html:has(#seasonsContainer) .trailer-video-container #trailer-video {
        flex: 1 1 auto;
        min-height: 0;
        height: auto;
        width: 100%;
    }

    html:has(#seasonsContainer) #trailer-play-button-container {
        order: -1;
        width: auto;
        height: auto;
        padding: 10px 0 0;
        justify-content: flex-start;
    }

    /* The still is `background-image` on a fixed box, so its ratio has to match
       the art or the picture is cropped by the frame. `trailer-covers/` is 3:2
       (720x480, and default.jpg is 1080x720); the 16/9 this file asked for
       before cut 11% off a 3:2 cover and 25% off a 4:3 one. `contain` because
       the pool is not uniform — a letterboxed cover shows grey inside a grey
       frame, a cropped one loses a face. */
    html:has(#seasonsContainer) #trailer-video {
        aspect-ratio: 3 / 2;
        background-size: contain;
        background-position: center;
    }


    /* ── SCREEN 2 — THE OVERVIEW ────────────────────────────────────────
       858px of prose measured on `the-agency`, which is two and a half screens
       of one paragraph. The desktop caps it at 175px and scrolls it; the phone
       rule above this one lifted the cap outright (`max-height: none`) back
       when the page was a single long scroll, and on a paged page that is what
       makes screen 1 and screen 3 unreachable in one gesture.

       The film page's ruling applies here unchanged (7.3, screen 3): the
       corpus fills its view and scrolls INSIDE the view. The ceiling comes from
       the SCREEN, not from the column beside it — on a phone there is no column
       beside it.

       🛑 NO `overscroll-behavior: contain`. On a box whose content already fits
       the containment still applies, so a thumb dragged on a short summary
       would move neither the summary nor the page. Chaining stays on: the
       overview scrolls to its end and then the page carries on to season 1.

       `margin-top` is what makes screen 1 exactly one screen. `--m-series-lead`
       is the measured height of the hero + still + button group, published by
       quizflix.mobile.js; the fallback is the value that group comes to on a
       390x844 render, so a missing script layer costs a slightly leaky first
       screen and nothing else. `max()` so a tall hero (a long "Starring:" line)
       never pulls the overview upward into it. */
    html:has(#seasonsContainer) #seriesOverview {
        order: 0;
        width: auto;
        min-height: calc(100svh - var(--m-bar));
        margin: 0 12px;
        margin-top: max(0px, calc(100svh - var(--m-bar) - var(--m-series-lead, 560px)));
        display: flex;
        flex-direction: column;
        justify-content: center;
        scroll-snap-align: start;
        scroll-snap-stop: normal;
    }

    html:has(#seasonsContainer) #seriesOverview p {
        max-height: calc(100svh - var(--m-bar) - 8rem);
        overflow-x: hidden;
        overflow-y: auto;
        padding-right: 0.75rem;
        -webkit-overflow-scrolling: touch;
    }

    /* The house scrollbar is 20px. That is a fifth of the gutter here — the
       rail is a hint on a phone and the thumb is all of it anyone sees. Same
       treatment `.film-corpus-scroll` gets in 7.3. */
    html:has(#seasonsContainer) #seriesOverview p::-webkit-scrollbar {
        width: 6px;
    }

    html:has(#seasonsContainer) #seriesOverview p::-webkit-scrollbar-button,
    html:has(#seasonsContainer) #seriesOverview p::-webkit-scrollbar-thumb {
        border: 0;
    }


    /* ── SCREENS 3..n — ONE PER SEASON ──────────────────────────────────
       🔴 THE ROW WAS USING 281 OF 390px, AND IT IS TWO RULES DEEP.
       `.category-row` is `width: 96%` PLUS `padding-left: var(--container-
       padding)` (32px) — a percentage width and a fixed padding on the same
       box, which is 313px of row and then 281px of canvas inside it. The first
       episode card started 71px in from the left edge of the screen. That
       gutter is the desktop's arrow lane; there are no arrows on a phone, so it
       is 109px of a 390px screen spent on nothing.

       Centred rather than pinned to the top, for the reason 7.1 gives: a row
       that does not fill the screen it owns leaves the empty half at the
       bottom, where it reads as a page that failed to load. */
    html:has(#seasonsContainer) .seasons-container {
        padding: 0;
    }

    html:has(#seasonsContainer) #seasonsContainer {
        padding-top: 0;
    }

    html:has(#seasonsContainer) #seasonsContainer>.category-row {
        width: auto;
        padding-left: 0;
        margin: 0 12px;
        min-height: calc(100svh - var(--m-bar));
        display: flex;
        flex-direction: column;
        justify-content: center;
        scroll-snap-align: start;
        scroll-snap-stop: normal;
    }

    /* The row is already a sideways scroller; on its own screen it gets snap
       points so a thumb lands on a card and not between two. Same as the
       character strip in 7.3. */
    html:has(#seasonsContainer) #seasonsContainer .row-canvas {
        scroll-snap-type: x proximity;
        -webkit-overflow-scrolling: touch;
    }

    html:has(#seasonsContainer) #seasonsContainer .scroll-container {
        gap: 12px;
    }

    /* 🔴 THE CARD WAS 132 WIDE AND 424 TALL — a tower three times its own
       width, and every part of it was a separate mistake:

         - `#seasonsContainer .scroll-container .episode-card` carries
           `min-height: 350px` in the base sheet, sized for a 240px card.
         - `.episode-thumb` is `height: 180px` AND `aspect-ratio: 4/3`. A
           declared height plus a constrained width wins over the ratio, so a
           4:3 still (measured: 1080x720 for the fallback, 720x480 for a real
           cover) was rendered into a 132x180 PORTRAIT box — the art turned on
           its side and centre-cropped.
         - `.episode-info` has 1rem of padding and the summary is cut at 80
           characters in JS, which is six lines at 132px.

       158px and 12px of gap is two cards and a slice of the third in a 366px
       row — the same "a slice tells a thumb it scrolls" reasoning as the
       posters in 7.1, and the card comes back to about 230px tall.

       These four selectors each match the base sheet's specificity exactly
       rather than adding an id or an `!important`: this file loads last in all
       nineteen heads, so a tie is a win, and matching keeps the override
       readable next to the rule it is beating. */
    html:has(#seasonsContainer) #seasonsContainer .scroll-container .episode-card {
        flex: 0 0 158px;
        min-height: 0;
        scroll-snap-align: start;
    }

    html:has(#seasonsContainer) #seasonsContainer .episode-card .episode-thumb {
        width: 100%;
        height: auto;
        aspect-ratio: 4 / 3;
        padding: 0;
    }

    html:has(#seasonsContainer) #seasonsContainer .episode-card .episode-info {
        padding: 8px 10px 6px;
    }

    /* Three lines, not two. The summary is already cut at 80 characters in JS
       (quizflix.logic.helpers.js) and 80 characters is 3.3 lines at this width,
       so two would be an ellipsis on top of an ellipsis — hiding text that the
       card has been given and the screen has room for. Three shows nearly all
       of it and the card still comes to about 260px. */
    html:has(#seasonsContainer) #seasonsContainer .episode-card .episode-desc {
        display: -webkit-box;
        -webkit-line-clamp: 3;
        -webkit-box-orient: vertical;
        overflow: hidden;
        font-size: 0.75rem;
        line-height: 1.35;
    }

    /* 🛑 THERE IS NO START LESSON BUTTON ON A PHONE. Owner, §2-3a item 3:
       "Remove start lesson there should not be anything like lessons used in
       the language."

       Round 12 restyled it instead — it is `float: right` at every other width,
       which inside a 158px card puts a 93px control against the right edge with
       the description wrapped round it, so it was refloated into the card's last
       row. That was the wrong fix for a control the same line asked to be gone,
       and the ruling is not only about the layout: it is about the WORD. The
       card is the way in on a phone and it opens the episode modal, which is
       where every route into the episode already lives.

       Deleted here rather than in `quizflix.logic.helpers.js` because §2-3 is
       headed "These changes are for mobile only, specifically phones" — the
       button and its progress mark are still the desktop card's, untouched. */
    html:has(#seasonsContainer) #seasonsContainer .scroll-container .episode-card .start-btn {
        display: none;
    }


    /* ── 8. THE REST OF THE MEASURED TAP TARGETS ────────────────────────
       Everything below is a real element that came back under 40px on a
       390x844 render. Each keeps its type size and grows its box. */

    .blog-rail .rail-link {
        min-height: 40px;
        display: flex;
        align-items: center;
        font-size: 0.85rem;
    }

    .rail-subheading {
        font-size: 0.75rem;
    }

    /* 🛑 A VOCABULARY WORD GETS NO TAP-TARGET TREATMENT AT ALL. Owner,
       2026-08-19: "they are supposed to be inline with the rest of the article
       words they are vocabulary words not some sort of normal control."

       The `padding: 6px 2px` that stood here was the tap-target rule applied
       where it does not belong. It bought nothing — padding on an inline box
       does not grow the line box, so the hit area barely changed — and it cost
       two things a reader can see: 2px of air on each side, so the word sat in
       its own little gap inside the sentence, and 6px underneath, which pushed
       the dashed underline away from the baseline until it read as a control's
       border rather than a gloss.

       Google's tap-target audit exempts links inside a block of text for exactly
       this reason: a word in a paragraph is spaced by the paragraph. The gold
       italic and the dashed rule (blog.css) are the whole affordance, and they
       are the same at every width. */

    .start-btn,
    .btn-quiet {
        min-height: 40px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }


    /* ── 5. ROUND 5 ─────────────────────────────────────────────────────
       Owner's fourth pass, 2026-08-19. Five items, and the two that are pure
       CSS live here; the rest are in landing.css, blog.css, site-chrome.js,
       feature.quizflix.js and feature.costars.js with their own notes.


       5.1 EVERY BUTTON, EVERY PAGE, ON A PHONE.

       "I really dont like the buttons at all anywhere the red rounded facebook
       childish same as every template god awful over sized buttons… you have
       used buttons like they are content written massive text blocks in them
       huge text just god awful design there."

       landing.css carries the same treatment for the five public pages at every
       width. This is the phone version of it and it reaches ALL nineteen pages
       that load this file, which is the "anywhere" half of the ruling — the
       portals included.

       🛑 It is rule 1 of the brief, not a contradiction of it. The 44px stays,
       because 44px is the AUDIT'S HIT AREA. What comes down is the ink: 0.75rem
       type, 16px of side padding, a 2px corner and no fill beyond the words. A
       bigger tap target and a smaller button are the same change here.

       ⚠️ `.submit-btn` and anything carrying an inline `width` are left alone —
       a form's submit is meant to be the width of the fields above it, and the
       alpha gate's "Next" sets its own. */
    .btn,
    .btn-lg,
    .btn-sm,
    button.btn,
    #content-modal-button {
        min-height: 44px;
        padding: 0 16px;
        font-size: 0.75rem;
        font-weight: 600;
        letter-spacing: 0.05em;
        line-height: 1.15;
        border-radius: 2px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 7px;
        /* Shrink-wrap the words. A button that fills its column is the "massive
           text block" — it is what turned "Get Free Access..." into two lines of
           20px type inside a 280x90 red panel. */
        width: auto;
        align-self: flex-start;
        max-width: 100%;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        box-shadow: none;
    }

    .btn:hover,
    .btn-lg:hover,
    .btn-sm:hover,
    button.btn:hover {
        transform: none;
        box-shadow: none;
        text-shadow: none;
    }

    /* The icon is a hint at this size, not a second subject. */
    .btn i,
    #content-modal-button i {
        font-size: 0.85em;
    }


    /* 5.2 THE FOOTER IS THE LOGO AND THE WAY BACK.

       "The footer itself has no valuable information on it so it doesnt need to
       show much aside from the logo and back to top since the page is a mile
       long."

       Four of the seven links go nowhere (§5 item 5 of the 1.0.6 list) and the
       three that do are reachable from the page above. So the two link columns
       are hidden and what is left is the wordmark, the one line under it, the
       copyright and a control that answers the actual complaint — the page is
       long, and this is the way to the top of it.

       HIDDEN, NOT DELETED. One footer definition serves five pages at every
       width (site-chrome.js); cutting the markup on a phone would put the
       desktop footer back to being maintained on its own, which is the drift
       that file exists to end. `display: none` on two columns is the whole cut.

       `#top` is an id on <body> on all five public pages, so Back to top is an
       anchor and works with the script blocked. */
    .main-footer {
        padding: 22px 16px 18px;
        gap: 0;
    }

    .main-footer .footer-col:nth-of-type(2),
    .main-footer .footer-col:nth-of-type(3) {
        display: none;
    }

    .main-footer .footer-col:first-child {
        margin-bottom: 0;
    }

    .logo-small {
        margin-bottom: 6px;
    }

    .footer-col p {
        margin: 0;
        font-size: 0.82rem;
        color: var(--text-muted);
    }

    .footer-bottom {
        margin-top: 14px;
        padding-top: 12px;
        justify-content: space-between;
        text-align: left;
        gap: 4px 12px;
    }

    /* 40px of hit area on the one control the footer still has. The ink stays
       0.8rem — rule 1 again. */
    .footer-bottom .footer-top {
        min-height: 40px;
        display: inline-flex;
        align-items: center;
    }


    /* 5.3 "ON QUIZFLIX" — the strips, sized for a thumb.

       The two rows and the square frames are in blog.css, because the ratio was
       wrong at every width and a strip is the right shape on a desktop too. What
       is here is only the size: a 132px card is a postage stamp on a phone and
       four of them fit across with nothing to grab. */
    .qf-titles {
        margin-top: 2rem;
        padding-top: 1.25rem;
    }

    .qf-titles-lede {
        font-size: 0.85rem;
        margin-bottom: 1rem;
    }

    .qf-title-card {
        flex: 0 0 40vw;
        width: 40vw;
    }

    /* Clamped, because the caption is what made the strip tall. "Cloudy with a
       Chance of Meatballs" is three lines on a 156px card and the role under it
       ("as Mayor Shelbourne (voice)") is two more — five lines of text under a
       square of art. Two and one, and the full string is still the accessible
       name on the button. */
    .qf-title-name {
        font-size: 0.82rem;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
    }

    .qf-title-role {
        font-size: 0.72rem;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }


    /* 5.4 RELATED ARTICLES — a strip of faces.

       "Worked together you can change the name of to related articles and on
       mobile it will be a film strip circular profile image behind the name
       under it."

       The rail's five full-width rows become one sideways row of portraits with
       the name beneath each. Same markup, same join, same links — the desktop
       list is untouched, because a column beside an article is the right shape
       there and a strip is the right shape here.

       The heading and the missing paragraph are in feature.costars.js. */
    .costar-list {
        flex-direction: row;
        gap: 14px;
        overflow-x: auto;
        overflow-y: hidden;
        padding-bottom: 4px;
        scroll-snap-type: x proximity;
        -webkit-overflow-scrolling: touch;
    }

    .costar-row {
        flex: 0 0 84px;
        scroll-snap-align: start;
    }

    .costar-link {
        flex-direction: column;
        align-items: center;
        gap: 8px;
        padding: 0;
        margin: 0;
        text-align: center;
    }

    .costar-art {
        flex: 0 0 72px;
        width: 72px;
        height: 72px;
    }

    .costar-text {
        align-items: center;
    }

    .costar-name {
        font-size: 0.78rem;
        line-height: 1.2;
    }

    /* The count is the reason a face is in this row, and on a 84px column it is
       three words wrapped to three lines under a name that is already wrapped.
       The join still decides the order; it just stops being narrated. */
    .costar-shared {
        display: none;
    }


    /* 5.5 "MORE FROM FEATURED" is one link now (site-chrome.js). The card around
       it keeps a card's own padding at every width; on a phone it does not need
       a border as well. */
    .sidebar-linkcard {
        padding: 0.75rem 1rem;
    }
}
