/* ── 21 · RESEARCH — REALITY ZOOM ──
   The Overview tab (research/reality-zoom.js): a sticky stage in a tall
   track, scrubbed by one scroll number. Its own 1800px override lives here
   at the end, on purpose — parked in 20-wide.css it would lose on order.

   Part of the numbered set in css/ — the sheets are one stylesheet cut into
   pieces and index.html links them in this order. Order is the cascade: a
   rule here loses to the same-specificity rule in any later file. */

/* ─────────────────────────────────────
   RESEARCH – Reality zoom (research/reality-zoom.js)

   One sticky stage held inside a tall track. Everything below is driven by
   custom properties the module writes on each scroll frame — no CSS animation
   owns the timeline, so the whole thing is scrubbable in both directions.

     --rz-scale     the zoom, turning around the painting's pupil
     --rz-veil      0 → 1 as black takes the screen
     --rz-rail-in   the landmark rail's presence — its own ramp, not the veil's
     --rz-in        per-element presence (question, hint, each landmark)
     --rz-z         a landmark's distance: < 1 approaching, > 1 passing you
     --rz-blur      the same, in pixels of defocus

   The pupil's position comes from JS too (--rz-pupil-*): it is painted into a
   JPEG, so nothing here can find it. See the module header before touching the
   geometry.
───────────────────────────────────── */
.rz {
    --rz-veil: 0;
    /* The rail's own presence. Not derived from --rz-veil: it is up earlier and
       down later — see the rail's own block, and RAIL_* in reality-zoom.js. */
    --rz-rail-in: 0;
    --rz-ink: rgba(255, 255, 255, 0.88);
    --rz-ink-dim: rgba(255, 255, 255, 0.62);
    /* Read back by the module to decide whether there is a zoom to drive at
       all. It cannot infer this from the stage's `position` — the gate moves
       that too — so the mode is stated outright, here and in the fallback's
       media query, and nowhere else. */
    --rz-mode: zoom;
    /* The rail's footprint at the right edge of the stage — its own width plus
       the inset it sits at. `.rz-scene` reserves exactly this, so the two have
       to be changed together; a station label longer than this will overhang
       into the landmark again. */
    --rz-rail-space: 11rem;
    position: relative;
    width: 100%;
}

.rz-track {
    position: relative;
    width: 100%;
    /* Height is written by the module, from the vh budgets its timeline is
       derived from — the two must not be stated separately. */

    /* Shutting the gate takes ~700vh out of the page in one go. Scroll
       anchoring would answer that by correcting the scroll position itself,
       against an anchor it picked inside the very thing that changed — on top
       of the correction the module already makes. Two answers to one shift is
       a jump. */
    overflow-anchor: none;
}

.rz-stage {
    position: sticky;
    top: 0;
    height: 100vh;
    width: 100%;
    overflow: hidden;
    background: #06070b;
    isolation: isolate;
    display: grid;
    place-items: center;
}

/* ── The eye ── */
/* Deliberately unpromoted: no `will-change`, no `backface-visibility` hack,
   nothing else that would hand this its own compositor layer.

   A promoted layer is rasterised at its own size *times its scale*, and this
   one is the whole stage scaled by up to `coverScale` (~9). That texture is
   `stage width × 9` across, and it goes past the 16384px GPU limit the moment
   the viewport is wider than ~1810px:

       1280px viewport → 11954px raster   ok
       1800px viewport → 16335px raster   ok, by 49px
       1900px viewport → 17079px raster   over — tiles are dropped

   Dropped tiles composite as nothing, so the stage's own black shows through
   in rectangles across the eye. It only ever showed on the way *back out*,
   because paint() hides this element while `--rz-veil` is 1 and un-hides it
   the moment the veil starts lifting — which is at ~9× scale, the worst case,
   and the point at which the reader is looking straight at it.

   Unpromoted, the transform is painted into the sticky stage's layer and
   clipped by its `overflow: hidden`, so the raster is bounded by the stage no
   matter how far the eye is scaled. `will-change: transform` was never buying
   anything here in the first place: it caches a raster at one scale, and this
   scale is rewritten every scroll frame. */
.rz-zoom {
    --rz-scale: 1;
    position: absolute;
    inset: 0;
    transform: scale(var(--rz-scale));
    transform-origin: var(--rz-pupil-x, 50%) var(--rz-pupil-y, 50%);
}

/* Laid out from JS-measured pixels rather than object-fit: the cover fit has to
   be knowable in script for the pupil to be placed on top of it. */
.rz-eye {
    position: absolute;
    left: var(--rz-img-x, 0);
    top: var(--rz-img-y, 0);
    width: var(--rz-img-w, 100%);
    height: var(--rz-img-h, 100%);
    max-width: none;
    display: block;
    user-select: none;
    -webkit-user-drag: none;
}

/* A vector stand-in for the painted pupil. It is what actually swallows the
   screen — scaling the JPEG alone would just magnify its grain. */
.rz-pupil {
    position: absolute;
    left: var(--rz-pupil-x, 50%);
    top: var(--rz-pupil-y, 50%);
    width: calc(var(--rz-pupil-r, 0px) * 2);
    height: calc(var(--rz-pupil-r, 0px) * 2);
    margin: calc(var(--rz-pupil-r, 0px) * -1) 0 0 calc(var(--rz-pupil-r, 0px) * -1);
    border-radius: 50%;
    /* The painted pupil, sampled: rgb(37,37,37). Anything else shows as a disc
       sitting on top of the canvas instead of replacing it. */
    background: #252525;
}

.rz-veil {
    position: absolute;
    inset: 0;
    background: #06070b;
    opacity: var(--rz-veil);
    pointer-events: none;
}

/* Faint depth once the eye is gone, so the dark is not a flat panel. */
.rz-depth {
    position: absolute;
    inset: -20%;
    pointer-events: none;
    opacity: calc(var(--rz-veil) * 0.9);
    background:
        radial-gradient(38% 44% at 50% 46%, rgba(90, 130, 220, 0.16), transparent 70%),
        radial-gradient(60% 60% at 18% 82%, rgba(160, 110, 255, 0.1), transparent 70%),
        radial-gradient(50% 50% at 86% 22%, rgba(255, 90, 90, 0.08), transparent 70%);
}

/* ── "What is Reality?" ── */
.rz-question {
    --rz-in: 0;
    --rz-drift: 0;
    position: relative;
    z-index: 3;
    margin: 0;
    padding: 0 var(--content-inline-gutter);
    text-align: center;
    font-family: Georgia, "Times New Roman", serif;
    font-size: clamp(2.2rem, 6vw, 5rem);
    font-weight: 400;
    line-height: 1.05;
    letter-spacing: -0.01em;
    color: #fff;
    opacity: var(--rz-in);
    transform: scale(calc(0.94 + var(--rz-drift) * 0.12));
    text-shadow: 0 0 60px rgba(0, 0, 0, 0.85);
    pointer-events: none;
}

.rz-hint {
    --rz-in: 1;
    position: absolute;
    z-index: 3;
    /* Clears .rz-close, which now sits centred at the foot of the stage. */
    bottom: 6.2rem;
    margin: 0;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.72);
    text-shadow: 0 1px 12px rgba(0, 0, 0, 0.7);
    opacity: var(--rz-in);
    pointer-events: none;
}

.rz-hint::after {
    content: "";
    display: block;
    width: 0.5rem;
    height: 0.5rem;
    margin: 0.7rem auto 0;
    border-right: 1px solid currentColor;
    border-bottom: 1px solid currentColor;
    rotate: 45deg;
    animation: rz-nudge 1.8s ease-in-out infinite;
}

@keyframes rz-nudge {
    0%,
    100% {
        transform: translateY(-0.15rem);
        opacity: 0.45;
    }
    50% {
        transform: translateY(0.25rem);
        opacity: 1;
    }
}

/* ── Landmarks ── */
.rz-scenes {
    position: absolute;
    inset: 0;
    z-index: 4;
    display: grid;
    place-items: center;
    pointer-events: none;
}

.rz-scene {
    --rz-in: 0;
    --rz-z: 1;
    --rz-blur: 8;
    --rz-accent: #5599ff;
    grid-area: 1 / 1;
    width: min(var(--content-max-width), calc(100% - var(--content-inline-gutter) * 2));
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
    align-items: center;
    gap: clamp(2rem, 5vw, 4.5rem);
    opacity: var(--rz-in);
    transform: scale(var(--rz-z));
    filter: blur(calc(var(--rz-blur) * 1px));
    color: var(--rz-ink);
}

/* Only the landmark actually holding the screen can be clicked — otherwise a
   widget still fading in would catch the pointer through the dark. */
.rz-scene--held {
    pointer-events: auto;
}

/* Room for the rail. It is absolutely placed over the right of the stage while
   the content column is centred in the *full* width, so without this the active
   station's label lands on top of whatever is in the right-hand column — which
   at 1280px is the map, the artworks and the strands tiles, all of which reach
   that far. Whichever column is on the right, since the figure swaps sides.
   Only while the rail can actually be up: the locked poster has no rail, and
   the stacked fallback has no rail either. */
.rz--unlocked:not(.rz--static) .rz-scene {
    padding-right: var(--rz-rail-space);
}

/* ── …and the same room again on the left, but only for a centred scene ──
   The reservation above is one-sided because it is protecting a *column*: a
   two-column landmark puts its figure hard against the rail, and padding the
   other side would only narrow the copy for nothing.

   A `--text-only` scene has no columns. Its content is centred in the padding
   box, so a one-sided reservation centres it 88px (half the rail's 11rem) left
   of the stage — which is exactly the offset the opening landmark showed once
   it lost its figure and became the only scene of this kind. Measured at 1280:
   the copy's centre sat 72px left of the stage's own. Mirroring the padding
   makes the padding box concentric with the stage again, so "centred" means
   centred on screen rather than centred in whatever is left over.

   It costs the copy 11rem of available width, which nothing here notices: the
   paragraphs cap at 38rem and the column is wider than that at every width
   this branch runs at. */
.rz--unlocked:not(.rz--static) .rz-scene--text-only {
    padding-left: var(--rz-rail-space);
}

.rz-scene:nth-child(even) .rz-scene__figure {
    order: -1;
}

