        /* the storefront's own section title — gold → silver, on a red → gold spine */
        .section-title {
            font-family: 'Orbitron', monospace;
            font-size: 1.4rem;
            font-weight: 700;
            background: linear-gradient(90deg, var(--accent-gold), var(--accent-silver));
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            margin: 2.4rem 0 1.2rem;
            position: relative;
            padding-left: 1.2rem;
            text-transform: uppercase;
            letter-spacing: 0.1em;
            word-break: break-word;
        }

        .section-title::before {
            content: '';
            position: absolute;
            left: 0;
            top: 0;
            width: 3px;
            height: 100%;
            background: linear-gradient(180deg, var(--accent-red), var(--accent-gold));
            box-shadow: var(--glow-red);
            border-radius: 2px;
        }

        /* ── THE CHAPTER — the box that makes the pin RELEASE ────────────────────────────────
           ★★ THIS WRAPPER IS NOT DECORATION AND DELETING IT BREAKS THE PAGE. A `position: sticky`
           element is constrained by its CONTAINING BLOCK — the nearest block-container ancestor.
           With every head a direct child of `main`, `main` IS that ancestor for all six of them,
           and `main`'s box spans the whole document: so each head docks when it arrives and then
           NEVER releases, and by the fourth chapter three titles are painted on top of each other
           at the same 144px. Measured on the shipped page: `<section class="chapter">` x0 against
           six heads, which is exactly what the operator photographed.

           Inside its own section, a head's constraint ends where its chapter's content ends, so it
           is pushed out of the dock as the next chapter arrives. That is the whole fix — the
           element, not the declaration below. `position: relative` is the anchor any absolutely
           positioned chapter furniture resolves against; the aperture currently resolves against
           the head itself (sticky is a positioned box), so this is the floor, not the mechanism. */
        .chapter {
            position: relative;
        }

        /* ── THE CHAPTER HEAD — the eyebrow, the title, the hairline, and THE PIN ─────────────
           The page reads as a document: each section announces itself with a number before its
           name, and that head then STICKS under the site header while its own cards scroll
           beneath it. /rnd/index.c.html's first proposition, carried onto this page's real
           content: you always know which chapter you are in, and the handover between two
           chapters is the transition — no divider needed, the pinning does the work.

           ★ THE SPACING IS MOVED, NOT ADDED, AND THAT IS THE WHOLE "NO GAP" ARGUMENT.
           A pinned head needs a PAINTED BOX — a slab with no padding has nothing to paint, and
           the title would ride the page's own content through it. But padding on top of the
           existing margins is exactly the dead vertical band this treatment must not introduce.
           So the 2.4rem/1.2rem the head already owned is SPLIT between margin and padding:

               margin 1.5rem + padding 0.9rem = 2.4rem   above, unchanged
               padding 0.7rem + margin 0.5rem = 1.2rem   below, unchanged

           The head's outer box opens and closes exactly where it did before, to the pixel, and
           the slab it needs comes out of space the section was already paying for.

           ★★ IT DOCKS AT `--header-clearance`, NEVER AT `top: 0`. app/shared/css/header.css pins
           the wordmark `position: fixed; z-index: 100`, so viewport-top belongs to the header and
           a head sticking at 0 is painted over — measured on the bench at 390px: visible slice
           0px, and elementFromPoint at the head's own top returned `header`. The dock is the
           header's OWN measured height (app/shared/js/header-clearance.js writes it off the
           border box), never a 144px literal, because the header's env(safe-area-inset-top) makes
           it genuinely taller on a notched phone.

           ⚠ NO backdrop-filter, per #218: a fixed-or-composited element carrying its own filter
           tracks the layout viewport on iOS and drifts. The slab is a plain gradient. */
        .chapter-head {
            position: sticky;
            top: var(--header-clearance);
            z-index: 12;                    /* over this chapter's cards, under the nav (9999) */
            margin: 1.5rem 0 0.5rem;
            padding: 0.9rem 0 0.7rem;
            /* THE SLAB IS THE PAGE'S OWN CANVAS COLOUR, FADED OUT DOWNWARD — the same technique
               app/shared/css/header.css uses for its phase-out, for the same reason: a pinned
               head with a flat fill and a hard bottom edge reads as a CUT sliding down the page.
               This one has no edge to see. */
            background: linear-gradient(180deg,
                rgba(10, 10, 15, 0.97) 0%,
                rgba(10, 10, 15, 0.92) 55%,
                rgba(10, 10, 15, 0.70) 80%,
                rgba(10, 10, 15, 0.00) 100%);
        }

        /* the head owns the rhythm now, so the title inside it carries no margin of its own */
        .chapter-head .section-title { margin: 0; }

        .chapter-no {
            display: block;
            font-family: 'Orbitron', monospace;
            font-size: 0.6rem;
            font-weight: 700;
            letter-spacing: 0.34em;
            text-transform: uppercase;
            color: var(--accent-red);
            opacity: 0.85;
            margin-bottom: 0.15rem;
            /* the spine's own inset, so the number and the title share one left edge */
            padding-left: 1.2rem;
        }

        /* ── THE TRAVELLING UNDERLINE. It tracks DOCUMENT PROGRESS, so a pinned head doubles as
           a progress read — the reason the head is worth pinning at all rather than merely
           announcing. `--doc-p` is published once per animation frame by app/shared/js/reveal.js.

           ★ THE `0` FALLBACK IS WHY THE SCRIPT IS NOT OPTIONAL: with no publisher the transform
           resolves to scaleX(0) and the rule is INVISIBLE on every chapter. The first port of this
           theme reasoned from exactly that fact to a STATIC hairline — which is backwards. The
           publisher is the fix; a static rule is the symptom.

           It starts at the spine's inset and stops at the column edge, so it underlines the two
           lines above it rather than cutting the frame. Shop's three accents in the wordmark's own
           order (red → gold → silver), which is what makes it read as this site. */
        .chapter-head::after {
            content: '';
            position: absolute;
            left: 1.2rem;
            right: 0;
            bottom: 2px;
            height: 1px;
            background: linear-gradient(90deg, var(--accent-red), var(--accent-gold), var(--accent-silver));
            transform: scaleX(var(--doc-p, 0));
            transform-origin: left center;
            opacity: 0.7;
        }

        /* ── THE APERTURE behind the head ────────────────────────────────────────────────────
           A conic ring masked to a thin annulus, so it reads as an iris rather than a pie chart.
           It sits BEHIND the head's slab, which is why it looks like light bleeding through
           rather than a shape sitting on the page. Art with no image and no request.

           ★ THE GEOMETRY DOES NOT MOVE, AND THAT IS LOAD-BEARING. Spinning the box with
           `transform: rotate()` makes its AXIS-ALIGNED BOUNDING BOX 190·(|cos θ|+|sin θ|) — up to
           268.7px against a declared 190px — which on the bench pushed the document's own
           scrollWidth to 418/421/423px on three reads at a 390px frame, a number that MOVED with
           the animation frame. The mask is an annulus centred on the box and is angularly
           SYMMETRIC, so rotating it was always a no-op: the only thing the spin ever showed was
           the conic's colour sweep. That sweep is driven directly, through a registered custom
           property. Identical motion, static box, zero horizontal overflow.
           `main` pads 5% (19.5px at 390), so −14px stops the iris 5.5px short of the frame. */
        @property --iris-turn {
            syntax: '<angle>';
            initial-value: 0deg;
            inherits: false;
        }
        .aperture {
            position: absolute;
            top: -46px;
            right: -14px;
            width: 190px;
            height: 190px;
            border-radius: 50%;
            pointer-events: none;
            z-index: -1;
            background: conic-gradient(from var(--iris-turn),
                rgba(230, 57, 70, 0.00),
                rgba(230, 57, 70, 0.42),
                rgba(212, 175, 55, 0.50),
                rgba(192, 192, 192, 0.30),
                rgba(230, 57, 70, 0.00));
            -webkit-mask: radial-gradient(circle, transparent 56%, #000 58%, #000 74%, transparent 76%);
                    mask: radial-gradient(circle, transparent 56%, #000 58%, #000 74%, transparent 76%);
            animation: irisTurn 34s linear infinite;
            opacity: 0.55;
        }
        @keyframes irisTurn { to { --iris-turn: 360deg; } }

        /* ── THE CURTAIN — full bleed between chapters ───────────────────────────────────────
           `main` pads to 5%, so a band that must reach the frame breaks the column with the
           standard `-50vw + 50%` trick. body already carries `overflow-x: hidden`
           (app/index/css/reset.css), which is what makes 100vw safe here even with a desktop
           scrollbar present. It is the page taking a breath between chapters, which is exactly
           what a flat panel column never does.

           ONE margin, not two: with an explicit `width: 100vw` a second negative margin
           over-constrains the box, and an over-constrained block resolves by ignoring one side —
           which makes the bleed depend on writing direction instead of on arithmetic. */
        .curtain {
            position: relative;
            margin-left: calc(-50vw + 50%);
            width: 100vw;
            height: 132px;
            overflow: hidden;
            display: flex;
            align-items: center;
            background:
                linear-gradient(180deg, transparent, rgba(230, 57, 70, 0.055) 42%, rgba(212, 175, 55, 0.05) 60%, transparent),
                radial-gradient(70% 120% at 50% 50%, rgba(212, 175, 55, 0.07), transparent 70%);
        }
        .curtain::before, .curtain::after {
            content: '';
            position: absolute;
            left: 6%; right: 6%;
            height: 1px;
            background: linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.55), transparent);
        }
        .curtain::before { top: 0; }
        .curtain::after  { bottom: 0; }

        /* the drifting wordmark. Doubled inside the span so the loop has no visible restart, and
           the band is `overflow: hidden` rather than scrollable — a horizontal scroller here
           would be read by app/shared/js/swipe.js as owning the touch, and the page flick would
           die on every curtain. */
        .curtain-run {
            white-space: nowrap;
            font-family: 'Orbitron', monospace;
            font-size: clamp(2.2rem, 11vw, 3.4rem);
            font-weight: 900;
            letter-spacing: 0.12em;
            text-transform: uppercase;
            color: transparent;
            -webkit-text-stroke: 1px rgba(212, 175, 55, 0.20);
            animation: curtainRun 40s linear infinite;
        }
        @keyframes curtainRun {
            from { transform: translateX(0); }
            to   { transform: translateX(-50%); }
        }

        /* ── THE CINEMATIC ARRIVAL ───────────────────────────────────────────────────────────
           Cards arrive up, scaled, and out of a slight blur, staggered by their own `--i`.
           app/shared/js/reveal.js marks `[data-reveal]` with `.is-in` as it enters and numbers
           each child. EVERYTHING here hangs off `html.reveal-on`, which only that script sets —
           so if it never runs, nothing is hidden. */
        html.reveal-on .card-grid[data-reveal] > *,
        html.reveal-on .carousel-track[data-reveal] > *,
        html.reveal-on .link-list[data-reveal] > * {
            opacity: 0;
            transform: translateY(34px) scale(0.94);
            filter: blur(6px);
            transition: opacity 0.7s ease,
                        transform 0.7s cubic-bezier(0.16, 1, 0.3, 1),
                        filter 0.7s ease;
            transition-delay: calc(var(--i, 0) * 90ms);
        }
        html.reveal-on .card-grid.is-in > *,
        html.reveal-on .carousel-track.is-in > *,
        html.reveal-on .link-list.is-in > * {
            opacity: 1;
            transform: none;
            filter: none;
        }

        /* app/index/css/entrance.css animates these same elements on LOAD with fixed nth-child
           delays. That is right for content above the fold and wrong for content revealed on
           scroll — the two would fight and the load animation would win, so every card would
           finish its entrance before it was ever on screen. Stood down ONLY while the reveal is
           actually driving. */
        html.reveal-on .card,
        html.reveal-on .carousel-card,
        html.reveal-on .link-item {
            animation: none !important;
        }

        /* on a wide screen the head pins against a column already inset 7%, so the aperture moves
           in with it rather than hanging off the frame. */
        @media (min-width: 769px) {
            .aperture { right: 2%; width: 230px; height: 230px; top: -60px; }
            .curtain  { height: 168px; }
        }

        @media (prefers-reduced-motion: reduce) {
            .aperture, .curtain-run { animation: none; }
            html.reveal-on .card-grid[data-reveal] > *,
            html.reveal-on .carousel-track[data-reveal] > *,
            html.reveal-on .link-list[data-reveal] > * {
                opacity: 1 !important;
                transform: none !important;
                filter: none !important;
                transition: none !important;
            }
        }