.rz-scene--text-only {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    text-align: center;
}

/* ── A landmark's background film ──
   Contained and feathered rather than bled across the stage. That is a decision
   about the *source*: the clip is 540 × 360, so a modest upscale is what 1896
   footage carries happily and a stage-wide one is mush. The mask is what keeps a
   contained video from reading as a boxed-in clip — with its edges dissolved
   there is no rectangle to notice, so it reads as the dark of the stage having a
   picture in it. It also sidesteps the full-bleed trap this file documents
   elsewhere: no `calc(-50vw + 50%)`, so the scene's padding for the rail cannot
   make it fall short.

   ── It is sized in its own right, NOT stretched to the scene, and that is the
   whole of why this was invisible ──
   It used to be `inset: -14% -9%`, i.e. the scene's box plus an overshoot. That
   was written when this landmark had a figure and its copy was one column of a
   two-column scene — a box roughly the shape of the film. The landmark then lost
   its figure and became `--text-only`, which spans the full width, and the box
   went with it: measured at 1280, **1477 × 331** for a 3:2 clip. `cover` answered
   by scaling 540px up to 1477 (2.7×) and cropping away two thirds of the frame's
   height, so what was left was a blown-up horizontal sliver of the middle — grey
   mush that reads as nothing at all, which is exactly how it was reported. No
   amount of opacity fixes that; the fault was the geometry.

   So the box states its own size and keeps the clip's own 3:2. `cover` then has
   nothing to crop. Centred on the scene, so it sits behind the copy whether or
   not the landmark has a figure, and capped three ways — see below. */
.rz-scene__film {
    position: absolute;
    z-index: -1;
    top: 50%;
    left: 50%;
    /* ── Three caps, and the third one is the stage ──
       Width, viewport width, and viewport *height* — because the box keeps a
       3:2 and the stage is exactly 100vh and clips, so a box capped on width
       alone grows past the stage the moment the window is wide and short.
       `132vh` is that bound solved for the ratio: height = width / 1.5, so
       width ≤ 1.32 × vh keeps the box inside 88% of the stage. Measured —
       1280 × 720 takes the 58rem and lands 928 × 619 in a 720 stage; 1280 × 600
       takes the vh cap instead at 792 × 528. The feathering means the last few
       per cent would not show if it did overrun, which is exactly why nothing
       would tell you about it.

       58rem is a 1.72× upscale of the 540px source, up from 1.36×. That is
       still well inside what archival footage carries once it is desaturated
       and feathered; what it must not become is stage-wide, which is ~3.5× and
       is mush. The copy caps at 38rem, so the picture now shows ~160px clear of
       the text on each side rather than being almost entirely behind it. */
    width: min(58rem, 92vw, 132vh);
    aspect-ratio: 3 / 2;
    translate: -50% -50%;
    overflow: hidden;
    border-radius: 1.5rem;
    -webkit-mask-image: radial-gradient(72% 68% at 50% 50%, #000 42%, transparent 100%);
    mask-image: radial-gradient(72% 68% at 50% 50%, #000 42%, transparent 100%);
    pointer-events: none;
}

/* ── Dimmed only once there is text to protect ──
   This is the ground four paragraphs sit directly on, with no scrim between
   them, so while they are up the film has to stay under the type rather than
   competing with it. A flat dimming answered that by holding the film at one
   value for the whole landmark — and the value the type wants is far below the
   value a film wants. Measured through the filter chain and composited over
   the stage's own #06070b: a mean frame reached **26 of 255** and the
   brightest patch (the station roof) 67. That is a film that is loaded,
   decoded and playing, and to the eye is not there — which is exactly how it
   was reported, and it is the same failure an earlier pass had already tried
   to fix by taking the opacity from 0.3 to 0.5. One number was never going to
   do it.

   `--rz-film-solo` is the beat before the first paragraph arrives, written
   every scroll frame by `paint()` (see FILM_TEXT_START). At 1 the film is
   simply the picture; it settles to the type's value as the first paragraph
   fades in, and scrubs back on the way out. Composited over the stage: mean
   **65 → 39**, brightest patch **180 → 105**.

   **Both ends came down when the clip was cut to the arrival, and that was not
   taste.** The band the copy sits over runs a mean of 84 and a peak of 221 in
   the arrival, against 67 and 171 in the full reel these numbers were first
   set against — bright sky and a bright platform, where the rest of the reel
   is a crowd in front of dark carriages. At the old 0.66/0.84 the new cut put
   the worst patch at 128 of 255, which is **3.97:1** for white body copy:
   under AA, on the one landmark whose copy has nothing between it and the
   picture. 0.58/0.78 takes it to 105 and **5.5:1**. Re-measure both if the
   clip is ever recut — a different ten seconds of the same film is a
   different exposure.

   **The settled floor is higher than the old flat value, and the copy is more
   readable rather than less** — see `.rz-scene--film .rz-scene__text`, which
   gives up `--rz-ink-dim` for solid white and gains more than the brighter
   film costs. Measured over the settled film, on the arrival cut: **14.9:1** on a
   mean frame and **5.5:1** over the brightest patch — where 62% white on the
   film as it was first shipped came to 7.3:1 and 5.0:1.
   Both numbers move together, so re-measure the pair whenever either does; the
   worst patch is a small highlight and the text-shadow is what carries it.

   `--rz-film-solo` defaults to 0 here rather than being left unset, so the
   settled value is what any branch where `paint()` never runs would get. */
.rz-scene--film {
    --rz-film-solo: 0;
}

.rz-scene__video {
    width: 100%;
    height: 100%;
    /* The loop and the still both come from the content module as inline
       custom properties, because a relative `url()` declared here would
       resolve against `css/` rather than through `<base>`. `cover` on the
       clip's own 3:2 in a 3:2 box has nothing to crop — see the box above,
       where stretching it is what made this invisible once before. */
    background-image: var(--rz-film-src);
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    opacity: calc(0.58 + var(--rz-film-solo) * 0.28);
    filter: grayscale(0.45) contrast(1.06) brightness(calc(0.78 + var(--rz-film-solo) * 0.14));
}

.rz-scene__copy {
    display: grid;
    gap: 1rem;
    align-content: center;
}

/* ── The film landmark's text arrives a line at a time ──
   Scoped to `--film` and not to `.rz-scene` at large: this is the opening
   landmark and the stagger is doing a specific job there — the three paragraphs
   are a story with a turn in it, and handing them over at once wastes it. Every
   other landmark states its case in one block and should keep arriving as one.

   There is no eyebrow and no title here any more — "How is reality
   constructed?" moved to the last landmark, where it lands the whole dive —
   so the film has nothing captioning it and these three lines are the entire
   scene.

   **Driven by `--rz-text-in`, not a CSS transition.** A transition-delay is
   wall-clock: it used to hold the first paragraph back a beat and step the
   rest in behind it, which reads well at a normal scroll speed and fails at a
   fast one — a reader who flicks through the landmark's slot faster than the
   delay counts down passes it having only ever seen the film, the paragraphs
   having technically run just off-screen. `--rz-text-in` is written every
   scroll frame in reality-zoom.js's `paint()`, as a fraction of the landmark's
   own `local` progress — the same number everything else in the dive already
   answers to — so a fast scroll compresses the reveal instead of skipping it,
   and it stays scrubbable in both directions.

   **`.rz--static` has to hand the text back**, and this is the trap the deck's
   `opacity: 1` is the other half of: `paint()` never runs in the stacked or
   reduced-motion branch, so `--rz-text-in` is never written and, without the
   reset below, the opening landmark would be three invisible paragraphs on a
   phone. */
/* Solid white and a shadow, where every other landmark's copy is
   `--rz-ink-dim` (62% white). That is not this landmark asserting itself: it
   is the only copy on the dive that sits on a *picture* rather than on the
   stage's own black, and 62% white over a film is the half of the contrast
   budget that costs nothing to buy back. It is what pays for the film being
   legible above — the pair was measured together. The shadow is for the
   brightest patch of the frame, which is a small highlight the average cannot
   speak for. */
.rz-scene--film .rz-scene__text {
    --rz-text-in: 0;
    max-width: 38rem;
    font-size: clamp(1.02rem, 1.35vw, 1.24rem);
    color: #fff;
    text-shadow:
        0 1px 2px rgba(6, 7, 11, 0.72),
        0 0 18px rgba(6, 7, 11, 0.85);
    opacity: var(--rz-text-in);
    translate: 0 calc((1 - var(--rz-text-in)) * 1.3rem);
}

.rz--static .rz-scene--film .rz-scene__text {
    opacity: 1;
    translate: 0 0;
}

/* The opening landmark's paragraphs are four short beats rather than one block
   of argument, so they take more air between them than `--essay`'s 0.75rem —
   that gap is sized for a twenty-line block fighting the 100vh stage for room,
   and this scene is 260px tall in a 720px stage with plenty to spare. It is
   also what makes the break after "…still continue to fade…" read as a beat
   instead of a wrap. Scoped to `--film` so no other essay landmark loosens. */
.rz-scene--film.rz-scene--essay .rz-scene__copy {
    gap: 1.35rem;
}

.rz-scene__eyebrow {
    margin: 0;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--rz-accent);
}

.rz-scene__title {
    margin: 0;
    font-family: Georgia, "Times New Roman", serif;
    font-size: clamp(2rem, 4.2vw, 3.4rem);
    font-weight: 400;
    line-height: 1.06;
    color: #fff;
}

.rz-scene__text {
    margin: 0;
    max-width: 34rem;
    font-family: Georgia, "Times New Roman", serif;
    font-size: clamp(0.95rem, 1.15vw, 1.08rem);
    line-height: 1.78;
    color: var(--rz-ink-dim);
}

/* ── A beat before a paragraph, in space as well as in time ──
   Set from the content module (`{ text, pause: true }`), which is where the
   reason for it lives: the line above ends on an ellipsis, and a paragraph
   arriving at the standard gap reads as the next sentence rather than as the
   turn it is. The stagger holds a matching step of dead air — see
   FILM_TEXT_STEP — because a wider gap alone, with the reveal still stepping
   at its usual cadence, reads as a hole in the layout rather than as a pause.

   It costs the scene ~26px of height, which the 100vh stage has: this
   landmark measures ~260px in a 720px stage, where `--essay`'s clipping
   problem is a twenty-line block.

   **It has to sit after `.rz-scene__text`, not with the `--film` rules
   above.** Both selectors are one class, so `margin: 0` there wins on source
   order from anywhere earlier in the file — which is the cascade this
   stylesheet is numbered around, and it silently zeroed the gap. */
.rz-scene__text--beat {
    margin-top: 1.6rem;
}

/* ── A landmark that argues rather than captions ──
   Set when the landmark's `text` is several paragraphs. The stage is exactly
   100vh and clips, so the leading that is right for four lines is not right for
   twenty: measured at 920 × 600 — the tightest two-column case there is, since
   below 900px the whole thing stacks and scrolls — the question landmark came
   to 617px in a 600px stage and lost its last paragraph. Nothing warns you,
   because the stage's own `overflow: hidden` is what is clipping it.

   The three steps are ordered by what they cost: the gap and the leading are
   typography that a block this long wanted anyway, and only the last resort
   touches the size of the type. */
.rz-scene--essay .rz-scene__copy {
    gap: 0.75rem;
}

.rz-scene--essay .rz-scene__text {
    line-height: 1.62;
}

/* Narrow *and* short, which is the only place the first two steps do not
   already clear the stage on their own. Measured at 920 × 600, the tightest
   two-column case: 617px before any of this, 561 with the leading alone — which
   fits, but by 39px, and a stage 40px shorter would clip again. Wide and short
   is never the problem, because the copy column grows with the viewport and the
   block wraps to fewer lines: 1120 × 600 comes to 474 untouched.

   The lower bound is the stacked fallback's own breakpoint, and it is not
   cosmetic: below 900px the stage stops being sticky and the page simply
   scrolls, so there is nothing to clip — a phone at 375 × 667 would otherwise
   match this query and shrink its text for a reason that does not apply. */
@media (min-width: 901px) and (max-width: 1100px) and (max-height: 700px) {
    .rz-scene--essay .rz-scene__text {
        font-size: clamp(0.9rem, 1.05vw, 1rem);
        line-height: 1.56;
    }
}

.rz-scene__note {
    margin: 0;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.78rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.4);
}

.rz-scene__tags {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 0.45rem;
    padding: 0;
    margin: 0.2rem 0 0;
}

.rz-scene__tags li {
    padding: 0.32em 0.85em;
    border: 1px solid color-mix(in srgb, var(--rz-accent) 40%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, var(--rz-accent) 10%, transparent);
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.7rem;
    letter-spacing: 0.06em;
    color: color-mix(in srgb, var(--rz-accent) 72%, #fff);
}

.rz-scene__figure {
    display: grid;
    justify-items: center;
}

/* ── Figures ── */
.rz-fig {
    display: grid;
    justify-items: center;
    gap: 1rem;
    width: 100%;
    max-width: 26rem;
}

/* The question and its button, as one block: every interactive landmark asks
   before it tells, and they have to read as the same control. */
.rz-fig__ask {
    display: grid;
    justify-items: center;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
}

/* The line reserves two lines' worth of room whether or not it needs them. The
   landmarks are centred in the stage, so a question and an answer of different
   lengths would otherwise resize the figure and shift the whole scene on the
   button press — at exactly the moment the reader is looking at the picture
   above it. */
.rz-fig__prompt {
    display: grid;
    align-items: center;
    margin: 0;
    max-width: 24rem;
    min-height: calc(0.78rem * 1.6 * 2);
    text-align: center;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.78rem;
    line-height: 1.6;
    color: rgba(255, 255, 255, 0.5);
}

.rz-fig__prompt--answer {
    color: color-mix(in srgb, var(--rz-accent) 55%, #fff);
}

.rz-fig__button {
    padding: 0.6em 1.5em;
    border: 1px solid color-mix(in srgb, var(--rz-accent) 55%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, var(--rz-accent) 12%, transparent);
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--rz-accent) 60%, #fff);
    cursor: pointer;
    transition:
        background 0.2s ease,
        border-color 0.2s ease,
        color 0.2s ease;
}

.rz-fig__button:hover,
.rz-fig__button:focus-visible {
    background: color-mix(in srgb, var(--rz-accent) 26%, transparent);
    border-color: var(--rz-accent);
    color: #fff;
}

/* Pressed once and there is nothing behind it yet. After the hover rule on
   purpose — same specificity, so order is what makes it win. */
.rz-fig__button[aria-disabled="true"],
.rz-fig__button[aria-disabled="true"]:hover {
    background: rgba(255, 255, 255, 0.04);
    border-color: rgba(255, 255, 255, 0.16);
    color: rgba(255, 255, 255, 0.45);
    cursor: default;
}

/* ── The cloud ──
   Reality at the centre, the six dimensions the lab measures large around it,
   and the rest of its vocabulary smaller further out. Every coordinate is
   computed in reality-zoom.js by packing the words along an outward spiral, so
   nothing here may set a position — and **the font here and the font the
   packer measures with have to be the same one**, family, weight and size, or
   the boxes it packed are not the boxes that get painted and the words
   overlap. `font-size` is written per word as an attribute for exactly that
   reason: it is the packer's number, not the stylesheet's.

   Three tones, and they are the three the rest of the zoom already uses for the
   same three ideas: the body's red, control's blue, the self's purple. A word
   with no tone is one of the lab's methods rather than one of its subjects. */
/* 26rem is `.rz-fig`'s own cap, which is what actually binds — repeated here so
   the number the packer's viewBox is scaled by is visible next to it. At 416px
   drawn for a 460-unit viewBox the scale is 0.90, and that is what turns the
   smallest word's 9.5 units into the 8.6px it is read at. */
.rz-cloud {
    width: 100%;
    max-width: 26rem;
    height: auto;
    overflow: visible;
}

.rz-cloud__word {
    --rz-cloud-tone: #8fb7ff;
    /* How far the word is let back up once its landmark holds the screen. It is
       a token rather than an `opacity` per tier because the entry animates this
       property from 0, and a tier that names its own value has to name it twice
       — once at rest and once in the `--held` rule — where the two then drift. */
    --rz-word-opacity: 1;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-weight: 600;
    letter-spacing: 0.05em;
    fill: color-mix(in srgb, var(--rz-cloud-tone) 62%, #fff);
}

.rz-cloud__word--body {
    --rz-cloud-tone: #ff5f57;
}

.rz-cloud__word--control {
    --rz-cloud-tone: #5599ff;
}

.rz-cloud__word--self {
    --rz-cloud-tone: #a98bff;
}

/* The two tiers are told apart by weight and by how far back each sits, not by
   size alone — size is the packer's and already varies inside a tier (a
   24-character dimension is set smaller than an 8-character one, or it is a
   banner rather than a word), so on its own the line between "a dimension" and
   "a keyword" is not one a reader could draw. */
.rz-cloud__word--dimension {
    font-weight: 700;
}

/* 0.68 and no lower, and it is a measurement rather than a taste: the dimmest
   word in the cloud is a control-blue keyword, rgb(150 192 255), and over the
   veil's own near-black that composites to 5.3:1 at this value and to 3.9:1 at
   0.56 — under AA for text this small. The smallest word here renders at 8.6px
   (9.5 in a 460-unit viewBox drawn 416px wide), which is the floor the network
   this replaced already sat at. */
.rz-cloud__word--keyword {
    font-weight: 500;
    --rz-word-opacity: 0.68;
}

/* Serif, white and much larger: it is the thing the other twenty-four are
   around, and every one of them is set in the same sans at the same weight. */
.rz-cloud__word--hub {
    font-family: Georgia, "Times New Roman", serif;
    font-size: 34px;
    font-weight: 400;
    letter-spacing: 0.01em;
    fill: #fff;
}

/* Index 0 is "Reality", so it is the first word there and the rest assemble
   around it — the same order the network's hub and ring had. The step is small
   (0.035s against the network's 0.08s) because there are twenty-five of these
   where there were seven: at the old spacing the last word arrived two seconds
   after the first, which is longer than the landmark's own way in. */
.rz-cloud__word {
    opacity: 0;
    transition: opacity 0.5s ease calc(var(--rz-word-index) * 0.035s);
}

.rz-cloud__glow {
    opacity: 0;
    transition: opacity 0.9s ease 0.15s;
}

.rz-scene--held .rz-cloud__word {
    opacity: var(--rz-word-opacity);
}

.rz-scene--held .rz-cloud__glow {
    opacity: 1;
}

/* ── A word that has a paper behind it ──
   **It grows, and it is not underlined.** An underline is what a link in a
   paragraph looks like, and these are not in a paragraph — a dotted rule under
   every second word turned the picture into a form. Growing is the thing a word
   cloud can do that prose cannot, and it says the same thing without adding a
   mark: the word comes forward, and the strip below fills in.

   `--rz-word-opacity` is what the keyword tier is dimmed to, so coming up to 1
   is the same move the tiers already use for recession, and the two together
   are what make the growth read as a lift rather than as a wobble.

   1.1 is a measured ceiling, not a taste. The packer pads every box by
   `PAD_X = 5` units *on each side*, so two words' ink is at least 10 units
   apart; the widest word here is "Phenomenological Control" at ~180 units,
   which grows by 18 — 9 a side — and still clears its neighbours. Going past
   that puts the longest words through whatever is beside them, and SVG paints
   in document order, so there is no z-index to lift the hovered one out with. */
.rz-cloud__anchor {
    cursor: pointer;
}

/* `fill-box` is what makes `center` the word's own centre rather than the
   viewBox's — without it every word flies toward the middle as it grows. */
.rz-cloud__word--linked {
    transform-box: fill-box;
    transform-origin: center;
    /* Deliberately two timings. The opacity is the landmark's way in and is
       staggered by word index; the transform is a response to the pointer and
       must not inherit a delay of up to 0.9s. */
    transition:
        opacity 0.5s ease calc(var(--rz-word-index) * 0.035s),
        transform 0.18s ease;
}

/* Not scoped to `--held`: a landmark that is not holding has `pointer-events:
   none` and is `inert`, so it can be neither hovered nor focused and does not
   need saying twice — and the stacked fallback, where nothing is ever held,
   would otherwise lose the feedback entirely. */
.rz-cloud__anchor:hover .rz-cloud__word--linked,
.rz-cloud__anchor:focus-visible .rz-cloud__word--linked {
    --rz-word-opacity: 1;
    transform: scale(1.1);
}

/* ── The strip under the cloud ──
   Empty until a word is pressed, and holding its full height throughout — a
   landmark is centred in the stage, so a strip that appeared on a press would
   shift the whole scene under the reader at the moment they pressed it. Same
   reason `.pub-scholar__states` stacks and the ask line reserves two lines.

   The height comes from the link itself, which is laid out at `opacity: 0`
   rather than removed; the one part that has no height of its own at rest is
   the title, hence its `min-height`.

   No margin: `.rz-fig` is a grid with its own 1rem gap, and a margin here would
   be that space twice over. */
.rz-cloud-cite {
    display: grid;
    width: 100%;
    text-align: left;
}

.rz-cloud-cite__link {
    display: grid;
    gap: 0.25rem;
    margin: 0;
    text-decoration: none;
    transition: opacity 0.25s ease;
}

.rz-cloud-cite[data-state="rest"] .rz-cloud-cite__link {
    opacity: 0;
    pointer-events: none;
}

/* The landmark's accent, as every other eyebrow in the zoom is. */
.rz-cloud-cite__label {
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--rz-accent);
}

/* ── The title is white, and gold was tried ──
   #e8b73f — the Creations tab's "this one is ours" mark over a dark plate — on
   the argument that the cloud's own three tones are spoken for (body, control,
   self) so a title in any of them reads as a fourth word in that scheme. It
   isn't wrong about the scheme and it is still wrong here: white is not one of
   the three either, and a warm title turned the strip into a thing of its own
   rather than the caption of the picture above it. Reverted. The one point of
   colour is the accent label, as everywhere else in the zoom.

   Clamped to two lines rather than allowed to run: it is the one string here
   not written for this box, and a 130-character review title is not going to be
   shortened because it is being quoted. The `min-height` is what reserves the
   space while it is empty — without it the strip is 16px until the first press
   and 72px after, which is the resize the reservation was for. Both
   declarations say the same thing; the second keeps saying it if the type
   changes. */
.rz-cloud-cite__title {
    min-height: 2.9em;
    min-height: 2lh;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    font-family: Georgia, "Times New Roman", serif;
    font-size: 0.86rem;
    line-height: 1.45;
    color: rgba(255, 255, 255, 0.82);
}

/* Reserved for the same reason the title above is, and it is easy to miss that
   it needs it too: with only the title reserved the strip was 63px at rest and
   75px filled, and the figure is centred in the scene — so the whole cloud
   stepped up 6px the first time a word was pressed. */
.rz-cloud-cite__who {
    min-height: 1.2em;
    min-height: 1lh;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.72rem;
    color: rgba(255, 255, 255, 0.5);
}

.rz-cloud-cite__link:hover .rz-cloud-cite__title,
.rz-cloud-cite__link:focus-visible .rz-cloud-cite__title {
    color: #fff;
    text-decoration: underline;
    text-underline-offset: 0.2em;
}

/* Ponzo. Both bars are the same rect; only the rails differ. */
.rz-ponzo {
    width: 100%;
    max-width: 19rem;
    height: auto;
}

.rz-ponzo__rails line {
    stroke: rgba(255, 255, 255, 0.55);
    stroke-width: 1.2;
    transition:
        opacity 0.45s ease,
        stroke 0.45s ease;
}

.rz-ponzo__rails .rz-ponzo__tie {
    stroke: rgba(255, 255, 255, 0.16);
}

.rz-fig--ponzo[data-context="off"] .rz-ponzo__rails {
    opacity: 0;
}

.rz-ponzo__rails {
    transition: opacity 0.45s ease;
}

.rz-ponzo__bar {
    fill: #ff4d4d;
    transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1) calc(var(--rz-bar-index) * 0.06s);
}

/* With the rails gone the bars slide together, which is the only way to see
   with your own eyes that they were the same length all along. The bars sit at
   y 83.5 and 201.5 in the viewBox, so ±44 leaves them 30 apart — close enough
   to compare, far enough not to read as one thick bar. */
.rz-fig--ponzo[data-context="off"] .rz-ponzo__bar {
    transform: translateY(calc((0.5 - var(--rz-bar-index)) * 88px));
}

.rz-ponzo__guide line {
    stroke: rgba(255, 255, 255, 0.35);
    stroke-width: 1;
    stroke-dasharray: 3 4;
}

.rz-ponzo__guide {
    opacity: 0;
    transition: opacity 0.4s ease 0.35s;
}

.rz-fig--ponzo[data-context="off"] .rz-ponzo__guide {
    opacity: 1;
}

/* Artworks. Two paintings, both human — the reveal is that the two badges say
   the same word. */
.rz-fig--artworks {
    max-width: 30rem;
}

.rz-artworks {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.8rem;
    width: 100%;
}

/* One even padding all round, and nothing in the card but the frame. It used to
   carry a verdict line under the painting which sat there at opacity 0 for as
   long as the question was open — invisible, but still a row of the grid, which
   is what put more space under the pair than over it. The verdict is gone and
   the badge inside the frame is absolutely placed, so revealing costs no
   height and the frame is what the card is sized to. */
.rz-artworks__card {
    display: block;
    padding: 0.5rem;
    border: 1px solid rgba(255, 255, 255, 0.16);
    border-radius: 0.9rem;
    background: rgba(255, 255, 255, 0.04);
    text-align: center;
    cursor: pointer;
    transition:
        border-color 0.3s ease,
        background 0.3s ease,
        transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

.rz-artworks__card:hover,
.rz-artworks__card:focus-visible {
    transform: translateY(-3px);
    border-color: color-mix(in srgb, var(--rz-accent) 55%, transparent);
}

/* 4/5, which is both canvases to within a percent (737×900 and 717×900), so
   `contain` below has almost nothing to letterbox. If a painting is ever
   swapped for one of a different shape, this is the number to revisit — a
   square frame works for anything but wastes a third of the card. */
.rz-artworks__frame {
    position: relative;
    display: block;
    aspect-ratio: 4 / 5;
    border-radius: 0.55rem;
    overflow: hidden;
    background: rgba(0, 0, 0, 0.45);
}

.rz-artworks__img {
    display: block;
    width: 100%;
    height: 100%;
    /* contain, never cover: the reader is being asked to judge the picture, and
       cropping it would change the thing they are judging. */
    object-fit: contain;
}

.rz-artworks__badge {
    position: absolute;
    left: 50%;
    bottom: 0.5rem;
    translate: -50% 0;
    padding: 0.3em 0.8em;
    border-radius: 999px;
    background: rgba(6, 7, 11, 0.78);
    backdrop-filter: blur(6px);
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.6rem;
    font-weight: 700;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: #fff;
    opacity: 0;
    transition: opacity 0.35s ease calc(var(--rz-card-index, 0) * 0.14s);
}

.rz-fig--artworks[data-revealed="true"] .rz-artworks__badge {
    opacity: 1;
}

/* The reader's own answer, kept on screen next to the reveal. */
.rz-artworks__card[data-picked="true"] {
    border-color: color-mix(in srgb, var(--rz-accent) 65%, transparent);
    background: color-mix(in srgb, var(--rz-accent) 10%, transparent);
}

/* ── Heart & brain ──
   One cardiac cycle, drawn as a ring that fills once per beat. Every moving
   part runs on `--rz-cycle` and on nothing else, so the ejection, the burst up
   the nerve and the ring's systolic arc cannot drift out of step however long
   the loop runs. The percentages below are all percentages of that one cycle:

     0 %          the ring starts over, the ventricles begin to contract
     ~12–14 %     ejection; the pressure wave reaches the baroreceptors
     ~16–38 %     the burst travels up the nerve and arrives at the brainstem
     35 %         end of systole — the ring crosses into its diastolic arc
     35–100 %     diastole, filling

   The ring is 2π·60 = 376.99 units around, which is where the dash numbers come
   from: systole is the first 35% of that (131.9), diastole the remaining 245.1,
   each pulled in a little to leave a gap where they meet.

   The transform origins repeat coordinates from the markup in reality-zoom.js
   (ring and heart centred at 160, 208; the baroreceptor node at 108, 146) —
   CSS cannot read them off the drawing, so the two have to be changed together
   if the geometry ever moves. */
/* Three colours, and all three are already site tokens rather than one-offs:
   the heart and its burst take the landmark's own accent (red), the brain the
   Beliefs purple, the diastolic half of the cycle the Perception blue. That
   also does the phase's work for it — the ring is red while it is ejecting and
   blue while it is filling, so the two arcs read apart at a glance. */
.rz-fig--heartbrain {
    --rz-cycle: 1.05s;
    --rz-brain: #a98bff;
    --rz-dia: #5599ff;
    max-width: 23rem;
}

.rz-hb {
    width: 100%;
    height: auto;
    overflow: visible;
}

/* Nothing here should be running while the landmark is still out in the dark.
   Scoped away from static mode, where no landmark is ever `--held` because
   paint() never runs — without that guard the stacked fallback would show a
   diagram frozen on frame one. */
.rz:not(.rz--static) .rz-scene:not(.rz-scene--held) .rz-hb * {
    animation-play-state: paused;
}

/* The brain, and its answer to the burst arriving. */
.rz-hb__brain-shape {
    fill: color-mix(in srgb, var(--rz-brain) 12%, transparent);
    stroke: color-mix(in srgb, var(--rz-brain) 65%, transparent);
    stroke-width: 1.4;
}

.rz-hb__fold,
.rz-hb__stem {
    fill: none;
    stroke: color-mix(in srgb, var(--rz-brain) 45%, transparent);
    stroke-width: 1.2;
    stroke-linecap: round;
}

.rz-hb__glow-stop {
    stop-color: var(--rz-brain);
}

.rz-hb__glow-stop--in {
    stop-opacity: 0.5;
}

.rz-hb__glow-stop--out {
    stop-opacity: 0;
}

.rz-hb__glow {
    opacity: 0.12;
    animation: rz-hb-glow var(--rz-cycle) ease-out infinite;
}

/* Starts at 36%, not before: the burst reaches the brainstem at 40% (the dash
   is at path position 80 of 100 by 35%), and a brain that lights while the
   signal is still in transit reads as two unrelated things blinking. */
@keyframes rz-hb-glow {
    0%,
    36% {
        opacity: 0.12;
    }
    48% {
        opacity: 1;
    }
    80%,
    100% {
        opacity: 0.12;
    }
}

/* The afferent path, and the burst that travels it. */
.rz-hb__nerve {
    fill: none;
    stroke: rgba(255, 255, 255, 0.18);
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-dasharray: 3 4;
}

/* pathLength="100" on the element, so the dash maths is in percent of the path
   and does not have to be remeasured if the curve is ever redrawn. The pattern
   is one 12-unit dash in a 212-unit period: at offset 12 the dash sits just
   before the start of the path and at -100 just past its end, so the two
   extremes are both off-path and the burst sweeps the whole way between. */
.rz-hb__burst {
    fill: none;
    stroke: var(--rz-accent);
    stroke-width: 3.4;
    stroke-linecap: round;
    stroke-dasharray: 12 200;
    stroke-dashoffset: 12;
    opacity: 0;
    filter: drop-shadow(0 0 5px color-mix(in srgb, var(--rz-accent) 80%, transparent));
    animation: rz-hb-burst var(--rz-cycle) linear infinite;
}

@keyframes rz-hb-burst {
    0%,
    12% {
        stroke-dashoffset: 12;
        opacity: 0;
    }
    16% {
        opacity: 1;
    }
    34% {
        opacity: 1;
    }
    40% {
        stroke-dashoffset: -100;
        opacity: 0;
    }
    100% {
        stroke-dashoffset: -100;
        opacity: 0;
    }
}

.rz-hb__baro {
    fill: var(--rz-accent);
}

/* The node's own flash. A second circle scaled outwards rather than an animated
   `r`: geometry properties are animatable but unevenly, and this is the same
   picture with no support question attached. */
.rz-hb__ping {
    fill: none;
    stroke: var(--rz-accent);
    stroke-width: 1.4;
    opacity: 0;
    transform-box: view-box;
    transform-origin: 108px 146px;
    animation: rz-hb-ping var(--rz-cycle) ease-out infinite;
}

@keyframes rz-hb-ping {
    0%,
    10% {
        transform: scale(0.5);
        opacity: 0;
    }
    14% {
        opacity: 0.9;
    }
    36%,
    100% {
        transform: scale(2.6);
        opacity: 0;
    }
}

/* The cycle ring. Both arcs are the same circle with different dashes. */
.rz-hb__arc {
    fill: none;
    stroke-width: 6;
    stroke-linecap: butt;
}

.rz-hb__arc--sys {
    stroke: color-mix(in srgb, var(--rz-accent) 34%, transparent);
    stroke-dasharray: 124 253;
    stroke-dashoffset: -4;
    animation: rz-hb-systole var(--rz-cycle) step-end infinite;
}

.rz-hb__arc--dia {
    stroke: color-mix(in srgb, var(--rz-dia) 40%, transparent);
    stroke-dasharray: 241 136;
    stroke-dashoffset: -136;
    animation: rz-hb-diastole var(--rz-cycle) step-end infinite;
}

/* The fill: one lap of the ring per beat, restarting where systole begins — but
   drawn as two arcs, one per phase, rather than one stroke that changes colour
   at the boundary.

   One stroke was wrong in a way that only shows once you watch a whole beat:
   the colour applies to the *entire* travelled arc, so the ring was all red
   through systole and then all of it turned blue — the phase just drawn was
   erased by the one being drawn, and by the end of the lap nothing on screen
   said where systole had been. Each phase now fills its own arc and holds it:
   red takes the first 35% and stays, blue grows from the boundary to the top.

   Both run at the same rate, which is what makes them one continuous head
   rather than two fills that happen to meet: 131.95 units (35% of 2π·60) over
   35% of the cycle, and 245.04 over the remaining 65%. The blue arc's own
   `rotate(126)` in the markup is what starts it at the boundary — 126° is
   0.35 of a turn, and it is the only number the two have to agree on. */
.rz-hb__sweep {
    fill: none;
    stroke-width: 2.4;
    /* butt, not round: the two arcs meet, and a round cap on each would put a
       bead of the wrong colour over the join. */
    stroke-linecap: butt;
    stroke-dasharray: 376.99;
}

.rz-hb__sweep--sys {
    stroke: var(--rz-accent);
    animation: rz-hb-sweep-sys var(--rz-cycle) linear infinite;
}

.rz-hb__sweep--dia {
    stroke: var(--rz-dia);
    animation: rz-hb-sweep-dia var(--rz-cycle) linear infinite;
}

@keyframes rz-hb-sweep-sys {
    0% {
        stroke-dashoffset: 376.99;
    }
    35%,
    100% {
        stroke-dashoffset: 245.04;
    }
}

@keyframes rz-hb-sweep-dia {
    0%,
    35% {
        stroke-dashoffset: 376.99;
    }
    100% {
        stroke-dashoffset: 131.95;
    }
}

/* The head of that fill, so the position is legible at a glance rather than
   having to be read off the length of an arc. Translate and rotate together in
   the keyframes: a CSS transform replaces the element's `transform` attribute
   outright, so the placement has to come along with the rotation. */
.rz-hb__hand circle {
    fill: #fff;
}

.rz-hb__hand {
    transform-box: view-box;
    transform-origin: 0 0;
    animation: rz-hb-hand var(--rz-cycle) linear infinite;
}

@keyframes rz-hb-hand {
    from {
        transform: translate(160px, 208px) rotate(0deg);
    }
    to {
        transform: translate(160px, 208px) rotate(360deg);
    }
}

/* The heart. Contracts through systole, refills through diastole. */
.rz-hb__heart {
    fill: var(--rz-accent);
    filter: drop-shadow(0 0 10px color-mix(in srgb, var(--rz-accent) 55%, transparent));
    transform-box: view-box;
    transform-origin: 160px 208px;
    animation: rz-hb-beat var(--rz-cycle) ease-in-out infinite;
}

@keyframes rz-hb-beat {
    0% {
        transform: scale(1);
    }
    6% {
        transform: scale(1.05);
    }
    15% {
        transform: scale(0.86);
    }
    33% {
        transform: scale(0.89);
    }
    47% {
        transform: scale(1.03);
    }
    64%,
    100% {
        transform: scale(1);
    }
}

.rz-hb__tag {
    fill: rgba(255, 255, 255, 0.55);
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 9.5px;
    font-weight: 600;
    letter-spacing: 0.1em;
}

.rz-hb__tag--brain {
    fill: color-mix(in srgb, var(--rz-brain) 70%, #fff);
}

.rz-hb__tag--baro,
.rz-hb__tag--sys {
    fill: color-mix(in srgb, var(--rz-accent) 60%, #fff);
}

.rz-hb__tag--dia {
    fill: color-mix(in srgb, var(--rz-dia) 60%, #fff);
}

/* The phase labels and their arcs share one pair of animations, so the word and
   the lit arc are the same statement rather than two that have to be kept in
   agreement. step-end: a phase boundary is a boundary, not a fade. */
.rz-hb__tag--sys {
    animation: rz-hb-systole var(--rz-cycle) step-end infinite;
}

.rz-hb__tag--dia {
    animation: rz-hb-diastole var(--rz-cycle) step-end infinite;
}

@keyframes rz-hb-systole {
    0% {
        opacity: 1;
    }
    35%,
    100% {
        opacity: 0.32;
    }
}

@keyframes rz-hb-diastole {
    0% {
        opacity: 0.32;
    }
    35%,
    100% {
        opacity: 1;
    }
}

/* Held with the burst in flight. Deliberately a composite rather than a real
   frame of the loop — the brain is lit here although at 30% of a running cycle
   it would not be yet. With only one image to make the point with, showing the
   whole chain at once beats showing an accurate instant of it. */
@media (prefers-reduced-motion: reduce) {
    .rz-hb__glow,
    .rz-hb__burst,
    .rz-hb__ping,
    .rz-hb__arc,
    .rz-hb__sweep,
    .rz-hb__hand,
    .rz-hb__heart,
    .rz-hb__tag {
        animation: none;
    }

    .rz-hb__glow {
        opacity: 0.75;
    }

    .rz-hb__burst {
        /* 46% of the way along, which is mid-nerve. */
        stroke-dashoffset: -46;
        opacity: 1;
    }

    .rz-hb__ping {
        transform: scale(1.7);
        opacity: 0.55;
    }

    /* 30% of the way round, which is inside systole: the red arc has drawn
       376.99 × 0.3 and the blue has not started. Head at 108°. */
    .rz-hb__sweep--sys {
        stroke-dashoffset: 264;
    }

    .rz-hb__sweep--dia {
        stroke-dashoffset: 376.99;
    }

    .rz-hb__hand {
        transform: translate(160px, 208px) rotate(108deg);
    }

    .rz-hb__heart {
        transform: scale(0.88);
    }

    .rz-hb__tag--dia {
        opacity: 0.32;
    }
}

/* ── The swap machinery ──
   The word cloud rewrites the landmark's own heading and paragraph as the
   reader moves over it, so those two lines have to hold still while their
   contents change. `.rz-scene__copy` is `align-content: center`, which means a
   line that changed height would not merely resize — it would move the whole
   column under the pointer mid-gesture.

   Each swapped line therefore keeps a hidden ghost of its longest possible
   contents in the same grid cell, with the live text laid over it. The cell is
   then as tall as the tallest candidate **at every column width**, which is the
   thing a `min-height` in `lh` gets wrong: the right number of lines is
   different at 264px and at 416px. Measured before this existed: the paragraph
   swung 66px → 133px as the pointer crossed the cloud.

   The heading's ghost is the longest *word* in the cloud, computed in
   reality-zoom.js — so a new word longer than "Phenomenological Control" widens
   the reservation on its own. Same trick, and the same reason, as the Scholar
   button stacking its five states in one cell. */
.rz-swap {
    display: grid;
}

.rz-swap__ghost,
.rz-swap__live {
    grid-area: 1 / 1;
}

/* The heading's cell is two lines tall, because the longest word in the cloud
   wraps to two and the reservation has to cover it — but most words, and the
   resting title, are one. Centring the live line in the cell splits that slack
   above and below instead of leaving it all underneath, where it read as a hole
   between the heading and the paragraph. Grid items stretch by default, so this
   has to be `align-self` on the item; `align-content` on the container does
   nothing here, the track being exactly as tall as its tallest item.
   The paragraph is deliberately *not* centred — prose starts at the top. */
.rz-scene__title .rz-swap__live {
    align-self: center;
}

/* ── The last landmark's copy, tightened ──
   Every candidate the title's ghost reserves against is wider than this
   column, so the cell is two lines tall even when the live line — almost
   always — is one: measured, 57px of dead air the centring above split
   evenly above and below the visible line. Above the title that reads as
   generous leading under the eyebrow; below it, it read as too much air
   between the heading, the paragraph and the cloud beside it, which is what
   was reported. Pushing the live line to the foot of its cell moves that
   same 57px above the title instead of splitting it, so the paragraph and
   the figure sit close under the heading where the reader's eye actually
   lands next. The copy's own gap and the gap to the figure are trimmed to
   match. */
.rz-scene[data-landmark="beyond"] {
    gap: clamp(1.5rem, 3vw, 3rem);
}

.rz-scene[data-landmark="beyond"] .rz-scene__copy {
    gap: 0.65rem;
}

.rz-scene[data-landmark="beyond"] .rz-scene__title .rz-swap__live {
    align-self: end;
}

/* `visibility`, never `display: none` — a ghost that is not laid out reserves
   nothing, which is the whole of its job. */
.rz-swap__ghost {
    visibility: hidden;
}

/* The link under the paragraph it belongs to. At rest it has no `href`, so it is
   neither followable nor a tab stop, but it is laid out all the same.

   A fixed box, not `min-height: 1lh`: the `↗` and `→` fall back to a font with
   taller metrics than the label's, so the line box grew by 1.2px the moment the
   link had any text in it — measured — and the centred column passed that
   straight up into the heading. `overflow: hidden` is the belt to that brace. */
.rz-swap__link {
    justify-self: start;
    height: 1.5em;
    line-height: 1.5;
    overflow: hidden;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-decoration: none;
    color: color-mix(in srgb, var(--rz-accent) 62%, #fff);
    opacity: 0;
    transition: opacity 0.3s ease;
}

.rz-swap__link[href] {
    opacity: 1;
}

.rz-swap__link[href]:hover,
.rz-swap__link[href]:focus-visible {
    color: #fff;
    text-decoration: underline;
    text-underline-offset: 0.25em;
}

/* ── The timeline ──
   A plate that cross-fades, a time axis under it that the reader can walk, and
   a caption naming the station showing. It replaced a four-faced turning prism,
   and the reason was the count: the claim is about a five-century arc, and two
   pairs of instruments cannot carry one.

   ── Its height is fixed, and that is the property to protect ──
   The frame is a fixed aspect ratio, the year and the credit are placed *over*
   the picture, and the caption's two lines are reserved below. The landmarks
   are centred in the stage, so anything here that grew when the station changed
   would shift the whole scene under the reader — the same rule the cloud's cite
   strip and the ask line are built on. A picture arriving at a different shape
   is cropped to 4/3 on the way in rather than changing that number.

   ── What is advancing it lives in reality-zoom.js ──
   The ticker only runs while the scene is `--held`, so the stacked and
   reduced-motion branch (where nothing is ever held) is a manual picker with no
   second code path to write. What is left here is the dressing and one drifting
   glow, and that glow is paused exactly as the cardiac loop is. */
.rz-fig--era {
    max-width: 26rem;
}

/* ── The table the cards are dealt on ──
   A plain box with nothing drawn in it: no border, no background, no padding.
   The dressing belongs to the cards, because each one is a photograph lying on
   a pile — a frame around the pile would be a picture frame with prints
   rattling inside it.

   ── Why these three numbers, and how to redo them ──
   The stage keeps the 4/3 the pictures were cropped to, and the fan has to fit
   inside it exactly. There is one derivation and **the card count is in it**, so
   adding a card means redoing all three:

     card = c (fraction of the stage's width)
     a 4/3 card in a 4/3 stage is also c of the stage's *height* — which is why
     one number describes both axes and the offsets come out equal
     c + (N − 1) · step = 1        → step = (1 − c) / (N − 1)   [of the stage]
     --rz-dx / --rz-dy = step / c                               [of the card]

   At N = 7 and c = 0.66: step = 0.34/6 = 5.67% of the stage, 8.59% of a card.
   `--rz-dx` is trimmed to 8.5% and `--rz-dy` to 8.4% so the pile lands just
   inside rather than exactly on the edge — measured at 1280, the last card's
   right edge reaches 339.9 in a 341.1 box.

   **The step is also the width of the sliver each card leaves showing, and that
   sliver is the only part of a card that can be pointed at**, so it is the thing
   the count trades against. It was c = 0.68 at six cards; dropping to 0.66 buys
   back a little of what the seventh card costs. Measured: 19px at the 1280px
   column, 14px in the narrow desktop column at 920px. Thin, but pointer-only —
   below 900px the deck stops being a fan at all and becomes the contact sheet,
   so **no sliver is ever a touch target** and the 24px floor does not apply
   here. An eighth card would take the narrow column under ~12px, which is the
   point at which this layout should be reconsidered rather than re-tuned.

   The offsets are per-card *transform* percentages, so they resolve against the
   card's own border box rather than the stage's. That is what keeps the maths
   stable when the figure is resized: every card is the same size, so one pair of
   percentages describes the whole fan at every width. */
.rz-era {
    --rz-card: 66%;
    --rz-dx: 8.5%;
    --rz-dy: 8.4%;
    /* One card's worth of stagger. Six cards land in ~0.95s including the last
       one's own travel, which is "fast sequence" without being a flicker. */
    --rz-deal: 0.115s;
    position: relative;
    aspect-ratio: 4 / 3;
    width: 100%;
}

/* ── A card ──
   Its own edge, radius and shadow, because it is a print lying on a pile. The
   shadow is what separates it from the card underneath — with six overlapping
   rectangles of similar tone, the shadow is doing more work here than anywhere
   else in the dive, and flattening it makes the fan read as one striped image.

   `z-index: var(--rz-i)` is the stacking *and* the hit-testing: a card is
   covered by the ones dealt after it, so the only part of it that can take a
   pointer is the sliver still showing — which is exactly the edge the reader is
   aiming at. Nothing computes hit areas. */
.rz-era__card {
    position: absolute;
    top: 0;
    left: 0;
    z-index: var(--rz-i, 0);
    width: var(--rz-card);
    aspect-ratio: 4 / 3;
    padding: 0;
    overflow: hidden;
    border: 1px solid color-mix(in srgb, var(--rz-accent) 28%, transparent);
    border-radius: 0.5rem;
    background:
        radial-gradient(120% 90% at 50% 10%, color-mix(in srgb, var(--rz-accent) 11%, transparent), transparent 72%),
        rgba(8, 8, 11, 0.7);
    box-shadow:
        0 0.9rem 1.8rem rgba(0, 0, 0, 0.55),
        0 0 0 1px rgba(0, 0, 0, 0.35);
    cursor: pointer;
    /* Undealt: the whole pile square on the first card's position, invisible.
       Dealing spreads it. */
    opacity: 0;
    transform: translate(0, 0);
    transition:
        transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
        opacity 0.32s ease,
        box-shadow 0.3s ease,
        filter 0.3s ease;
}

/* Dealt. The stagger is the whole animation and it is `--rz-i` times a step, so
   adding a card costs nothing here. Only set while the deck carries the class —
   without it the delay is zero, which is what makes the gather-up on leaving a
   single movement rather than the deal run backwards. */
.rz-era.is-dealt .rz-era__card {
    opacity: 1;
    transform: translate(calc(var(--rz-i) * var(--rz-dx)), calc(var(--rz-i) * var(--rz-dy)));
    transition-delay: calc(var(--rz-i) * var(--rz-deal));
}

/* Peeled: lifted off the pile and out to the right so the card underneath can
   be seen whole. It keeps its own fan offset and adds to it, so the cards leave
   along the pile's own diagonal rather than collapsing to one place.
   No delay — a peel is one gesture and must answer immediately. */
.rz-era.is-dealt .rz-era__card[data-peeled="true"] {
    opacity: 0.12;
    transform: translate(calc(var(--rz-i) * var(--rz-dx) + 46%), calc(var(--rz-i) * var(--rz-dy) - 7%)) rotate(3.5deg);
    transition-delay: 0s;
    pointer-events: none;
}

/* The card being read. Deliberately **no transform**: the one way to make this
   gesture feel unreliable is to move the thing the reader is trying to look at,
   so the lift is light and shadow only. */
.rz-era.is-dealt .rz-era__card[data-focus="true"] {
    box-shadow:
        0 1.4rem 2.6rem rgba(0, 0, 0, 0.62),
        0 0 0 1px color-mix(in srgb, var(--rz-accent) 45%, transparent);
    filter: brightness(1.05);
}

.rz-era__card:focus-visible {
    outline: 2px solid color-mix(in srgb, var(--rz-accent) 70%, #fff);
    outline-offset: 2px;
}

/* A vignette closing the corners so each print reads as lit rather than
   printed flat. */
.rz-era__card::after {
    content: "";
    position: absolute;
    inset: 0;
    background: radial-gradient(128% 108% at 50% 44%, transparent 56%, rgba(4, 5, 9, 0.5) 100%);
    pointer-events: none;
}

.rz-era__img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Warm at the old end of the run and neutral at ours — five centuries in one
       property, and with the years now hidden behind a hover it is the only
       thing saying at a glance that the pile runs from old to new. */
    filter: sepia(calc((1 - var(--rz-plate-t)) * 55%)) saturate(0.92) contrast(1.04);
}

.rz-era__card--drawn {
    display: grid;
    place-items: center;
    background: radial-gradient(72% 72% at 50% 46%, color-mix(in srgb, var(--rz-accent) 10%, transparent), transparent 74%);
}

/* ── The legend ──
   Two reserved lines, filled only for the card being pointed at and otherwise
   describing the one on top. Its height is fixed whether or not either line has
   anything in it — the landmarks are centred in a stage that clips, so a strip
   that grew when the reader pointed at something would shift the whole scene
   under them mid-gesture. Same rule as the cloud's cite strip. */
.rz-era__legend {
    display: grid;
    gap: 0.15rem;
    width: 100%;
    margin-top: 0.85rem;
}

.rz-era__legend-title {
    margin: 0;
    min-height: 1lh;
    overflow: hidden;
    font-family: Georgia, "Times New Roman", serif;
    font-size: 0.95rem;
    line-height: 1.3;
    white-space: nowrap;
    text-overflow: ellipsis;
    color: #f4f2ee;
}

.rz-era__legend-meta {
    margin: 0;
    min-height: 1lh;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.7rem;
    line-height: 1.4;
    letter-spacing: 0.02em;
    color: color-mix(in srgb, var(--rz-accent) 45%, #fff);
}

/* ── The drawings ──
   Line art on a 200×200 grid, stroked in the landmark's accent. They are the
   fallback a card shows if it is ever given no picture; every card has one
   today, so nothing renders these. See ERA_ARTS in reality-zoom.js for why they
   are drawn rather than sourced, and why they stay. */
.rz-era__art {
    width: min(11rem, 50%);
    height: auto;
    fill: none;
    stroke: var(--rz-accent);
    stroke-width: 3.2;
    stroke-linecap: round;
    stroke-linejoin: round;
    overflow: visible;
}

.rz-era__solid {
    fill: var(--rz-accent);
    stroke: none;
}

/* The part of a drawing that is scaffolding rather than the instrument. */
.rz-era__faint {
    opacity: 0.35;
}

.rz-era__trace {
    stroke-width: 2.4;
    opacity: 0.7;
}

/* ── The rail ──
   Once the section is black this is its only navigation, so it stays visible
   rather than appearing on hover.

   Its presence is `--rz-rail-in`, written every scroll frame, and not the veil:
   it comes up during the dive while there is still eye on screen, and goes down
   late in the pull-back, after the eye is back. See RAIL_* in reality-zoom.js.
   No transition on the opacity — the value is already a scroll position, and
   easing it would only make it lag the scroll. */
.rz-rail {
    position: absolute;
    z-index: 6;
    right: clamp(0.9rem, 2.4vw, 2.2rem);
    top: 50%;
    translate: 0 -50%;
    display: grid;
    gap: 1.4rem;
    justify-items: end;
    width: max-content;
    opacity: var(--rz-rail-in, 0);
    /* It is now up while the painting is still behind it, and the painting is
       a bright sky. Cheap, and it costs nothing once the veil has closed. */
    filter: drop-shadow(0 1px 10px rgba(0, 0, 0, 0.75));
    /* Never on the container. It is as wide as its widest label and sits over
       the middle of the stage, so making the box itself clickable would put an
       invisible sheet across the landmark's own controls. */
    pointer-events: none;
}

/* `.rz--rail`, not `.rz--dark`: the rail outlives the dark at both ends now. */
.rz--rail .rz-rail__dot {
    pointer-events: auto;
}

/* One equal row per landmark, each dot centred in its own — see the note in
   reality-zoom.js. The line and its fill are the two pseudo-elements. */
.rz-rail__track {
    --rz-dot: 0.62rem;
    position: relative;
    display: grid;
    grid-template-rows: repeat(var(--rz-count, 4), 1fr);
    align-items: center;
    justify-items: end;
    height: min(46vh, 330px);
}

.rz-rail__track::before,
.rz-rail__track::after {
    content: "";
    position: absolute;
    top: 0;
    right: calc(var(--rz-dot) / 2);
    width: 2px;
    translate: 50% 0;
    border-radius: 999px;
}

.rz-rail__track::before {
    bottom: 0;
    background: rgba(255, 255, 255, 0.14);
}

/* The travelled part. Height is written every scroll frame, so it reads as a
   continuous position rather than stepping from dot to dot. */
.rz-rail__track::after {
    height: calc(var(--rz-fill, 0) * 100%);
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.22), var(--rz-accent, rgba(255, 255, 255, 0.8)));
    box-shadow: 0 0 10px color-mix(in srgb, var(--rz-accent, #fff) 45%, transparent);
    transition:
        background 0.45s ease,
        box-shadow 0.45s ease;
}

.rz-rail__dot {
    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
    color: rgba(255, 255, 255, 0.45);
}

.rz-rail__dot::after {
    content: "";
    flex: none;
    width: var(--rz-dot, 0.62rem);
    height: var(--rz-dot, 0.62rem);
    border: 1px solid currentColor;
    border-radius: 50%;
    /* Opaque, not transparent: the line runs behind these and a hollow dot
       would read as a bead threaded on it rather than a stop along it. */
    background: #06070b;
    transition:
        background 0.25s ease,
        scale 0.25s ease;
}

.rz-rail__label {
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 0.64rem;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    white-space: nowrap;
    opacity: 0;
    translate: 0.4rem 0;
    transition:
        opacity 0.25s ease,
        translate 0.25s ease;
}

.rz-rail__dot:hover,
.rz-rail__dot:focus-visible {
    color: rgba(255, 255, 255, 0.9);
}

.rz-rail__dot:hover .rz-rail__label,
.rz-rail__dot:focus-visible .rz-rail__label,
.rz-rail__dot--active .rz-rail__label {
    opacity: 1;
    translate: 0 0;
}

.rz-rail__dot--active {
    color: var(--rz-accent);
}

.rz-rail__dot--active::after {
    background: var(--rz-accent);
    scale: 1.15;
}

/* The rail ended in a button through to the other tab. It has gone: the
   standing FAB says the same thing and says it for the whole section, where
   this one only existed once the dive had gone dark. */

/* The nav bar is fixed over the top of the stage. A cream band across a black
   cinematic section reads as a mistake, so it borrows the stage's palette for
   as long as the zoom owns the screen. */
#main-page.main-page--dark-zoom nav {
    background: rgba(8, 9, 13, 0.72);
    backdrop-filter: blur(12px);
    border-bottom-color: rgba(255, 255, 255, 0.12);
}

#main-page.main-page--dark-zoom nav .nav-brand,
#main-page.main-page--dark-zoom nav a:not(.active) {
    color: rgba(255, 255, 255, 0.82);
}

#main-page.main-page--dark-zoom nav .nav-logo,
#main-page.main-page--dark-zoom nav .nav-logo--sussex {
    filter: invert(1) brightness(1.7);
}

nav,
nav .nav-brand,
nav .nav-logo,
nav .nav-logo--sussex {
    transition:
        background 0.35s ease,
        color 0.35s ease,
        filter 0.35s ease,
        border-color 0.35s ease;
}

/* ── The gate ──
   The zoom costs the reader ~700vh of scrolling, so it is opt-in. Nothing here
   resizes anything: locked, the stage is the same full screen of the eye it is
   once open, and the module simply takes the track's travel away so the page
   carries it past like any other section. All that is added is the overlay —
   opening the zoom removes it and changes nothing else on screen.

   The rest is hidden only because it has nothing to say at rest; the veil, the
   question and the landmarks are all driven to zero anyway. */
.rz:not(.rz--unlocked) .rz-scenes,
.rz:not(.rz--unlocked) .rz-rail,
.rz:not(.rz--unlocked) .rz-hint {
    display: none;
}

/* The overlay *is* the button — the whole stage, so anywhere on the section
   opens the zoom. It stays light: rather than dimming the painting to carry the
   text, only the middle is softened, and the line itself does the work with its
   own shadow. */
.rz-gate {
    position: absolute;
    inset: 0;
    z-index: 5;
    display: grid;
    place-items: center;
    width: 100%;
    padding: 2rem var(--content-inline-gutter);
    border: none;
    text-align: center;
    background: radial-gradient(46% 34% at 50% 50%, rgba(6, 7, 11, 0.44) 0%, rgba(6, 7, 11, 0.16) 62%, rgba(6, 7, 11, 0) 100%);
    cursor: pointer;
    appearance: none;
}

.rz--unlocked .rz-gate {
    display: none;
}

.rz-gate__label {
    font-family: Georgia, "Times New Roman", serif;
    font-size: clamp(1.25rem, 2.5vw, 2.05rem);
    font-weight: 400;
    letter-spacing: 0.01em;
    color: #fff;
    text-shadow:
        0 2px 18px rgba(0, 0, 0, 0.92),
        0 0 55px rgba(0, 0, 0, 0.75);
    animation: rz-breathe 3.6s ease-in-out infinite;
    /* `scale` rather than a transform, so the hover step and any transform the
       label might grow later cannot overwrite each other. */
    scale: 1;
    transition:
        scale 0.5s cubic-bezier(0.22, 1, 0.36, 1),
        letter-spacing 0.5s cubic-bezier(0.22, 1, 0.36, 1),
        opacity 0.4s ease,
        text-shadow 0.5s ease;
}

@keyframes rz-breathe {
    0%,
    100% {
        opacity: 0.5;
    }
    50% {
        opacity: 1;
    }
}

/* Settle it while the pointer is on the *text*, not anywhere in the button —
   the button is the whole 100vh stage, and with the section this tall the
   pointer is "over" it for practically the entire time it is on screen. Hover
   has to be scoped to the label's own small box or the animation would settle
   immediately and never pulse again. Keyboard focus has no such box to aim at,
   so it keeps the whole button. */
.rz-gate__label:hover,
.rz-gate:focus-visible .rz-gate__label {
    animation: none;
    opacity: 1;
    scale: 1.07;
    letter-spacing: 0.045em;
    text-shadow:
        0 2px 18px rgba(0, 0, 0, 0.95),
        0 0 70px rgba(0, 0, 0, 0.85),
        0 0 26px rgba(255, 255, 255, 0.18);
}

/* Pressing it starts the dive; the label ducks first. */
.rz-gate:active .rz-gate__label {
    scale: 1.01;
    transition-duration: 0.12s;
}

/* Shown by the module rather than by the reader arriving at it, so it comes up
   under its own steam — after the pull-back, at the end of a shot the reader is
   still watching. */
.rz-gate {
    animation: rz-gate-in 0.6s ease both;
}

@keyframes rz-gate-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

@media (prefers-reduced-motion: reduce) {
    .rz-gate {
        animation: none;
    }

    .rz-gate__label {
        animation: none;
        opacity: 1;
        transition: none;
    }

    .rz-gate__label:hover,
    .rz-gate:focus-visible .rz-gate__label {
        scale: 1;
        letter-spacing: 0.045em;
    }

    /* The deck. Nothing here ever turns — this preference also selects the
       stacked branch, where `paint()` never runs, so no landmark is ever
       `--held` and the ticker never advances. The contact-sheet layout that
       covers that is `.rz--static .rz-era` below; all this has to do is make
       sure nothing animates on the way into it. */
    .rz-era__card,
    .rz-era__img,
    .rz-era__art {
        transition: none;
    }

    /* Nothing is ever `--held` here, so the paragraphs' reveal has to be undone
       or the opening landmark is four invisible lines. The beat's own margin
       stays: it is space, not motion. */
    .rz-scene--film .rz-scene__text {
        opacity: 1;
        translate: 0 0;
        transition: none;
    }
}

/* Puts the overlay back. Centred at the foot of the stage, where the eye is
   already looking for the next scroll, and only there once the zoom is open.
   The hint above it is moved up to clear this. */
.rz-close {
    position: absolute;
    z-index: 7;
    bottom: 1.9rem;
    left: 50%;
    translate: -50% 0;
    display: none;
    place-items: center;
    width: 3.3rem;
    height: 3.3rem;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 50%;
    background: rgba(8, 10, 16, 0.4);
    backdrop-filter: blur(8px);
    font-size: 1.1rem;
    line-height: 1;
    color: rgba(255, 255, 255, 0.65);
    cursor: pointer;
    opacity: 0.62;
    transition:
        opacity 0.25s ease,
        background 0.25s ease,
        border-color 0.25s ease,
        color 0.25s ease;
}

.rz--unlocked .rz-close {
    display: grid;
}

.rz-close:hover,
.rz-close:focus-visible {
    opacity: 1;
    background: rgba(255, 255, 255, 0.14);
    border-color: rgba(255, 255, 255, 0.5);
    color: #fff;
}

/* At the sidebar breakpoint .research-full takes the sidebar as padding so its
   header and card tab stay aligned with every other section. The stage is the
   exception: it has to bleed back across that padding or the black stops 14rem
   short of the screen edge for the whole zoom. The section's overflow-x clips
   at its padding box — which is its border box, since there is no border — so
   the negative margin survives. */
@media (min-width: 1800px) {
    .rz-stage {
        margin-left: -14rem;
        width: calc(100% + 14rem);
    }
}

/* ── Static fallback ──
   Reduced motion, and anything too narrow for a two-column stage held at 100vh.
   The stage stops being sticky, which is also the signal the module reads to
   stand down — it checks the computed position rather than re-testing the media
   queries, so this block is the single place the mode is decided. */
@media (max-width: 900px), (prefers-reduced-motion: reduce) {
    .rz {
        --rz-mode: stack;
    }

    /* ── The still, not the loop ──
       It has to be **this** query and not the reduced-motion one next to it:
       this is the single place the mode is decided, and reduced motion is only
       one of its two halves — put here first, a 375px phone with animation
       switched on still took the loop. Only the winning `background-image` is
       ever fetched, so naming both costs one request either way, and this
       branch pays 13 KB instead of 687.

       It is also more than the branch used to get: the old `<video>` was gated
       on `--rz-mode` in script, so a phone and a reduced-motion reader had no
       film of any kind and this landmark opened on nothing but its paragraphs.

       `--rz-film-solo` is never written here, since `paint()` does not run, so
       the picture takes `.rz-scene--film`'s own default of 0 — the settled
       value the copy's contrast is measured against. */
    .rz-scene__video {
        background-image: var(--rz-film-still);
    }

    /* The film box is absolute, and stacked there is nothing under it to
       resolve against: `.rz-scene` gives up its `transform` and `filter` here,
       so both containing blocks go with them and the box climbed all the way to
       `.rz-track`. Measured on a 375px phone — the picture landed at y=2406
       while its own landmark was at y=379, i.e. floating behind whichever
       station happens to be half way down the stack. Restoring a containing
       block on the landmark itself is the whole fix; every offset in the base
       rule is already written against it. */
    .rz-scene--film {
        position: relative;
    }

    .rz-track {
        height: auto !important;
    }

    .rz-stage {
        position: static;
        height: auto;
        display: block;
        padding: 0 0 3rem;
        /* The base hides overflow to clip the zoomed eye. There is no zoom
           here, and leaving it on makes the stage its own scrollport — which
           silently stops .rz-close from sticking to the foot of the screen. */
        overflow: visible;
    }

    /* .rz-gate is placed against the stage, which is unpositioned here; without
       this it would resolve against #main-page and cover the whole page. */
    .rz:not(.rz--unlocked) .rz-stage {
        position: relative;
    }

    /* No rail here, so this is the only way out other than scrolling. Sticky
       against the foot of the viewport rather than absolute against a stage
       that is now thousands of pixels tall, so it stays with the reader all the
       way down the stack — same place on screen as in the zoom. */
    .rz--unlocked .rz-close {
        position: sticky;
        bottom: 1.9rem;
        left: auto;
        translate: none;
        margin: 0 auto 1rem;
        opacity: 0.85;
    }

    .rz-zoom {
        position: relative;
        inset: auto;
        display: block;
        aspect-ratio: 2000 / 1345;
        max-height: 52vh;
        overflow: hidden;
        transform: none;
    }

    .rz-eye {
        position: static;
        width: 100%;
        height: 100%;
        object-fit: cover;
    }

    /* .rz-depth is absolute against the stage; with the stage unpositioned it
       would resolve against #main-page and wash the whole page. */
    .rz-pupil,
    .rz-veil,
    .rz-depth,
    .rz-hint {
        display: none;
    }

    .rz-question {
        padding: 2.6rem var(--content-inline-gutter) 0;
        opacity: 1;
        transform: none;
    }

    .rz-scenes {
        position: static;
        display: grid;
        gap: 4rem;
        padding: 3rem 0 0;
        pointer-events: auto;
        /* All three fight the same thing: stacked, this grid and its column
           both shrink-wrap the landmarks instead of taking the stage's width,
           which left them 495px wide inside an 805px stage. Spell out the
           width, the column, and the stretch rather than relying on any of
           them being inferred — .rz-scene then takes its gutter as padding
           instead of out of a percentage of a column that is sizing to it. */
        width: 100%;
        justify-items: stretch;
        grid-template-columns: minmax(0, 1fr);
    }

    .rz-scene {
        grid-area: auto;
        grid-template-columns: minmax(0, 1fr);
        gap: 2rem;
        width: 100%;
        max-width: var(--content-max-width);
        padding: 0 var(--content-inline-gutter);
        margin: 0 auto;
        opacity: 1;
        transform: none;
        filter: none;
        pointer-events: auto;
    }

    .rz-scene:nth-child(even) .rz-scene__figure {
        order: 0;
    }

    .rz-cloud__glow {
        opacity: 1;
    }

    .rz-cloud__word {
        opacity: var(--rz-word-opacity);
    }


    /* ── The deck, as a contact sheet ──
       Nothing is ever `--held` in this branch, because paint() never runs — so
       the ticker in reality-zoom.js never advances and the deck never turns.
       **That used to need no layout and now does.** While there was a dot axis
       under the cards, a reader who got no animation could still walk the run
       by hand; with the axis gone they would be left looking at the first card
       — one Bosch — for ever, and would never reach the photograph the whole
       sequence exists to arrive at.

       So the fan is unwound into a flat grid and every card is shown at once.
       It is the honest still of a deal: the same pictures in the same order,
       nothing to wait for and nothing overlapping. It also lets this branch be
       taller than the zoom's, which it can afford — below 900px the stage is
       not 100vh and the landmark scrolls.

       **The cards are still buttons here, and that is what makes the legend
       work without a hover.** A tap fires `click`, `focus()` runs, and the strip
       underneath names the picture — so a phone reader gets the attributions
       the fan gives a mouse, by the one gesture they have.

       `auto-fit` with a floor rather than a fixed column count, so a phone gets
       two across and a reduced-motion desktop gets three or more, with no
       breakpoint here that has to be kept in step with anything.

       A definite width, and `100%` is not one here. In this branch `.rz-scene`
       is a single auto-sized grid track, so a percentage width on anything
       inside it resolves against an indefinite basis and the track falls back
       to the content's own max-content. */
    .rz-fig--era {
        width: min(26rem, 100%);
    }

    .rz--static .rz-era {
        aspect-ratio: auto;
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
        gap: 0.5rem;
    }

    /* Back into flow, un-fanned and fully visible. The aspect-ratio is what the
       absolute positioning used to give them; without it a grid item with no
       intrinsic height collapses. `opacity: 1` is the load-bearing one — the
       deal never fires in this branch, so without it every card would sit at
       the undealt `opacity: 0` and the figure would be blank.

       `transition: none` is required, not tidiness: without it, crossing into
       this branch (a resize past 900px, a phone turned on its side) animates
       six cards out of the fan, which is a deal nobody asked for on a layout
       whose entire point is that nothing moves. */
    .rz--static .rz-era__card {
        position: static;
        width: auto;
        aspect-ratio: 4 / 3;
        opacity: 1;
        transform: none;
        transition: none;
        border-radius: 0.5rem;
        box-shadow: 0 0.5rem 1.2rem rgba(0, 0, 0, 0.45);
    }

    /* Nothing is peeled or lifted when nothing overlaps. */
    .rz--static .rz-era__card[data-peeled="true"] {
        opacity: 1;
        transform: none;
        pointer-events: auto;
    }

    .rz-rail {
        display: none;
    }

    #main-page.main-page--dark-zoom nav {
        background: #f5f4ef;
        backdrop-filter: none;
    }
}

