/* journey/site.css — the journey layer's page styles: the right-side
   navigator + its site-map panel, chapter copy blocks, hotspot proxies,
   detail card / bottom sheet, focus treatments, touch targets. None of these
   elements exist until journey.js boots (journey/ui.js and rail.js build
   them), so every rule here is dead CSS until then — the hero's first paint
   is untouched by construction. Moved verbatim from index.html's inline
   journey <style> at the M5 shell step. Linked AFTER hero.css; keep that
   order. */

/* journey-v6: the scene is a non-interactive backdrop here — the journey
   owns scroll and pointer gestures via the organism's input policy
   (organism.js setInputPolicy; ?free=1 keeps the hero's free camera). */
body:not(.free-cam) #stage canvas { cursor: default; }
body:not(.free-cam) #stage canvas:active { cursor: default; }

  /* ============================================================
     JOURNEY GREY-BOX (W3-A) — chapter nav, chapter copy, detail
     cards, hotspot proxies.

     NONE of these elements exist until journey.js boots (after the
     hero entry choreography); journey/ui.js builds them. So the hero's
     first paint, TTI and Mission screenshot are untouched by
     construction, and every rule below is dead CSS until then.
     Everything starts at opacity 0 and is driven by journey
     progress — at p = 0 the page is the hero, exactly.
     ============================================================ */

  /* ============================================================
     THE RIGHT-SIDE NAVIGATOR (Hannah, 2026-08-09 redux) —
     journey/rail.js. First built 2026-08-07 on the left flank;
     the redux moves it to the right, gives the resting state TWO
     symbols (current section + menu), and folds the old footer's
     whole job into the menu panel.

     ---- THE LAYOUT DECISION: ANCHORED ON THE CURRENT MARK ----

     The control is a fixed instrument: the current section's mark
     sits at the vertical centre of the right edge at every chapter,
     with the menu mark directly below it. Expanding does not move
     either the visitor's pointer target or the control's home —
     the OTHER sections' marks fan out from behind the current one
     to their manifest positions (slot i at (--i - --cur) tiles from
     the anchor), and the menu mark slides down to hold the foot of
     the stack. Collapse folds them back in. The geometry is three
     custom properties (--i per slot, --cur / --n on the root), so
     the whole choreography is authored here and rail.js only states
     where the visitor is.

     The detail card also stands on this flank (`.j-card`, right:
     3.4vw). While a modal detail is open the rail is already inert
     (rail.js update); `.dim` is the visual half of that statement.

     ---- THE MOTION FAMILY ----

     The gestures are the hero's `.co` callout boot (hero.css), the
     same family the node popovers took in d1ecc23:

       hero `co-on` / `tag-in`   marks slide out on a per-distance
                                 delay (--d); a pointed-at name
                                 arrives from a wider letter-spacing
                                 exactly as `.co .tag` does.
       hero `.co .ck`            the current section wears the
                                 reticle: four corner brackets
                                 locking in clockwise with the same
                                 overshoot curve.
       hero `lead-draw`          the menu mark's filaments draw
                                 themselves as the rail first
                                 reveals, and stay drawn.

     Every rule ends at the element's resting style, so the
     reduced-motion block at the foot of this file switches the
     transitions off and each state is simply already there.
     ============================================================ */
  .j-rail {
    position: fixed;
    right: 0;
    top: 0; bottom: 0;
    z-index: 4;                 /* over copy + hotspots, under the card */
    /* Centred by FLEX, not by `top:50%` + `translateY(-50%)`: a transform on
       an ancestor becomes the containing block for `position: fixed`
       descendants (the static tier's twin holds its menu panel INSIDE the
       rail, where the transform version clipped it). Same layout, no trap.
       It is also a real <nav> landmark, in reach of the hero's BARE `nav {}`
       rule (hero.css: flex + padding + the `fade-on` entry animation).
       `animation: none` is the load-bearing reset — fill-mode `both` would
       pin opacity at the animation's END value and light the rail at the
       Mission pose, moving the reference frame. */
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding: 0;
    animation: none;
    /* The container is inert to the pointer; only the tiles are live. That is
       what keeps the resting state honest — two 44px symbols are the only
       things on the flank that answer a pointer, not a full-height column. */
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.55s ease;
  }
  /* `.on` is the first-travel reveal (latched — see rail.js). `:focus-within`
     overrides it unconditionally: at the Mission pose the rail is dark but NOT
     inert, so the first Tab brings it up rather than landing on nothing. The
     captured Mission frame has no focus, so it is unaffected. */
  .j-rail.on,
  .j-rail:focus-within { opacity: 1; }
  /* A modal detail owns the frame — and this same flank. The rail is already
     inert (rail.js); this is the picture agreeing with the hit model. */
  .j-rail.on.dim { opacity: 0.1; }

  /* The anchor: a zero-height point at the vertical centre of the right
     edge. Every slot and the menu mark position themselves off it. */
  .j-rail-inner {
    position: relative;
    width: 52px;
    height: 0;
  }
  /* A local scrim so marks and names stay legible over a bright chapter
     (Connect's lit ground, Owned's colony). It exists only while the rail is
     expanded, and it is sized by the same custom properties that place the
     slots, so it always covers exactly the fanned stack. */
  .j-rail-inner::before {
    content: "";
    position: absolute;
    right: 0;
    width: 6rem;
    /* the 2.4rem overhangs are the FEATHER'S ROOM: the mask fades over a
       fixed 34px and needs the box to extend past the first and last tile */
    top: calc(var(--cur, 0) * -44px - 22px - 2.4rem);
    height: calc(var(--n, 5) * 44px + 52px + 4.8rem);
    z-index: -1;
    opacity: 0;
    /* Quieter than the first build's 0.95/58%: the names carry their own
       scrim pills now, so this band's only job is the MARKS' legibility over
       a bright chapter — solid early, then a long falloff, so it reads as
       the frame darkening at the edge rather than as a docked panel. */
    background: linear-gradient(to left, rgba(9, 6, 2, 0.82) 22%, rgba(9, 6, 2, 0));
    /* horizontal falloff is the gradient; vertical falloff is a mask, because
       a scrim with hard top and bottom edges reads as a PANEL laid over the
       composition, and this should read as the frame simply getting darker
       where the rail is. */
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 34px, #000 calc(100% - 34px), transparent);
    mask-image: linear-gradient(to bottom, transparent, #000 34px, #000 calc(100% - 34px), transparent);
    transition: opacity 0.35s ease;
  }
  /* THE RESTING HALO (Hannah, 2026-08-09: "sometimes the side menu thing is
     invisible … it's on top of a similarly coloured mushroom"). The two
     resting marks are gold line-work over a scene made of gold line-work; on
     the epilogue's lit field the current mark simply vanished. Same recipe as
     the expanded band above — a gradient falloff with a masked feather, the
     frame darkening at its edge, NOT a card — but sized to the two resting
     marks alone and a shade quieter. It fades out as the expanded band fades
     in, so the darkening simply grows with the fan rather than doubling
     under it. (The marks additionally carry their own dark rim — see the
     .j-sym filter below — which is what keeps the glyph itself crisp where
     even the halo sits on bright ground.) */
  .j-rail-inner::after {
    content: "";
    position: absolute;
    right: 0;
    z-index: -1;
    width: 4.6rem;
    /* the resting pair: current mark at -22..22, menu mark at 30..74, plus
       the same 2.4rem feather room the expanded band reserves */
    top: calc(-22px - 2.4rem);
    height: calc(96px + 4.8rem);
    background: linear-gradient(to left, rgba(9, 6, 2, 0.62) 20%, rgba(9, 6, 2, 0));
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 38px, #000 calc(100% - 38px), transparent);
    mask-image: linear-gradient(to bottom, transparent, #000 38px, #000 calc(100% - 38px), transparent);
    opacity: 0;
    transition: opacity 0.35s ease;
  }
  .j-rail.on .j-rail-inner::after { opacity: 1; }
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-inner::after { opacity: 0; }

  .j-rail-list { display: block; margin: 0; padding: 0; list-style: none; }

  /* THE MOON'S ARC IS OFF BY DEFAULT, and that is a fail-closed rule rather
     than tidiness. rail.js builds the `<svg class="j-rail-arc">` for every
     rail, because the element cannot know which geometry the media query will
     hand it; every rule that gives it a stroke, a size and its dash lives
     inside the half-moon block, under `(hover: hover)`. Outside that block the
     two paths are an unstyled inline SVG — `fill: rgb(0,0,0)`, `stroke: none`
     — and MEASURED at 375x812 with touch emulation on, that is exactly what
     they painted: a solid black wedge, 52x52, sitting on the column beside the
     marks. Hidden here, shown by the geometry that owns it. */
  .j-rail-arc { display: none; }

  /* ---- the fan ----
     Collapsed, every slot sits AT the anchor, stacked behind the current one
     (opacity 0, hit area off). Expanded, each slides to (--i - --cur) tiles
     from the anchor — so the current mark, which is the thing the pointer is
     on when the fan opens, does not move by a pixel in either direction. */
  .j-rail-slot {
    position: absolute;
    top: -22px; right: 0;
    opacity: 0;
    transform: translateY(0);
    transition: transform 0.34s cubic-bezier(0.22, 1, 0.36, 1),
                opacity 0.3s ease;
    /* the callout boot's cascade, nearest first — distance, not list order */
    transition-delay: calc(var(--d, 0) * 0.035s);
  }
  .j-rail-slot.now { opacity: 1; }

  .j-rail-item {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    /* 44px tall (PL-1.4) and the tiles ABUT when fanned: a pointer travelling
       from the resting symbol to a neighbour never crosses dead space, so the
       expansion cannot collapse under a moving cursor. */
    width: 52px;
    height: 44px;
    color: rgba(201, 191, 168, 0.52);
    text-decoration: none;
    transition: color 0.3s;
  }
  a.j-rail-item, .j-rail-menu { pointer-events: none; }
  .j-rail.on a.j-rail-item,
  .j-rail.on .j-rail-menu,
  .j-rail:focus-within a.j-rail-item,
  .j-rail:focus-within .j-rail-menu { pointer-events: auto; }
  /* …but at rest only the CURRENT tile is live in the stack; the rest are
     folded behind it and take their hit areas with them. The negation is the
     exact complement of the expanded-state `:is()` below — `:has(:focus-
     visible)`, not `:focus-within`, so a click-focused link neither keeps
     folded tiles hit-testable nor holds the fan (keyboard focus does both,
     deliberately). */
  .j-rail:not(.j-rail-hot):not(.j-rail-open):not(:has(:focus-visible))
    .j-rail-slot:not(.now) .j-rail-item { pointer-events: none; }

  .j-rail-mark {
    position: relative;
    flex: 0 0 auto;
    display: block;
    width: 24px; height: 24px;
  }
  /* 24px in a 22-unit box: a shade over 1:1, so a unit of symbol geometry is
     about a pixel and the smallest authored dot (r 0.6) still rasterises as a
     dot. See the radius note in journey/symbols.js.
     The dark rim is the marks carrying their own contrast (the other half of
     the resting-halo note above): a tight dark halo behind every stroke, so
     gold line-work stays a drawn glyph even directly over the scene's own
     gold line-work. */
  .j-sym {
    display: block; width: 24px; height: 24px; overflow: visible;
    transform: scaleX(var(--j-sym-flip, 1));
    filter: drop-shadow(0 0 1px rgba(9, 6, 2, 0.95)) drop-shadow(0 0 3px rgba(9, 6, 2, 0.6));
  }
  .j-sym path {
    fill: none;
    stroke: currentColor;
    stroke-width: 1;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  /* Anything the scene renders as a lit point — spores, hubs, bodies, the
     crown — is a filled dot here, so the marks read as light on dark rather
     than as outline drawings. */
  .j-sym circle { fill: currentColor; stroke: none; }

  /* The Connect ground-network mark should face into the journey. Mirror only
     that SVG in the live rail: the tile, reticle, label and menu copy keep
     their authored orientation. */
  .j-rail-slot[data-chapter="connect"] .j-rail-mark > .j-sym-connect {
    --j-sym-flip: -1;
    transform-origin: center;
  }
  .j-rail-slot.j-rail-recycle .j-rail-mark > .j-sym {
    pointer-events: none;
  }

  /* ---- the names: revealed per mark, to the LEFT of the stack ----
     "Hovering an individual symbol reveals the name of that section." The
     name is a label, not a target (pointer-events none), carried on its own
     small scrim pill so it stays legible over any chapter without asking the
     whole flank for a panel. */
  .j-rail-name {
    position: absolute;
    right: 100%;
    top: 50%;
    transform: translateY(-50%);
    margin-right: 0.1rem;
    padding: 0.34rem 0.7rem;
    border-radius: 2px;
    background: rgba(9, 6, 2, 0.85);
    font-size: 0.6rem;
    font-weight: 600;
    letter-spacing: 0.3em;
    text-transform: uppercase;
    white-space: nowrap;
    color: var(--parchment);
    pointer-events: none;
    /* the hero's `tag-in`: wider tracking on the way in, settling as it lands */
    opacity: 0;
    transition: opacity 0.24s ease, letter-spacing 0.24s ease;
  }
  /* The name reveals on hover or keyboard focus — but only where hover is
     REAL. A touch tap leaves a STICKY :hover on the tile it pressed
     (Chrome Android keeps it until another element is tapped), so on a
     hoverless device this rule would hold the travelled-to tile's label on
     screen after the fan has collapsed. There the only reveals are the fan
     (.j-rail-open, the deliberate arming tap) and a keyboard
     :focus-visible — a hoverless device with a keyboard still names what
     it focuses (2026-08-19). */
  @media (hover: hover) {
    .j-rail-item:is(:hover, :focus-visible) .j-rail-name,
    .j-rail-menu:is(:hover, :focus-visible) .j-rail-name {
      opacity: 1;
      letter-spacing: 0.24em;
    }
  }
  @media (hover: none) {
    .j-rail-item:focus-visible .j-rail-name,
    .j-rail-menu:focus-visible .j-rail-name {
      opacity: 1;
      letter-spacing: 0.24em;
    }
  }
  /* Touch has no hover to reveal names one at a time, so the deliberate tap
     that opens the fan brings every name with it. */
  .j-rail-open .j-rail-name { opacity: 1; letter-spacing: 0.24em; }

  /* ---- RESTING: the current mark, lit ---- */
  .j-rail-slot.now .j-rail-item { color: rgba(228, 182, 102, 0.82); }
  /* the rim rides under the glow — `filter` replaces, it does not compose */
  .j-rail-slot.now .j-sym {
    filter: drop-shadow(0 0 1px rgba(9, 6, 2, 0.95)) drop-shadow(0 0 3px rgba(9, 6, 2, 0.6))
            drop-shadow(0 0 5px rgba(240, 190, 90, 0.4));
  }

  /* ---- EXPANDED: hover on the section mark, keyboard focus, or the tap ---- */
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-inner::before { opacity: 1; }
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot {
    opacity: 1;
    transform: translateY(calc((var(--i, 0) - var(--cur, 0)) * 44px));
  }
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-item {
    color: rgba(201, 191, 168, 0.62);
  }
  /* the current section, clearly marked — gold, and wearing the reticle */
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot.active .j-rail-item,
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot.now .j-rail-item {
    color: var(--gold-bright);
  }
  /* focus states visually equal hover states (PL-2.2) */
  .j-rail-item:is(:hover, :focus-visible) { color: var(--parchment); }
  .j-rail-slot.active .j-rail-item:is(:hover, :focus-visible),
  .j-rail-slot.now .j-rail-item:is(:hover, :focus-visible) { color: var(--gold-bright); }

  /* the epilogue: a real control with the echo's quieter voice (rail.js,
     THE EPILOGUE) — dimmer in the fan than its peers, but it answers a
     hover and a focus exactly as they do, because it takes a click. */
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-echo:not(.now) .j-rail-item {
    color: rgba(201, 191, 168, 0.42);
  }
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
    .j-rail-echo:not(.now) .j-rail-item:is(:hover, :focus-visible) {
    color: var(--parchment);
  }

  /* ---- the reticle: hero.css's `.co .ck`, on the current symbol ---- */
  /* The brackets frame the drawing without grazing its outer strokes. This
     stays inside the 48px moon tile and changes no hit or layout geometry. */
  .j-rail-ret { position: absolute; inset: -7px; display: block; pointer-events: none; }
  .j-rck {
    position: absolute;
    width: 6px; height: 6px;
    opacity: 0;
    --rck: rgba(240, 200, 119, 0.85);
    transition: opacity 0.14s, transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .j-rck.tl { top: 0;    left: 0;  border-top: 1px solid var(--rck);    border-left: 1px solid var(--rck);  transform: translate(6px, 6px)   scale(0.4); }
  .j-rck.tr { top: 0;    right: 0; border-top: 1px solid var(--rck);    border-right: 1px solid var(--rck); transform: translate(-6px, 6px)  scale(0.4); }
  .j-rck.br { bottom: 0; right: 0; border-bottom: 1px solid var(--rck); border-right: 1px solid var(--rck); transform: translate(-6px, -6px) scale(0.4); }
  .j-rck.bl { bottom: 0; left: 0;  border-bottom: 1px solid var(--rck); border-left: 1px solid var(--rck);  transform: translate(6px, -6px)  scale(0.4); }
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot.active .j-rck {
    opacity: 1;
    transform: translate(0, 0) scale(1);
    transition: opacity 0.12s cubic-bezier(0.22, 1, 0.36, 1) calc(var(--d, 0) * 0.035s + 0.1s),
                transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) calc(var(--d, 0) * 0.035s + 0.1s);
  }
  /* …locking in clockwise, tl -> tr -> br -> bl, as the hero's do */
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot.active .j-rck.tr { transition-delay: calc(var(--d, 0) * 0.035s + 0.15s); }
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot.active .j-rck.br { transition-delay: calc(var(--d, 0) * 0.035s + 0.20s); }
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot.active .j-rck.bl { transition-delay: calc(var(--d, 0) * 0.035s + 0.25s); }

  /* ---- the menu control: the SECOND resting symbol ----
     Beside the current mark at rest (one tile below, past a hairline), and
     holding the foot of the stack while the fan is open. A bare glyph, a
     shade dimmer than the current mark — present, not loud. */
  .j-rail-menu {
    position: absolute;
    top: -22px; right: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 52px;
    height: 44px;
    padding: 0;
    background: none;
    border: 0;
    color: rgba(217, 164, 65, 0.6);
    font: inherit;
    cursor: pointer;
    transform: translateY(52px);
    transition: transform 0.34s cubic-bezier(0.22, 1, 0.36, 1), color 0.3s;
  }
  .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-menu {
    transform: translateY(calc((var(--n, 5) - 1 - var(--cur, 0)) * 44px + 52px));
  }
  /* the hairline between the two jobs: sections above, the site's own
     contents below. It rides the menu mark through both states. */
  .j-rail-menu::before {
    content: "";
    position: absolute;
    top: -4px;
    left: 50%;
    width: 24px;
    height: 1px;
    transform: translateX(-50%);
    background: rgba(217, 164, 65, 0.26);
  }
  /* …and the hairline's 8px of breathing room is bridged for the POINTER: a
     hit pad riding the button, so travelling from the stack (or the resting
     mark) down past the hairline never crosses dead space — dead space under
     a moving pointer is a pointerleave, and a pointerleave folds the fan
     (measured 2026-08-09, the Epilogue report). Pseudo-elements hit-test as
     part of their element, so this costs no extra node and follows the
     button through both states. */
  .j-rail-menu::after {
    content: "";
    position: absolute;
    top: -8px;
    left: 0;
    right: 0;
    height: 8px;
  }
  /* Touch browsers can retain :hover after a tap. Keep persistent emphasis
     tied to keyboard focus and add hover only for a real hover pointer. */
  .j-rail-menu:focus-visible { color: var(--gold-bright); }
  @media (hover: hover) and (pointer: fine) {
    .j-rail-menu:hover { color: var(--gold-bright); }
  }
  /* hero `lead-draw`: the contents filaments draw themselves as the rail
     first reveals, then stay drawn — the instrument powering on. */
  .j-rail-menu .j-sym path {
    stroke-dasharray: 12;
    stroke-dashoffset: 12;
    transition: stroke-dashoffset 0.4s ease 0.3s;
  }
  .j-rail.on .j-rail-menu .j-sym path,
  .j-rail:focus-within .j-rail-menu .j-sym path { stroke-dashoffset: 0; }
  /* ---- …and it says so again when someone stops on it ----
     The MENU pill is gone from the half moon (THE HUB HAS NO PILL, below) and
     this stands in for it: after a second of dwell the three filaments draw
     themselves a second time. Same element, same property, same 12 units of
     dash as the arrival directly above — the gesture and the entry are one
     drawing, not two.

     IT RETRACTS FIRST, AND THAT IS THE WHOLE DIFFERENCE FROM A REPLAY. The
     entry runs 12 -> 0 because it begins from a glyph that is not there yet.
     Replaying it on a glyph that IS there has to put the dash back to 12 to
     start, and `from` is instantaneous: shot at 0.05x, the first frame of that
     candidate is an already-erased mark. That is a BLINK — the exact shape
     `b079f84` went and removed from the popover — so the wind-up is animated
     instead. Nothing on screen ever jumps: the filaments withdraw towards
     their nodes and write back out, and the mark begins and ends drawn.

     RETRACTS TO 8 OF THE 12, NOT ALL OF IT. Shot side by side at 0.05x: at a
     full retract the mark spends ~0.15s as three bare dots and reads as the
     icon having blinked out; at 8 it never stops being a list of three lines
     and the motion still plainly reads as the lines being redrawn. Her word
     is "gently", and that is the difference between the two frames.

     THE STAGGER IS DOING MORE WORK THAN THE DEPTH. Shot with and without:
     drawn together the three lines read as the whole glyph breathing — a
     pulse, and a pulse is a thing asking for attention. Drawn 0.07s apart
     they read as an INDEX being written line by line, which is what the mark
     is (three filaments leaving three nodes, symbols.js). The nodes do not
     move: they are lit points, lit before the gesture and after it, and
     flickering them would be the second gesture on one control that the
     popover pass had to go and unpick.

     The keyframe BEGINS AND ENDS at the resting style — dashoffset 0, the
     drawn glyph — which is this file's contract for anything animated, and is
     why the reduced-motion path is a switch-off rather than a rewrite. rail.js
     arms it after a 1s dwell (THE BUTTON SAYS IT IS A BUTTON) and drops the
     class on animationend, so a fresh hover is what re-arms it. */
  @keyframes j-menu-rewrite {
    0%   { stroke-dashoffset: 0; }
    38%  { stroke-dashoffset: 8; }
    100% { stroke-dashoffset: 0; }
  }
  .j-rail-menu.j-rail-nudge .j-sym path {
    animation: j-menu-rewrite 0.62s ease both;
  }
  .j-rail-menu.j-rail-nudge .j-sym path:nth-of-type(2) { animation-delay: 0.07s; }
  .j-rail-menu.j-rail-nudge .j-sym path:nth-of-type(3) { animation-delay: 0.14s; }

  /* ============================================================
     THE SITE-MAP PANEL — the whole site as a page, from the right.
     Replaces the post-epilogue footer (removed 2026-08-09): chapter
     deep links, per-section contents, social links and the legal line all
     live here now. Content source: see journey/rail.js.
     ============================================================ */
  /* THE HIT MODEL FOR EVERY FULL-FRAME OVERLAY (2026-08-09 — see the poke
     regression note in journey-v6-plan/25-navigation-redux.md).

     This scrim, the panel below it and the node-index scrim are the only
     surfaces on this page big enough to cover the CANVAS, and the canvas is
     the whole interaction: organism.js §10c reads a tap off it (the poke —
     wobble, ripple, spores, haptic) and journey/chapters/final/interact.js
     reads the field bodies off the same element. An invisible one of these
     left hit-testable does not look like anything at all; it silently eats
     every pointerdown on the frame, and the site simply stops answering
     touch. That is exactly what happened once already (9e674d3).

     So hit-testability follows the VISIBLE state, declaratively: these
     surfaces are inert unless they carry `.open`, which is the same class
     that makes them visible. `hidden` is still managed in JS and still
     right — it is now a second belt rather than the only one. An overlay
     that is not open cannot take a pointer even if its `hidden` bookkeeping
     is wrong, missing, or forgotten by the next overlay built to this
     pattern. tools/inputgates.js asserts it. */
  .j-menu-scrim {
    position: fixed;
    inset: 0;
    z-index: 6;
    opacity: 0;
    pointer-events: none;
    touch-action: manipulation;
    background: rgba(5, 3, 1, 0.62);
    transition: opacity 0.3s ease;
  }
  .j-menu-scrim.open { opacity: 1; pointer-events: auto; }
  /* the explicit belt the index scrim already carried, for parity: the UA
     `[hidden]` rule is one author `display` declaration away from being lost */
  .j-menu-scrim[hidden] { display: none; }

  .j-menu {
    position: fixed;
    right: 0; top: 0; bottom: 0;
    z-index: 7;
    width: min(29rem, 92vw);
    padding: 1rem 2.2rem 2.4rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* OPAQUE, deliberately. At 0.97 the hero's wordmark ghosted through the
       panel's own heading, and the obvious fix — a backdrop-filter blur — asks
       for a full-frame blur every frame over a live WebGL scene that is
       already the page's whole frame budget. A dialog does not need to be
       translucent; the scrim behind it is what says there is still a page
       under here. */
    background: #0b0702;
    border-left: 1px solid rgba(217, 164, 65, 0.3);
    box-shadow: -24px 0 70px rgba(0, 0, 0, 0.6);
    opacity: 0;
    /* 92vw on a phone: full height, nearly full width. Inert unless open —
       see the hit-model note above the scrim. */
    pointer-events: none;
    /* it arrives from the edge the rail lives on — the same statement the
       popover's unfurl makes about where a panel comes from */
    transform: translateX(14px);
    transition: opacity 0.3s ease, transform 0.34s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .j-menu.open { opacity: 1; transform: translateX(0); pointer-events: auto; }
  .j-menu[hidden] { display: none; }

  .j-menu-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 0.5rem;
    min-height: 44px;
    margin: 0 0 1.15rem;
  }
  .j-menu-close-zone {
    display: flex;
    align-items: center;
    margin-left: 0.25rem;
    padding-left: 0.75rem;
    border-left: 1px solid rgba(217, 164, 65, 0.22);
  }
  .j-menu-x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    background: rgba(217, 164, 65, 0.025);
    border: 1px solid rgba(217, 164, 65, 0.24);
    border-radius: 50%;
    color: var(--muted);
    cursor: pointer;
    transition: color 0.3s, border-color 0.3s, background 0.3s;
  }
  .j-menu-x svg {
    display: block;
    width: 14px;
    height: 14px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.35;
    stroke-linecap: round;
  }
  .j-menu-x:focus-visible {
    color: var(--gold-bright);
    border-color: rgba(217, 164, 65, 0.62);
    background: rgba(217, 164, 65, 0.07);
  }
  @media (hover: hover) and (pointer: fine) {
    .j-menu-x:hover {
      color: var(--gold-bright);
      border-color: rgba(217, 164, 65, 0.62);
      background: rgba(217, 164, 65, 0.07);
    }
  }

  .j-menu-h {
    min-width: 0;
    margin: 0;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.26em;
    text-transform: uppercase;
    color: var(--gold);
  }
  /* Pointer entry focuses this non-interactive heading so the dialog is
     announced. It is a neutral focus target, not a control, so it must not
     inherit a browser or shared-control selection ring. */
  .j-menu .j-menu-h:focus,
  .j-menu .j-menu-h:focus-visible {
    outline: none;
    box-shadow: none;
  }
  /* same bare-`nav` reset the rail takes (hero.css `nav {}`) */
  .j-menu-nav { display: block; padding: 0; animation: none; }
  .j-menu-list { margin: 0; padding: 0; list-style: none; }
  .j-menu-list > li { border-top: 1px solid rgba(217, 164, 65, 0.16); }
  .j-menu-list > li:last-child { border-bottom: 1px solid rgba(217, 164, 65, 0.16); }
  .j-menu-item {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    padding: 0.85rem 0.6rem 0.7rem 0;
    color: var(--parchment);
    text-decoration: none;
    transition: color 0.3s, background 0.3s;
  }
  .j-menu-mark { flex: 0 0 auto; margin-top: 0.15rem; color: rgba(217, 164, 65, 0.66); transition: color 0.3s; }
  .j-menu-txt { display: block; }
  .j-menu-no {
    display: inline-block;
    margin-right: 0.6em;
    font-size: 0.58rem;
    font-weight: 500;
    letter-spacing: 0.14em;
    color: rgba(217, 164, 65, 0.5);
  }
  .j-menu-name {
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.24em;
    text-transform: uppercase;
  }
  .j-menu-line {
    display: block;
    margin-top: 0.3rem;
    font-family: "Didot", "Playfair Display", Georgia, "Times New Roman", serif;
    font-size: 1rem;
    line-height: 1.32;
    color: var(--muted);
  }
  .j-menu-item:is(:hover, :focus-visible) { color: var(--gold-bright); background: rgba(217, 164, 65, 0.05); }
  .j-menu-item:is(:hover, :focus-visible) .j-menu-mark { color: var(--gold-bright); }
  .j-menu-item[aria-current="true"] { color: var(--gold-bright); }
  .j-menu-item[aria-current="true"] .j-menu-mark { color: var(--gold-bright); }
  .j-menu-item[aria-current="true"] .j-menu-no { color: var(--gold-bright); }

  /* ---- a section's contents: item — one sentence → primary link ----
     Indented to the section text's own column (mark 22px + 1rem gap), so the
     map reads as an outline: section, then what lives inside it. */
  .j-menu-sub {
    margin: 0;
    padding: 0 0.6rem 0.9rem 2.4rem;
    list-style: none;
  }
  .j-menu-row {
    margin: 0 0 0.5rem;
    font-size: 0.8rem;
    line-height: 1.55;
    color: var(--muted);
  }
  .j-menu-row:last-child { margin-bottom: 0; }
  .j-menu-il {
    display: inline-flex;
    align-items: center;
    gap: 0.42rem;
    color: var(--parchment);
    font-weight: 600;
  }
  .j-menu-pict {
    display: inline-flex;
    width: 13px;
    height: 13px;
    flex: 0 0 13px;
    color: rgba(240, 200, 119, 0.78);
    filter: drop-shadow(0 0 4px rgba(240, 170, 60, 0.35));
  }
  .j-menu-pict svg {
    display: block;
    width: 100%;
    height: 100%;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.1;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  .j-menu-il::after { content: " — "; color: rgba(201, 191, 168, 0.45); font-weight: 400; }
  .j-menu-ia {
    margin-left: 0.45em;
    color: var(--gold-bright);
    text-decoration: none;
    white-space: nowrap;
    border-bottom: 1px solid rgba(217, 164, 65, 0.4);
    transition: border-color 0.3s;
  }
  .j-menu-ia::after { content: " \2192"; }
  .j-menu-ia:is(:hover, :focus-visible) { border-color: var(--gold-bright); }

  .j-menu-owned {
    padding: 0 0.6rem 0.9rem 2.4rem;
    font-size: 0.8rem;
    line-height: 1.55;
  }
  .j-menu-owned-copy {
    margin: 0;
    color: var(--parchment);
    font-weight: 600;
  }
  .j-menu-owned-more { margin: 0.25rem 0 0; color: var(--muted); }
  .j-menu-owned-more a {
    color: var(--gold-bright);
    text-decoration: none;
    border-bottom: 1px solid rgba(217, 164, 65, 0.45);
    transition: border-color 0.3s;
  }
  .j-menu-owned-more a:is(:hover, :focus-visible) { border-color: var(--gold-bright); }

  .j-menu-links {
    display: flex;
    flex-wrap: nowrap;
    gap: 0.25rem;
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .j-menu-links a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 44px;
    color: rgba(201, 191, 168, 0.7);
    text-decoration: none;
    border-radius: 50%;
    transition: color 0.3s, background 0.3s;
  }
  .j-menu-links svg { display: block; width: 15px; height: 15px; fill: currentColor; }
  .j-menu-links a[aria-label="X"] svg { width: 13px; height: 13px; }
  .j-menu-links a:is(:hover, :focus-visible) {
    color: var(--gold-bright);
    background: rgba(217, 164, 65, 0.06);
  }

  .j-menu-legal {
    margin: 1rem 0 0;
    font-size: 0.7rem;
    line-height: 1.6;
    color: rgba(201, 191, 168, 0.6);
  }

  @media (max-width: 420px) {
    .j-menu { padding: 0.9rem 0.8rem 1.8rem; }
    .j-menu-head { gap: 0.25rem; }
    .j-menu-h { font-size: 0.66rem; letter-spacing: 0.18em; }
    .j-menu-links { gap: 0.25rem; }
    .j-menu-close-zone { margin-left: 0.15rem; padding-left: 0.55rem; }
  }

  /* --- chapter copy: stable at rests, released during major travel --- */
  .j-copy { position: fixed; inset: 0; z-index: 1; pointer-events: none; }
  .j-block {
    position: absolute;
    /* Fallbacks only. ui.js writes both per arrival (armCopyEntry); without
       them a `calc(var(--j-in) * …)` would resolve to an invalid duration and
       take its whole `animation` shorthand down with it. */
    --j-in: 620ms;
    --j-in-wait: 500ms;
    opacity: 0;
    visibility: hidden;
    max-width: 34rem;
    /* each block carries its own local scrim so it never depends on the
       hero's Mission-pose gradient, which releases as the journey starts */
    padding: 1.6rem 2rem;
  }
  .j-block::before {
    content: "";
    position: absolute;
    inset: -2.5rem -3.5rem;
    z-index: -1;
    background: radial-gradient(ellipse 62% 68% at 40% 50%, rgba(12, 8, 3, 0.66), transparent 72%);
  }
  .j-h {
    margin: 0 0 1rem;
    font-family: "Didot", "Playfair Display", Georgia, "Times New Roman", serif;
    font-weight: 400;
    font-size: clamp(1.9rem, 3.1vw, 3rem);
    line-height: 1.12;
    color: var(--parchment);
    text-shadow: 0 2px 30px rgba(10, 6, 2, 0.9), 0 0 8px rgba(10, 6, 2, 0.6);
  }
  .j-sub {
    margin: 0;
    max-width: 26rem;
    font-size: 1rem;
    line-height: 1.6;
    color: var(--muted);
    text-shadow: 0 1px 18px rgba(10, 6, 2, 0.9);
  }
  /* The `.j-claim*` rules that stood here are gone with the Owned claims LIST
     they styled (Hannah, 2026-08-05 — the claims are one prose `sub` now; see
     content/content.js for the copy and the lock-override provenance). Owned
     was the only chapter that ever had a claims list, so nothing else referred
     to them and they would have been dead CSS.
     What survives is the list's one BEHAVIOUR: hovering the copy pulses the
     colony. `.j-copy` turns pointer-events off wholesale, so the line that
     answers a hover opts back in. */
  .j-sub.j-pulse { pointer-events: auto; }

  /* ============================================================
     THE ACTION ROW (Hannah, 2026-08-07; halved 2026-08-13)

     "a button that says 'Learn more', and then next to it ... a
      remix button ... make them work nicely together visually."
                                                    — 2026-08-07
     "remove the visible Remix button ... [the crown light-flash]
      interaction should take over the Remix behaviour ...
      integrated into the scene rather than exposed as a separate
      UI control."                                  — 2026-08-13

     So the pair is a row of one, and everything that was here to
     tell the second pill apart from the first has gone with it:
     the `.j-act-explore` weight, the three-node glyph and its
     filament, the hover-sequence and the swap spark, and the
     `aria-disabled` cursor the busy state needed. None of it is
     re-homed — the crown paints nothing, so it needs no artwork;
     what it inherited is BEHAVIOUR, not decoration. The whole of
     the crown's own CSS is a focus ring, down in the hotspot
     block (`.j-hotzone`).

     What is left is the hero nav's `.pill`, once: same height,
     same 999px radius, same 0.78rem/0.04em type, same
     0.5rem/1.35rem box, same 0.7rem gap the `.nav-cta` pair uses.
     A lone destination is exactly what that silhouette was
     already for, so nothing about it needed re-tuning when its
     neighbour left — measured at 1440x900, 1280x800 and 375x812,
     the pill's box is unchanged to the pixel.

     Learn more carries CONTRAST: parchment hairline at full
     strength plus a 4.5% parchment wash, i.e. the hero `.pill`
     with its interior lit.

     It is NOT a solid fill, and that is a composition decision
     rather than a taste one: this chapter is staged so the crown
     is the brightest point in the frame (chapters/owned/index.js
     EXPOSURE_PLANES note). A filled parchment button dropped at
     top-centre would take that job away from the scene — and now
     that the crown is also where the interaction lives, competing
     with it would be worse than a look problem.

     MOTION belongs to the hero's `.co` "instrument powering on"
     family — each keyframe ends at the resting style, so the
     reduced-motion rule at the foot of this file is just
     `animation: none`.
     ============================================================ */
  .j-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.7rem;                 /* the hero `.nav-cta` gap, verbatim */
    margin: 1.5rem 0 0;
    /* `.j-copy` turns pointer-events off wholesale. Only the PILLS opt back
       in — never this row — so the gap between them, and the dead space either
       side of them, stay transparent to the portrait field's hit pads. */
    pointer-events: none;
  }
  .j-block.pos-topcentre .j-actions { justify-content: center; }

  .j-act {
    pointer-events: auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5em;
    margin: 0;
    padding: 0.5rem 1.35rem;
    font-family: inherit;
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.04em;
    line-height: 1.5;
    white-space: nowrap;
    text-decoration: none;
    border-radius: 999px;
    background: transparent;
    cursor: pointer;
    transition: color 0.25s, border-color 0.25s, background 0.25s;
  }
  .j-act-primary {
    color: var(--parchment);
    border: 1px solid rgba(242, 237, 225, 0.58);
    background: rgba(242, 237, 225, 0.045);
    text-shadow: 0 1px 14px rgba(10, 6, 2, 0.9);
  }
  .j-act-primary:is(:hover, :focus-visible) {
    border-color: var(--parchment);
    background: rgba(242, 237, 225, 0.10);
  }
  /* --- entry: the row is the LAST part of the arriving copy -------------
     Same envelope as the heading and the sub above (`--j-in-wait` +
     fractions of `--j-in`), so a longer flight stretches it with
     everything else. It starts at 0.40 and settles at 0.92 of the
     envelope, which is exactly where the sub lands — d1ecc23's contract
     ("the last part lands at 92%, leaving the final beat as a pure
     settle") is kept, and it was the FIRST pill that always carried those
     numbers, so losing the second one leaves the timing untouched. */
  .j-block.j-arrive .j-act {
    animation: j-line-in calc(var(--j-in) * var(--j-act-dur, 0.52))
               cubic-bezier(0.22, 1, 0.36, 1) both;
    animation-delay: calc(var(--j-in-wait) + var(--j-in) * var(--j-act-lead, 0.40));
  }

  /* ============================================================
     ARRIVING COPY (Hannah, 2026-08-07) — the nav-jump entry.

     The block's OPACITY is not here: ui.js drives it as one envelope
     placed inside the camera's blend (see armCopyEntry, and
     COPY_JUMP_LEAD / COPY_JUMP_TAIL_S). What is here is the order the
     parts inside it come up in, borrowed from the hero's callout boot
     — the bed of shadow lights first, then the heading, then the sub:
     a composition assembling, not a paragraph appearing.

     Every duration is a fraction of `--j-in` and every delay carries
     `--j-in-wait`, so the whole choreography stretches with the flight
     and always closes INSIDE the envelope (the last part lands at 92%
     of it, leaving the final beat as a pure settle of the whole block).

     Nothing here may change the block's LAYOUT. ui.js reads
     `getBoundingClientRect()` on these blocks every frame, for every
     hotspot, to decide which chips the copy suppresses — so an entry
     that animated width, letter-spacing or margin would both thrash
     layout and make hotspots strobe through the arrival. Opacity and
     child transforms only; a child's transform leaves its parent's
     rect alone, which is exactly why the rise sits on `.j-h`/`.j-sub`
     rather than on the block.

     Each keyframe ENDS at the resting style, so the reduced-motion
     rule at the foot of this file just turns them off (same contract
     the hero's `.co` entry keeps in hero.css).
     ============================================================ */
  .j-block.j-arrive::before {
    animation: j-bed-in calc(var(--j-in) * 0.40) ease both;
    animation-delay: var(--j-in-wait);
  }
  @keyframes j-bed-in { from { opacity: 0; } to { opacity: 1; } }
  .j-block.j-arrive .j-h {
    animation: j-line-in calc(var(--j-in) * 0.66) cubic-bezier(0.22, 1, 0.36, 1) both;
    animation-delay: calc(var(--j-in-wait) + var(--j-in) * 0.12);
  }
  .j-block.j-arrive .j-sub {
    animation: j-line-in calc(var(--j-in) * 0.66) cubic-bezier(0.22, 1, 0.36, 1) both;
    animation-delay: calc(var(--j-in-wait) + var(--j-in) * 0.26);
  }
  @keyframes j-line-in {
    from { opacity: 0; transform: translateY(0.16em); }
    to   { opacity: 1; transform: translateY(0); }
  }

  .j-block.pos-left       { left: 5.2vw; top: 50%; transform: translateY(-50%); }
  .j-block.pos-bottom     { left: 50%; bottom: 8vh; transform: translateX(-50%); text-align: center; }
  .j-block.pos-bottom .j-sub { margin: 0 auto; }
  /* D22 (2026-08-17, Hannah's balance pass): the Inspire copy steps up out of
     the ground network's brightest band — the block used to sit with the
     headline right where the floor detail begins to read. ~40px on desktop
     (judged on the rendered 1440x900 frame, not computed), and further on a
     portrait tablet, where the same pass drops the mushroom ~46px and grows
     it ~13% (portrait.js tablet band) so the stack closes from both ends:
     mushroom -> headline -> body -> ground. PHONES ARE EXEMPT on purpose —
     the D19/D21 phone poses balanced the cap against the copy band midpoint
     by measure, and a raise here would silently un-solve them; hence the
     min-width guards. The short-landscape override later in this file
     (max-height 520) still wins for squat windows: same specificity, later
     rule. */
  @media (min-width: 901px) {
    .j-block.pos-bottom { bottom: calc(8vh + 40px); }
  }
  @media (min-width: 600px) and (max-width: 900px) and (orientation: portrait) {
    .j-block.pos-bottom { bottom: calc(8vh + 88px); }
    /* the same tablet pass grows the mushroom ~13%; the initiative chips keep
       pace so the constellation reads as one ring of callouts rather than
       three small floating tags (Hannah's tablet note E). The pill measures
       itself at runtime (ui.js pillW), so a media-scoped size is safe. */
    .j-hot { font-size: 0.7rem; }
  }
  .j-block.pos-topcentre  { left: 50%; top: 15vh; transform: translateX(-50%); text-align: center; }
  /* Owned's sub is centred prose now rather than a centred list, so it needs
     the same auto margins the other centred position gives its sub. */
  .j-block.pos-topcentre .j-sub { margin: 0 auto; }
  /* Connect restage (16-connect-ground-restage.md, Hannah): the chapter's copy
     claims the TOP-RIGHT of the frame while the mushroom claims the top-left
     and the tendril network spreads through the gap between them. The block
     hangs off the right margin and its type sets right-ragged, so the ragged
     edge faces the open ground instead of the frame edge. The local scrim's
     ellipse mirrors with it (60%, not 40%).

     15vh -> 19vh (2026-08-13, Hannah's third report on this frame: "it still
     feels too high on the page tbh"). The subject came down 54px in the same
     pass, and at fixed eye/radius/fov that is a pure rotation which sweeps the
     ground's far edge down by the same angle — so the band under this block
     reopens exactly as far as the mushroom drops. Two previous passes traded
     along that axis in opposite directions and each re-opened the other's
     complaint.

     This block's own height is the third term nobody had moved. The band is
     measured from the block's BOTTOM EDGE to the first scene content below it,
     so every vh of drop comes off the band for nothing — no camera move, no
     exchange rate. Measured at 1440x900 with this pass's far field in, at the
     shipped aim: the band runs 173px at 15vh, 137px at 19vh, 104px at 22vh.

     19vh IS A CEILING SET BY THE RAIL, and 1280x800 sets it, not 1440x900 —
     which is why it is worth writing down. `.j-rail`'s items are 48px tall and
     sit in the right 98px of the frame at every width, while this block's
     right edge sits 5.2vw in, so the two ALWAYS overlap horizontally (23px at
     1440, 31px at 1280) and have only ever cleared each other VERTICALLY. The
     rail's first item starts at y 378 of 900 but at y 328 of 800, while this
     block is ~163px tall at both — so the shorter viewport is the binding one
     by 50px. 22vh clears 1440x900 by 12px and OVERLAPS 1280x800 by 11px; 19vh
     clears both (38px and 13px). 26vh and 34vh were also shot and both put the
     sub's last line straight through the rail glyphs at 1440 as well.

     The <=900px override below is deliberately left at 11vh: that layout puts
     the copy top-centre over a portrait frame whose band already measures
     0px, so it has nothing to buy and a drop there would only crowd the rail
     (7px of clearance as it stands). */
  .j-block.pos-topright   { right: 5.2vw; top: 19vh; text-align: right; }
  .j-block.pos-topright .j-sub { margin-left: auto; }
  .j-block.pos-topright::before {
    background: radial-gradient(ellipse 62% 68% at 60% 50%, rgba(12, 8, 3, 0.66), transparent 72%);
  }

  /* ============================================================
     CONNECT MEASURE (Hannah, 2026-08-11)

     The chapter heading became "Connect the community." (content/
     content.js, with the lock-override provenance). One word, and
     it does not fit where the old one did:

       viewport   font       "Connect the ecosystem."  "…community."
       1440x900   44.64px    476px                     494px

     The block's column was 476px — 34rem of block less 2rem of
     padding a side — which the outgoing heading filled to the
     pixel. The incoming one overruns it by 18px, and a heading
     that has always set one line would have arrived in two.

     So the block widens to 38rem (608px), giving 544px of column:
     50px of headroom over the heading at 1440, where there was
     none at all before. The house type scale in `.j-h` is
     untouched, exactly as it was for the Final heading.

     The block only widens; it does not move. It is anchored by its
     RIGHT edge (`right: 5.2vw`) and sets right-ragged, so the
     extra width is taken on the left, into the open ground the
     restage gave it, and the type's own edge is where it was.
     ============================================================ */
  .j-block.pos-topright { max-width: 38rem; }
  /* Final restage (17-final-field.md): the epilogue copy floats on the dark
     cutaway wedge in the lower-left corner, giving the upper frame to the
     mushroom field. (The 9vh bottom was chosen to clear the footer cue's
     band; the cue is gone — navigation redux, 2026-08-09 — and the offset
     stays because the composition was signed off with it.) */
  .j-block.pos-bottomleft { left: 5.2vw; bottom: 9vh; }

  /* ============================================================
     FINAL HEADING MEASURE (Hannah, 2026-08-05)

     The epilogue h1 became "The open source ecosystem can accelerate
     a 2nd Renaissance" (content/content.js, with the lock-override
     provenance), and it has to WRAP TO TWO LINES. That is a measure
     problem, not a font-size one, so it is solved here and the house
     type scale in `.j-h` above is untouched at desktop sizes.

     Measured, at the shipped font, for the balanced break
     "The open source ecosystem can / accelerate a 2nd Renaissance":

       viewport   font       natural 1-line   longest of the 2 lines
       1440x900   44.64px    1232px           637px
       1280x800   39.68px    1095px           566px

     So the heading needs a measure of at least 637px to reach two
     lines, and must stay well under 1232px or it collapses to one.
     46rem (736px) of block minus 2rem padding a side = 672px of text
     column sits in that window at BOTH desktop sizes, with ~35px of
     headroom over the 1440 requirement — enough that font hinting or
     a fallback serif cannot tip it to three lines.

     The block only widens; it does not move. Its left edge and bottom
     are unchanged, so it still floats on the same dark cutaway wedge.
     ============================================================ */
  .j-block.pos-bottomleft { max-width: 46rem; }

  /* ============================================================
     CONNECT AND FINAL SUB MEASURE (Hannah, 2026-08-11)

     "Make that over two lines instead of three" — said of the
     Final sub and then of the Connect sub. Both set THREE lines
     against the house `.j-sub` measure of 26rem (416px), and both
     are now two. Same shape of answer as the Final heading's:
     measure and copy, never the type scale.

     TWO LEVERS, IN THAT ORDER.

     1. COPY, for Final only. The line was rewritten to Hannah's
        sense (see content/content.js). Written long it is 147
        characters and needs 544px of column to break in two;
        written as shipped it is 120 and needs 448px. Connect's sub
        is a locked string she did not ask to change, so it was not
        touched — it needs 448px as it stands.

     2. MEASURE. Both subs are released from 26rem to the column
        their block already owns:

          block            column   sub measure   needs   headroom
          pos-topright     544px    34rem 544px   448px   96px
          pos-bottomleft   672px    36rem 576px   448px   128px

        Neither block is WIDENED for the sub — both ceilings were
        already set by the heading over it, and a sub cannot push
        past one. `pos-bottomleft` has held 672px of column since
        the FINAL HEADING MEASURE above and its sub stops 96px
        short of that, so that block's rendered width is genuinely
        unchanged. `pos-topright`'s ceiling is the 38rem its new
        heading needed; its sub takes that column to the pixel, so
        the block renders 608px where the heading alone would have
        asked for 558px. That is width the block was already
        permitted, spent on text rather than left as padding. The
        block is right-anchored, so the difference is taken on the
        left and the type's own edge and rag do not move.

     The headroom is the point of the wider figure. `.j-sub` names
     Inter first and NOTHING IN THIS BUILD LOADS A WEBFONT
     (`document.fonts` is empty), so what a visitor actually gets is
     wherever their machine falls in the stack — SF here, Segoe UI
     on Windows, Arial at the wide end. A measure sized to the 448px
     requirement would be a two-line promise that only held on the
     developer's font. These hold to ~+20%.

     WHY `text-wrap: balance` COMES WITH THE WIDTH. Greedy wrapping
     spends a wide measure on a long first line and a stub:

       Final,   greedy  @576px   534px / 312px
       Connect, greedy  @544px   536px / 282px

     which is two lines by the count and a line-and-a-half by eye.
     Balanced, both settle into a pair — 447/399 and 394/423 — and,
     because balance minimises the longest line rather than filling
     to the measure, the RENDERED result is identical at 480px and
     at 576px. That is what makes the headroom above free: it buys
     tolerance without widening the text the reader sees.

     Scoped to these two blocks. Mission, Inspire and Owned keep the
     26rem measure and the greedy break they shipped with.

     DESKTOP ONLY, and deliberately so. Below 900px the block takes
     its width from the viewport (88vw / 94vw), leaving ~266px of
     column at Connect and ~285px at Final on a 375px phone, against
     lines whose natural widths are 822px and 850px. Two lines is
     unreachable there by a factor of three, and the only lever left
     would be the type scale — which is the one this site does not
     pull. Both subs run to four balanced lines at phone widths,
     under headings that already made the same trade for the same
     reason (see FINAL HEADING AT PHONE WIDTHS below, and
     journey-v6-plan/24-mobile-pass.md §Final).
     ============================================================ */
  .j-block.pos-topright   .j-sub { max-width: 34rem; text-wrap: balance; }
  .j-block.pos-bottomleft .j-sub { max-width: 36rem; text-wrap: balance; }

  /* --- hotspot proxies: hit-model correctness now, polish later --- */
  .j-hotspots { position: fixed; inset: 0; z-index: 1; pointer-events: none; }
  .j-hot {
    position: absolute;
    top: 0; left: 0;
    display: flex;
    align-items: center;
    gap: 0.55rem;
    margin: -11px 0 0 -11px;
    padding: 0.35rem 0.8rem 0.35rem 0.35rem;
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    background: rgba(12, 8, 3, 0.3);
    border: 0;
    border-radius: 999px;
    color: rgba(201, 191, 168, 0.72);
    font: inherit;
    font-size: 0.64rem;
    font-weight: 600;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    white-space: nowrap;
    cursor: pointer;
    text-shadow: 0 1px 10px rgba(10, 6, 2, 0.9);
    transition: color 0.3s, background 0.3s;
  }
  .j-hot.vis { visibility: visible; pointer-events: auto; }
  /* THE HIT PAD (2026-08-06, report A). A round target the size of the thing
     the node draws, pinned to the node inside the button's own box — so the
     edge-flip and the narrow-viewport nudge below move the LABEL and leave the
     target on the face. Zero-sized, and therefore inert, for any chapter that
     does not tell ui.js a node radius. */
  .j-hot-hit {
    position: absolute;
    /* --j-dot-dy is the PILL COLLISION DODGE's compensation (ui.js): when a
       pill is raised clear of a neighbour, the dot and the pad stay pinned on
       the node by the same offset, so the visible marker and the hit surface
       cannot disagree with the anchor. 0 for every un-dodged chip. */
    top: calc(11px + var(--j-dot-dy, 0px));
    left: var(--j-hit-x, 11px);
    width: var(--j-hit, 0px);
    height: var(--j-hit, 0px);
    margin: 0;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    pointer-events: auto;
  }
  /* A hover-only chip shows NOTHING at rest, so its pill must not be a hit
     surface then: an invisible 200-300px bar lying beside the node was most of
     why the hit model and the picture disagreed. The pad still answers (it
     sets pointer-events itself), and the moment the chip is hot the pill is
     visible and interactive again, so a pointer can walk from the face onto
     the label and on to the popover exactly as before. */
  .j-hot.label-hover.vis { pointer-events: none; }
  .j-hot.label-hover.vis:is(:hover, .hot, :focus-visible) { pointer-events: auto; }

  /* --- A BARE CHIP: a control with no pixels of its own (2026-08-14) -------
     Owned's sixteen contributors, per `chip: 'none'` in their label policy.
     ui.js has already DELETED the dot and the label from the DOM by the time
     this applies, so these rules only take away the box those two sat in — the
     pill's padding and its lit background. What remains is a zero-size button
     wrapping a face-sized hit pad: the same arrangement `.j-hotzone` already
     ships for the crown, which paints nothing and answers everything.

     The face is not unmarked by this. It is drawn by the scene, and hovering
     it lights the node, its ember rim and its own local strands — that is the
     marker, and it is the one the reference asked for. */
  .j-hot.bare,
  .j-hot.bare:is(:hover, .hot, :focus-visible) { padding: 0; background: transparent; }
  /* THE FOCUS RING MOVES TO THE PAD, and has to. The shared journey ring below
     draws on `.j-hot`, which for a bare chip is a 0x0 box at the node's centre
     — a ring nobody could see, on a control a keyboard visitor has no other
     way to locate. The pad is the face-sized circle they are actually on, so
     the ring is drawn there and made round to match it. Same decision the
     crown's `<button>` zone took on 2026-08-13: a scene response is not a
     focus indicator a conformance test can see. */
  .j-hot.bare:focus-visible { outline: none; }
  .j-hot.bare:focus-visible .j-hot-hit {
    outline: 2px solid var(--gold-bright);
    outline-offset: 3px;
    border-radius: 50%;
  }
  /* A hover zone is a target that is part of the scene, not the UI: no chip,
     no label, NOTHING DRAWN. See ui.addHoverZone.

     2026-08-13: a zone that declares an `action` is built as a <button> so
     the crown's re-deal is keyboard-operable, which means every UA button
     default has to be taken back off — a zone that painted so much as a
     border would be a grey disc parked on the brightest point in the frame,
     and would move a frozen golden. The rules below are written for both
     element types at once, so `<i>` and `<button>` render identically:
     nothing.

     `visibility: hidden` is doing two jobs here and both matter. It is what
     makes an off-chapter zone untouchable, and it is ALSO what keeps a
     <button> zone out of the tab order without a roving tabIndex — a
     visibility:hidden element is not focusable, by the spec. So the same
     one-word gate that ui.js already flips for the pointer is the gate for
     the keyboard. */
  .j-hotzones { position: fixed; inset: 0; z-index: 0; pointer-events: none; }
  .j-hotzone {
    position: absolute;
    top: 0; left: 0;
    border-radius: 50%;
    margin: 0;
    padding: 0;
    -webkit-appearance: none;
    appearance: none;
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    visibility: hidden;
    pointer-events: none;
  }
  .j-hotzone.vis { visibility: visible; pointer-events: auto; }
  /* A zone built as a <button> is pressable, so it says so. The zone paints
     nothing, so the cursor is the ONLY resting affordance it can offer — the
     light response on hover is the rest of the invitation. */
  button.j-hotzone { cursor: pointer; }
  /* Busy: the chapter is still answering the last press (ui.js `busyMs`).
     aria-disabled rather than `disabled`, so focus survives the swap. */
  button.j-hotzone[aria-disabled="true"] { cursor: default; }
  /* THE FOCUS RING. The crown's real focus response is out in the scene —
     focusing it lights the whole colony, exactly as hovering it does — but a
     scene response is not a focus indicator a conformance test can see, so
     the zone draws its own: a ring on the circle the crown actually occupies,
     which is also the shape of the hit pads a Tab press moves on to next.
     Never visible at rest, so no golden can see it. */
  button.j-hotzone:focus { outline: none; }
  button.j-hotzone:focus-visible {
    outline: 2px solid var(--gold-bright);
    outline-offset: -2px;
    box-shadow: 0 0 22px rgba(240, 200, 119, 0.35);
  }
  /* Right-edge chips run their pill LEFT of the dot instead of off the frame
     (ui.js measures and sets `.flip` + a compensating translate, so the dot
     stays exactly on its node either way). Added for the Owned root-network
     restage, whose portrait arc deliberately reaches |ndc x| 0.89 — but the
     rule is general and inert for every chip that already fits. */
  .j-hot.flip { flex-direction: row-reverse; padding: 0.35rem 0.35rem 0.35rem 0.8rem; }
  .j-hot-dot {
    width: 9px; height: 9px;
    border-radius: 50%;
    background: rgba(240, 200, 119, 0.6);
    box-shadow: 0 0 8px rgba(240, 170, 60, 0.45);
    /* the collision dodge's pin — see .j-hot-hit above. The dot rides the
       pill's flex row, so a raised pill would carry it off its node; this
       puts it back, visually, without touching the layout. */
    transform: translateY(var(--j-dot-dy, 0px));
    transition: background 0.3s, box-shadow 0.3s, transform 0.3s;
  }
  /* SMALL-FRAME CONTRAST (2026-08-17, Hannah's label-visibility pass): on
     phones — both orientations — the cap fills most of the frame, so the
     Inspire exit labels (Arca Gidan Prize / ArtCompute) land ON the bright
     cap wireframe instead of against dark sky as at desktop. The rest tint
     and wash below were tuned against the sky; over the lattice they go
     thin. Deeper wash + brighter ink only where the framing forces the
     overlap; the desktop cascade is untouched. Same phone conditions as the
     PL-1.4 touch block (max-width) plus the short-landscape frame hero.css
     compresses (max-height). */
  @media (max-width: 720px), (max-height: 560px) {
    .j-hot {
      background: rgba(12, 8, 3, 0.55);
      color: rgba(216, 207, 185, 0.92);
    }
  }
  .j-hot:is(:hover, .hot, :focus-visible) { color: var(--gold-bright); background: rgba(18, 12, 4, 0.6); }
  .j-hot:is(:hover, .hot, :focus-visible) .j-hot-dot {
    background: rgba(255, 226, 160, 1);
    transform: translateY(var(--j-dot-dy, 0px)) scale(1.35);
    box-shadow: 0 0 12px rgba(255, 200, 100, 0.9), 0 0 28px rgba(240, 170, 60, 0.5);
  }
  /* .j-hot's focus RING is the shared journey treatment further down; the
     colour/dot response above is what makes focus equal hover (PL-2.2). */

  /* ============================================================
     NODE POPOVER (Hannah, 2026-08-05) — the detail, beside the chip.

     Built by journey/ui.js (see the POPOVER block there for the
     popover-vs-card decision and the a11y contract). It lives INSIDE
     `.j-hotspots` on purpose, three ways at once: it inherits that
     layer's stacking so it clears the chapter copy, it is hidden by
     capture.py's existing `.j-hotspots` selector so no golden gains a
     tooltip, and it can sit next to its own chip in the DOM, which is
     what puts its link in the right place in the tab order.

     Position comes from ui.js placePop() as a transform — the chip it
     hangs off is world-tracked and moves, so this is re-placed per
     frame and CSS must not fight it with a transition on transform.
     ============================================================ */
  .j-pop {
    position: absolute;
    top: 0; left: 0;
    z-index: 2;                 /* over the chips, under the card */
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.45rem;
    width: max-content;
    max-width: min(19rem, 62vw);
    padding: 0.85rem 1rem 0.9rem;
    opacity: 0;
    visibility: hidden;         /* also takes it out of the a11y tree when shut */
    pointer-events: none;
    background: rgba(14, 9, 3, 0.94);
    border: 1px solid rgba(217, 164, 65, 0.3);
    border-radius: 3px;
    box-shadow: 0 12px 34px rgba(0, 0, 0, 0.5);
    text-align: left;           /* never inherit pos-topright's ragged-right */
    /* the house 0.3s, on opacity only */
    transition: opacity 0.3s ease;
  }
  .j-pop.open { opacity: 1; visibility: visible; pointer-events: auto; }

  /* ============================================================
     POPOVER ENTRY (Hannah, 2026-08-07)

     "Make the thing open with a nice cool understated but elegant
     entry animation, similar to the one that shows when I hover over
     Equip etc. on the hero mushrooms."

     The reference is the `.co` callout boot in hero.css — the numbered
     instrument powering on — so this is built from the same four
     gestures rather than from a generic fade or slide:

       hero `lead-draw`   a line that draws itself node -> label
         here            the panel lamps up with a quick gold-edge glint;
                         the contact filament below carries direction.

       hero `core-pop`    the node flares as it lands
       hero `ring-ping`   a ring pings once and ends at nothing
         here            a one-pixel FILAMENT on the contact edge draws to
                         full height, and goes out again — the connection to
                         the chip, stated once. ONE monotone rise and one
                         monotone fall, which is what `ring-ping` is; see the
                         note at `j-pop-filament` for the 2026-08-14 correction
                         and why the first cut was not that.

       hero `no-flicker`  the small `01` gutters alive in hard steps
         here            the link — the same typographic species, 0.66rem
                         uppercase gold at 0.18em — switches on in hard steps,
                         last of all. It does NOT gutter: see the note at
                         `j-pop-strike` for why the borrowed keyframe's
                         down-stops do not survive the move from a numeral
                         that boots once to a control that boots on every
                         hover.

       hero `tag-in`      the label fades in as its number flickers
         here            title then short line, staggered, opacity only.

     WHAT IS DELIBERATELY ABSENT: any transform, any scale, and any
     change of size. `transform` on `.j-pop` is ui.js's placement
     channel, rewritten every frame from placePop() — a transition or
     animation on it would fight the world-tracking. Scale would be
     motion toward the viewer, which the hover-reliability work
     (696e95d) exists to keep out of this element. And the panel's own
     box never changes, so `placePop()`'s per-frame
     `getBoundingClientRect()` reads a stable width and height right
     through the entry: the popover cannot walk while it opens, and the
     hit target it presents at 40 ms is in the same place as the one it
     presents at rest.

     Every keyframe ends at the element's resting style, so the
     reduced-motion rule at the foot of this file simply switches them
     off and the popover appears finished (hero.css's `.co` keeps the
     same contract).
     ============================================================ */
  /* The lamp is not the `transition` above doing its job — it cannot be.
     showPop() re-parents the popover next to its chip (`h.btn.after(pop)`,
     which is what puts the pinned link in the right place in the tab order),
     and a freshly inserted element has no previous computed style for a
     transition to run FROM. That is why the shipped popover arrived at full
     opacity on frame one with no ceremony at all: the 0.3 s transition only
     ever ran on the way out. The explicit shared shell animation below gives
     every vessel a rendered start state and keeps transform exclusively for
     placement. Direction remains a value only for the contact filament. */
  .j-pop {
    --fil-x0: 1;   --fil-y0: 0.12;                   /* filament draws its height */
    --detail-clip-start: inset(-80px 14px -80px -80px);
  }
  .j-pop[data-side="left"] { --detail-clip-start: inset(-80px -80px -80px 14px); }
  .j-pop[data-side="below"] { --detail-clip-start: inset(-80px -80px 14px -80px); }
  .j-pop[data-side="below"] { --fil-x0: 0.12; --fil-y0: 1; }
  /* One fast shell gesture for every journey detail surface. The box itself
     never translates: ui.js owns transform for live world anchoring (and the
     mobile sheet owns it for drag-to-dismiss). A quick lamp-up plus a small
     gold border glint gives popovers and profile cards the same arrival
     without moving their hit area or stacking a second slide on their own
     interior choreography. */
  @keyframes j-detail-arrive {
    0%   { opacity: 0; clip-path: var(--detail-clip-start); border-color: rgba(217, 164, 65, 0.12); }
    68%  { opacity: 1; clip-path: inset(-80px); border-color: rgba(255, 226, 160, 0.62); }
    100% { opacity: 1; clip-path: inset(-80px); border-color: rgba(217, 164, 65, 0.35); }
  }
  @keyframes j-detail-depart {
    0%   { opacity: 1; clip-path: inset(-80px); border-color: rgba(217, 164, 65, 0.35); }
    32%  { opacity: 0.9; clip-path: inset(-80px); border-color: rgba(255, 226, 160, 0.48); }
    100% { opacity: 0; clip-path: var(--detail-clip-start); border-color: rgba(217, 164, 65, 0.12); }
  }
  .j-pop.j-detail-enter,
  .j-card.j-detail-enter {
    animation: j-detail-arrive 0.2s cubic-bezier(0.22, 1, 0.36, 1) both;
  }
  .j-pop.j-pop-exit,
  .j-card.j-card-exit {
    animation: j-detail-depart 0.17s cubic-bezier(0.4, 0, 1, 1) both;
  }
  /* the contact filament — lit at rest is NOTHING, so opacity 0 both ends */
  .j-pop::before {
    content: "";
    position: absolute;
    top: -1px; bottom: -1px; left: -1px;
    width: 1px;
    opacity: 0;
    pointer-events: none;
    background: linear-gradient(to bottom,
      transparent, rgba(255, 216, 140, 0.95) 24%,
      rgba(255, 216, 140, 0.95) 76%, transparent);
    box-shadow: 0 0 6px rgba(240, 190, 90, 0.7);
  }
  .j-pop[data-side="left"]::before { left: auto; right: -1px; }
  .j-pop[data-side="below"]::before {
    top: -1px; bottom: auto; left: -1px; right: -1px;
    width: auto; height: 1px;
    background: linear-gradient(to right,
      transparent, rgba(255, 216, 140, 0.95) 24%,
      rgba(255, 216, 140, 0.95) 76%, transparent);
  }
  /* THE FILAMENT PINGS ONCE, which is what it always said it did.

     Its stated reference is hero.css's `ring-ping`, and the commit that
     introduced it describes it as "lit once ... it ends at opacity 0, exactly
     as ring-ping does". But `ring-ping` is one monotone fall — 0.9 -> 0, one
     stop to the next, nothing in between — and what was built here went
     0 -> 0.95 -> 0.35 -> 1 -> 0. That is a GUTTER, and a gutter is
     `no-flicker`'s gesture, not `ring-ping`'s. The filament was wearing the
     wrong reference's shape under the right reference's name: the contact
     edge lit, went half out, came back brighter and then faded, all in
     620 ms, on the one-pixel line the visitor's eye is already on because it
     is the edge nearest the chip they just pointed at.

     Rise, then out. One ping, stated once, exactly as claimed. The direction
     comes from --fil-x0/--fil-y0 rather than a second animation-name, for the
     same value-based direction contract used by live placement above. */
  .j-pop.j-pop-enter::before { animation: j-pop-filament 0.62s ease-out both; }
  @keyframes j-pop-filament {
    0%   { opacity: 0;    transform: scale(var(--fil-x0), var(--fil-y0)); }
    30%  { opacity: 0.95; transform: scale(1, 1); }
    100% { opacity: 0;    transform: scale(1, 1); }
  }

  .j-pop.j-pop-enter .j-pop-t { animation: j-pop-word 0.34s ease both; animation-delay: 0.10s; }
  .j-pop.j-pop-enter .j-pop-s { animation: j-pop-word 0.34s ease both; animation-delay: 0.17s; }
  @keyframes j-pop-word { from { opacity: 0; } to { opacity: 1; } }
  /* THE LINK POWERS ON; IT NO LONGER GUTTERS.

     `j-pop-strike` was hero.css's `no-flicker` copied stop for stop —
     0 -> 1 -> 0.35 -> 1 in hard `steps(1, end)` — and taken together with the
     filament above it is the other half of what Hannah reported: two gutters
     inside 0.5 s in one small panel, the edge first and the link second, with
     their dips 190 ms apart.

     Keeping the borrowed gesture here was the mistake, and the reason is not
     taste. `no-flicker` lights a NUMERAL on a diagram — a mark with no
     function to lose while it is dark — and the hero's callouts boot ONCE,
     on arrival at the top of the page. The last thing to arrive in this panel
     is a LINK: the only control it has, and the thing the visitor is on their
     way to click. A control that blanks and then half-lights reads as a
     fault rather than as a flourish, and it does it on EVERY hover, which on
     this page is the most repeated gesture there is. The same keyframe at a
     different repetition rate, on an element that can be broken rather than
     merely unlit, is a different gesture.

     So the character is kept and the gutter is removed: three hard steps up
     to full, monotone. It still arrives last, on the same delay, and it still
     switches on rather than fading in — `steps()` is what carries the
     instrument reading, and the down-stops were never what carried it. */
  .j-pop.j-pop-enter .j-pop-link {
    animation: j-pop-strike 0.30s steps(3, end) both;
    animation-delay: 0.24s;
  }
  @keyframes j-pop-strike {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  .j-pop-t {
    font-family: "Didot", "Playfair Display", Georgia, serif;
    font-weight: 400;
    font-size: 1.05rem;
    letter-spacing: 0.01em;
    color: var(--parchment);
  }
  .j-pop-s {
    font-size: 0.84rem;
    line-height: 1.5;
    color: var(--muted);
  }
  .j-pop-link {
    margin-top: 0.15rem;
    color: var(--gold-bright);
    text-decoration: none;
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    border-bottom: 1px solid rgba(217, 164, 65, 0.5);
    transition: border-color 0.3s;
  }
  .j-pop-link[hidden] { display: none; }
  .j-pop-link:is(:hover, :focus-visible) { border-bottom-color: var(--gold-bright); }

  /* ============================================================
     DETAIL CARD — ANCHORED TO ITS NODE (Hannah, 2026-08-13)

     "the associated information box currently appears far off to
      the right ... it should feel spatially connected to the
      person the user clicked."

     `right: 3.4vw; top: 50%` is gone. Position is now ui.js's
     `placeCard()`, written as a TRANSFORM from `left:0; top:0` —
     the same placement channel `.j-pop` uses, for the same reason
     (the anchor is world-tracked and moves, so this is rewritten
     per frame and CSS must not fight it with a transition on
     transform). `data-side` says which way it opened, and
     `--j-card-fx/fy` say where the node sits inside the card's
     own box, which is what lets the contact filament point at the
     person rather than run along the whole edge.

     The BOTTOM SHEET below is unchanged and still wins on a
     coarse pointer or a narrow viewport: it restates
     `transform: none` and its own insets, and placeCard()
     declines outright while `.sheet` is set.
     ============================================================ */
  .j-card {
    position: fixed;
    left: 0;
    top: 0;
    /* 5, not 3 (2026-08-18, Hannah: the card "should appear in front of the
       menu button"): the rail — hamburger included — sits at 4, so the old 3
       slid the person's name UNDER the chrome whenever a right-side face put
       its card on the flank. Above the rail, still under the opened menu's
       scrim (6) and panel (7): a committed overlay outranks an annotation. */
    z-index: 5;
    width: max-content;
    max-width: min(21rem, 84vw);
    max-height: 74vh;
    overflow-y: auto;
    padding: 1.15rem 1.35rem 1.1rem;
    background: rgba(14, 9, 3, 0.9);
    border: 1px solid rgba(217, 164, 65, 0.35);
    border-radius: 3px;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55);
    opacity: 0;
    /* Default / above: the near edge is the bottom, so the small shared
       aperture opens upward from there. Other anchor sides override below. */
    --detail-clip-start: inset(14px -80px -80px -80px);
    transition: opacity 0.3s ease;
  }
  .j-card[data-side="below"] { --detail-clip-start: inset(-80px -80px 14px -80px); }
  .j-card[data-side="right"] { --detail-clip-start: inset(-80px 14px -80px -80px); }
  .j-card[data-side="left"]  { --detail-clip-start: inset(-80px -80px -80px 14px); }
  .j-card[data-side="flank"] { --detail-clip-start: inset(5px); }
  .j-card.open { opacity: 1; }
  /* A closing card is out of the a11y tree and out of Tab on the tick
     (ui.js sets `inert`), but it keeps painting for the 0.34 s fade — and
     the hit pads it was covering come back on that same tick. Belt and
     braces for 696e95d: a fading card is never a hit surface over a face. */
  .j-card:not(.open) { pointer-events: none; }

  /* --- THE TRANSIENT TIER (2026-08-14) ---------------------------------
     The card as a HOVER reveal rather than a committed dialog (ui.js, THE
     CARD'S TWO TIERS). Two rules, and both of them are load-bearing:

     1. `pointer-events: none`. A panel that opens because a pointer is on a
        face must never become the thing that pointer is on — it would take
        the hover off the chip that is holding it open, the chip would go
        cold, the panel would close, the pointer would land back on the face,
        and the pair would oscillate at frame rate. It is also what lets a
        pointer travel STRAIGHT from one contributor to the next through the
        space a card happens to occupy. (The popover needs the opposite —
        POP_HIDE_MS exists so a pointer can walk onto it and reach its link —
        because a popover HAS a link. This panel has no controls at all while
        it is transient, which is rule 2.)
     2. No controls. The ✕ closes a card the visitor opened; de-hover closes
        this one, so the button would be an affordance for something that has
        already happened. Hiding it is also what makes the transient panel
        genuinely focus-free, which is what earns the right to drop
        `role="dialog"`/`aria-modal` in ui.js: there is nothing inside to trap.
        The grab handle goes with it — a sheet is a committed form only. */
  .j-card.transient { pointer-events: none; }
  .j-card.transient .j-card-x,
  .j-card.transient .j-card-grip { display: none; }

  /* The card shell uses the same j-detail-arrive/depart contract as the six
     initiative popovers. Its content and contact filament keep their own
     card-specific choreography below. */

  /* THE CONTACT FILAMENT — a short bright segment on the edge that faces the
     node, centred on the node's own column (`--j-card-fx`) rather than on the
     card. That is what makes it read as a contact and not a rule: when a face
     near the frame edge has pushed its card sideways, the filament still
     points at the face. Lit at rest is NOTHING, so opacity 0 at both ends.

     It sits at inset 0, not -1px like the popover's, because `.j-card` is an
     overflow-y:auto box and anything outside the padding box is clipped. */
  .j-card::before {
    content: "";
    position: absolute;
    left: calc(var(--j-card-fx, 50%) - 34px);
    bottom: 0;
    width: 68px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
    background: linear-gradient(to right,
      transparent, rgba(255, 216, 140, 0.95) 24%,
      rgba(255, 216, 140, 0.95) 76%, transparent);
    box-shadow: 0 0 6px rgba(240, 190, 90, 0.7);
  }
  .j-card[data-side="below"]::before { bottom: auto; top: 0; }
  .j-card[data-side="right"]::before,
  .j-card[data-side="left"]::before {
    left: 0; right: auto;
    top: calc(var(--j-card-fy, 50%) - 34px);
    bottom: auto;
    width: 1px; height: 68px;
    background: linear-gradient(to bottom,
      transparent, rgba(255, 216, 140, 0.95) 24%,
      rgba(255, 216, 140, 0.95) 76%, transparent);
  }
  .j-card[data-side="left"]::before { left: auto; right: 0; }
  /* the flank fallback (no anchor to point at) draws no filament */
  .j-card[data-side="flank"]::before { content: none; }
  .j-card.j-card-enter::before { animation: j-card-filament 0.62s ease-out both; }
  @keyframes j-card-filament {
    0%   { opacity: 0;    transform: scaleX(0.12); }
    18%  { opacity: 0.95; transform: scaleX(0.72); }
    30%  { opacity: 0.35; }
    42%  { opacity: 1;    transform: scaleX(1); }
    100% { opacity: 0;    transform: scaleX(1); }
  }
  .j-card.j-card-enter[data-side="right"]::before,
  .j-card.j-card-enter[data-side="left"]::before { animation-name: j-card-filament-v; }
  @keyframes j-card-filament-v {
    0%   { opacity: 0;    transform: scaleY(0.12); }
    18%  { opacity: 0.95; transform: scaleY(0.72); }
    30%  { opacity: 0.35; }
    42%  { opacity: 1;    transform: scaleY(1); }
    100% { opacity: 0;    transform: scaleY(1); }
  }
  /* the words arrive after the vessel, exactly as the popover's do */
  .j-card.j-card-enter .j-card-h { animation: j-card-word 0.34s ease both; animation-delay: 0.10s; }
  .j-card.j-card-enter .j-card-body > :not(.j-card-h) {
    animation: j-card-word 0.34s ease both;
    animation-delay: 0.17s;
  }
  @keyframes j-card-word { from { opacity: 0; } to { opacity: 1; } }
  .j-card-x {
    position: absolute;
    top: 0.7rem; right: 0.8rem;
    padding: 0.35rem 0.5rem;
    background: none;
    border: 0;
    color: var(--muted);
    font-size: 0.85rem;
    cursor: pointer;
  }
  .j-card-x:hover { color: var(--gold-bright); }
  .j-card-h {
    margin: 0 0 0.55rem;
    font-family: "Didot", "Playfair Display", Georgia, serif;
    font-weight: 400;
    font-size: 1.35rem;
    color: var(--parchment);
  }
  /* The person's avatar, left of the name — SHEET FORM ONLY (2026-08-18,
     Hannah, mobile): the desktop side card stands beside the real face, so a
     thumbnail there would say the same thing twice. Sprite tile via
     background-position, inline styles set per person in ui.js mountCard. */
  .j-card-ava {
    display: none;
    width: 2.15rem;
    height: 2.15rem;
    border-radius: 50%;
    border: 1px solid rgba(217, 164, 65, 0.4);
    background-repeat: no-repeat;
    vertical-align: -0.6rem;
    margin-right: 0.6rem;
  }
  .j-card.sheet .j-card-ava { display: inline-block; }

  /* The role, worn as a TAG (2026-08-18, Hannah) — same pill language as the
     site's chips: ember border, whisper of fill, tracking caps. It sits tight
     under the name so name-and-role read as one label beside the face. */
  .j-card-tag {
    display: inline-block;
    margin: 0 0 0.8rem;
    padding: 0.24em 0.85em 0.26em;
    border: 1px solid rgba(217, 164, 65, 0.45);
    border-radius: 999px;
    font-size: 0.64rem;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--gold-bright);
    background: rgba(217, 164, 65, 0.08);
  }
  .j-card-claim { margin: 0 0 0.8rem; color: var(--gold-bright); font-size: 1.05rem; }
  .j-card-p { margin: 0 0 0.8rem; font-size: 0.9rem; line-height: 1.6; color: var(--muted); }
  .j-card-status { margin: 0.6rem 0 0.9rem; font-size: 0.75rem; letter-spacing: 0.05em; color: rgba(201, 191, 168, 0.62); }
  .j-card-link {
    display: inline-block;
    color: var(--gold-bright);
    text-decoration: none;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    border-bottom: 1px solid rgba(217, 164, 65, 0.5);
  }

  /* The polite live region (journey/ui.js) — announced, never seen. `clip-path`
     rather than display:none or visibility:hidden, both of which would take it
     out of the a11y tree along with the announcement. */
  .j-live {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* ============================================================
     BOTTOM SHEET (PL-1.3) — the same dialog, re-formed.

     On a coarse pointer or a narrow viewport the detail card is not a
     shrunken side panel: it comes up from the bottom edge, gets a grab
     handle, and scrolls INSIDE itself. The shell stops scrolling and
     becomes a flex column so the handle and the close control stay put
     while the body moves under them — which is also what makes the
     drag-to-dismiss gesture unambiguous (the handle never scrolls away).

     journey/ui.js adds `.sheet` at open time from the live pointer/viewport
     condition, and registers the card with journey/scroll.js's input-owner
     registry so the journey's window-capture preventDefault leaves this
     subtree alone and the native scroll below actually runs.
     ============================================================ */
  .j-card-grip { display: none; }

  .j-card.sheet {
    left: 0; right: 0; bottom: 0;
    top: auto;
    width: auto;
    max-width: none;
    max-height: min(78vh, 32rem);
    transform: none;
    display: flex;
    flex-direction: column;
    overflow: hidden;                       /* the BODY scrolls, not the shell */
    padding: 0;
    border-width: 1px 0 0;
    border-radius: 16px 16px 0 0;
    /* A side card sits beside the composition; a sheet sits ON it and covers
       the chapter copy, so the 0.9 panel let two texts read through each
       other. Opaque enough that the body copy keeps its authored contrast
       (PL-2.3) without becoming a flat black slab. */
    background: rgba(10, 7, 2, 0.975);
    box-shadow: 0 -18px 60px rgba(0, 0, 0, 0.6);
    transition: opacity 0.3s ease, transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
  }
  /* The shared shell motion is safe on sheets, but the node-contact filament
     is not: a bottom-edge sheet has no single world node to point back to. */
  .j-card.sheet::before { content: none; }
  /* 1:1 with the pointer while the finger is down — no easing to fight */
  .j-card.sheet.dragging { transition: none; }

  .j-card.sheet .j-card-grip {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    min-height: 44px;                       /* PL-1.4: a real drag target */
    touch-action: none;                     /* the gesture is ours, not the page's */
    cursor: grab;
  }
  .j-card.sheet.dragging .j-card-grip { cursor: grabbing; }
  .j-card.sheet .j-card-grip::before {
    content: "";
    width: 44px; height: 4px;
    border-radius: 99px;
    background: rgba(217, 164, 65, 0.45);
  }
  .j-card.sheet .j-card-body {
    flex: 1 1 auto;
    min-height: 0;                          /* or the flex child refuses to scroll */
    overflow-y: auto;
    overscroll-behavior: contain;           /* no scroll chaining out to the journey */
    -webkit-overflow-scrolling: touch;
    /* the bottom padding is load-bearing, not decoration: the LAST control in
       the body carries a 44px hit area that reaches ~22px below its text, and
       a scroll container clips hit-testing at its own padding box. Without it
       the final link's enlarged target is silently cut in half. */
    padding: 0.1rem 1.5rem 1.5rem;
  }
  .j-card.sheet::after {                    /* safe-area / home-indicator gutter */
    content: "";
    flex: 0 0 auto;
    height: max(1.1rem, env(safe-area-inset-bottom, 0px));
  }
  /* the close control sits IN the handle row, aligned to it, so the two read
     as one header strip and the body starts immediately below both */
  .j-card.sheet .j-card-x {
    top: 0; right: 0.3rem;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px; height: 44px;      /* PL-1.4, in the box rather than a pseudo:
                                       a pseudo would overhang the sheet's own
                                       right edge and land off-viewport */
    padding: 0;
  }

  /* --- focus visibility across the whole journey layer (PL-2.2) ---------
     "Focus states visually equal hover states": every rule above pairs
     :focus-visible with :hover. This is the ring on top of that, one
     treatment for every journey control, matching the static journey. The
     hero region is untouched — its own focus debt is recorded, not patched
     from here. */
  .j-rail :is(a, button):focus-visible,
  .j-menu :is(a, button):focus-visible,
  .j-act:focus-visible,
  .j-hot:focus-visible,
  .j-pop a:focus-visible,
  .j-card :is(a, button):focus-visible {
    outline: 2px solid var(--gold-bright);
    outline-offset: 3px;
    border-radius: 2px;
  }
  .j-menu .j-menu-x:focus-visible,
  .j-menu .j-menu-links a:focus-visible { border-radius: 50%; }
  /* The shared ring above sets `border-radius: 2px`, which is right for the
     square controls it was written for and would flatten a pill. The outline
     follows the element's radius, so the pair states its own. */
  .j-act:focus-visible { border-radius: 999px; }
  .j-card-x:is(:hover, :focus-visible) { color: var(--gold-bright); }
  .j-card-link:is(:hover, :focus-visible) { border-bottom-color: var(--gold-bright); }

  @media (max-width: 900px) {
    /* PL-1.4 again: the pair's desktop box is ~37px tall. The pills keep their
       silhouette and gain the touch minimum in height only, so they still read
       as the hero's pill rather than as two slabs. */
    .j-act { min-height: 44px; }
    .j-block { max-width: none; width: 88vw; }
    .j-block.pos-left { left: 6vw; }
    .j-block.pos-bottomleft { left: 6vw; bottom: 11vh; }
    .j-block.pos-topright   { right: 6vw; top: 11vh; }
    .j-block.pos-bottom, .j-block.pos-topcentre { left: 50%; }
    .j-card { right: 50%; transform: translate(50%, -50%); }
  }

  /* ============================================================
     PL-1.4 — 44px minimum touch targets.

     "Decorative density may drop to preserve 44px minimum touch targets" —
     but the targets themselves are grown WITHOUT growing the artwork. Each
     control gets a transparent pseudo-element centred on it, sized to
     max(its own box, 44px), so the hit area reaches the minimum while the
     type, the pill and the dot render at exactly the size they were
     designed at. `max()` means a control that is already big enough is not
     given a hit area larger than itself, which would steal presses from
     whatever sits next to it.

     Same media condition as the bottom sheet (journey/ui.js `sheetQuery`), so
     the touch form and the touch targets are never out of step.
     ============================================================ */
  @media (pointer: coarse), (max-width: 720px) {
    /* Only the controls that are `static` by default need a containing block.
       .j-hot and .j-card-x are absolute — a blanket `position: relative` here
       would RESET those (equal specificity, later in the sheet) and, in the
       bottom sheet, turn the close button into a full-width flex item. The
       side navigator's own tiles are 44px tall by construction (see
       .j-rail-item) and need nothing from this block. */
    .j-card-link, .j-pop-link { position: relative; }
    .j-menu-links a, .j-menu-ia { position: relative; }
    .j-menu-links a::before,
    .j-menu-ia::before,
    .j-hot::before,
    .j-card-x::before,
    .j-card-link::before,
    .j-pop-link::before {
      content: "";
      position: absolute;
      left: 50%; top: 50%;
      width: max(100%, 44px);
      height: max(100%, 44px);
      transform: translate(-50%, -50%);
      /* hit area only: it must never paint, and it must never sit above the
         control's own text for selection purposes */
      background: none;
      pointer-events: auto;
    }
    /* ...with one exception, added 2026-08-06 (report A, the mobile half).
       A hover-only chip draws NOTHING at rest, so `max(100%, 44px)` on its
       pill is a 306x44 invisible bar lying beside the node — and because this
       rule states `pointer-events: auto` outright, it overrode the pill's own
       `none` and put that bar back on top of the neighbouring node's face.
       Measured at 375x812: contributor-8's pad was covered by contributor-9's,
       2 of 13 sample points on the drawn face reached the right chip.
       While the chip shows nothing, the target is the node's own pad, which
       ui.js floors at 44px under this same query so PL-1.4 still holds. The
       moment the chip is hot — showing a label worth aiming at — its full
       touch target comes back. */
    .j-hot.label-hover:not(:hover):not(.hot):not(:focus-visible)::before {
      pointer-events: none;
    }
    /* --- the side navigator at phone widths ---------------------------------
       The rail was the FIX for this query's original problem, not a new
       victim of it: the old `.j-nav` sat in the hero's one flex row
       (wordmark | chapters | 2RP+Discord), which added up to ~556px at 375
       and pushed the last chapter entry and both hero CTAs off-screen
       entirely. It needed a fixed full-width band of its own to be reachable
       at all. A side rail is off that row by construction, so the hero row is
       back to wordmark + CTAs (~315px, fits) with nothing lifted out of it.

       What phone width does change is the EXPANSION. There is no hover here,
       so the expanded state is always a deliberate tap (rail.js's touch
       model), every name arrives with it (no hover to reveal them one at a
       time), and while it is open the band is a panel over the composition
       rather than a hint beside it — hence the fuller scrim and the tighter
       tracking. Deliberately NOT solved here: the broader mobile layout pass. */
    .j-rail-inner::before {
      /* FULLY opaque under the tiles, not merely dark. At 375 the expanded
         band unavoidably lies across the chapter copy — there is no width
         left to sit beside it — so it stops pretending to be a scrim and
         becomes a panel for as long as the visitor holds it open. At 0.96 the
         copy ghosted through the names; measured, "Learn more" was still
         legible under CONNECT.

         SIZED TO THE ICON COLUMN (2026-08-10). Hannah: "the black thing that
         shows over the open menu spreads out far too much to the left — it
         should just cover the icons." At the inherited 6rem the opaque run
         reached 73 px from the edge and its feather 96 px — nearly twice the
         52 px tile column, a black flag flying left of the marks (measured at
         375x812: opaque to x 302, feather to x 279, tiles at x 323..375).
         4rem puts the opaque run (68%) at 43.5 px — the tiles' own footprint
         with the marks at x -38..-14 fully inside — and the feather's end at
         64 px. The names never needed it: each carries its own 0.85 scrim
         pill (the desktop mechanism), and the chapter copy behind them is at
         0.14 while the fan is open (body.j-rail-on below), so the band's only
         remaining job at phone width is exactly the marks — same as the
         resting halo it grows out of. The legibility win from 48b7795 stays:
         the run under the tiles is still #090602, opaque, not merely dark. */
      width: 4rem;
      background: linear-gradient(to left, #090602 68%, rgba(9, 6, 2, 0));
    }
    .j-rail-name { font-size: 0.58rem; letter-spacing: 0.22em; }
    .j-rail-item:is(:hover, :focus-visible) .j-rail-name,
    .j-rail-menu:is(:hover, :focus-visible) .j-rail-name,
    .j-rail-open .j-rail-name { letter-spacing: 0.18em; }
    .j-copy { transition: opacity 0.28s ease; }
    body.j-rail-on .j-copy { opacity: 0.14; }
    .j-hotspots { transition: opacity 0.28s ease; }
    body.j-rail-on .j-hotspots { opacity: 0.14; }
  }

  /* MOBILE IS A FIXED FILE, NOT A ROTATING INSTRUMENT. At rest its current
     mark is the centred section-selector trigger, with the separated Menu
     control directly beneath it. The former fans out the complete manifest-
     ordered list; the latter opens the site-map sidebar immediately. The
     rail's full-height flex container centres the expanded file as one stable
     object. Progress may update --cur for desktop; coarse layout ignores it. */
  @media (pointer: coarse) {
    .j-rail-inner {
      width: 52px;
      height: calc(var(--n, 5) * 44px + 52px);
    }
    .j-rail-slot {
      top: calc(50% - 22px);
      transform: none;
      transition: top 0.34s cubic-bezier(0.22, 1, 0.36, 1),
                  opacity 0.3s ease;
    }
    .j-rail:is(:has(:focus-visible), .j-rail-open) .j-rail-slot {
      top: calc(var(--i, 0) * 44px);
      transform: none;
    }
    .j-rail-menu {
      /* Current tile ends at centre + 22px; Menu begins 8px later, with its
         existing ::before hairline sitting inside that breathing room. */
      top: calc(50% + 30px);
      transform: none;
      opacity: 1;
      pointer-events: auto !important;
      transition: top 0.34s cubic-bezier(0.22, 1, 0.36, 1),
                  opacity 0.22s ease, color 0.3s;
    }
    .j-rail:is(:has(:focus-visible), .j-rail-open) .j-rail-menu {
      top: calc(var(--n, 5) * 44px + 8px);
      transform: none;
      opacity: 1;
    }
    /* Permanent navigation must not claim or grade the composition. The
       glyph rims provide local contrast without a black edge panel, and the
       copy/hotspots retain the opacity their journey envelopes authored. */
    .j-rail-inner::before,
    .j-rail-inner::after { display: none; }
    body.j-rail-on .j-copy,
    body.j-rail-on .j-hotspots { opacity: 1; }
  }

  /* ============================================================
     FINAL HEADING AT PHONE WIDTHS — THREE LINES AT THE HOUSE SCALE.
     (2026-08-07 mobile pass. Supersedes the two-line step-down that
     shipped here, and answers the question that rule flagged.)

     The geometry the old rule measured is still true and worth
     keeping on the record: at 375x812 the heading's natural
     single-line width is 839px at the house 1.9rem (30.4px) floor,
     the best possible TWO-line break needs 419px of column and the
     balanced one 434px, and a 375px phone can offer ~320px once the
     block's padding is paid. Two lines really is unreachable here.

     What was wrong was the conclusion. Buying two lines cost a
     font step-down to 5.6vw — 21.0px at 375 — against a 16px sub,
     i.e. a heading/sub ratio of 1.31 where every other chapter on
     the same phone runs 30.4/16 = 1.90. The epilogue is the one
     place the site raises its voice, and it was arriving in the
     smallest display type on the page. A reader does not count
     lines; they read hierarchy, and that hierarchy was gone.

     So the line count gives way instead. The heading keeps the
     house clamp, which floors at 1.9rem across the whole phone
     range, and runs to THREE lines:

       375:  30.4px, 3 lines, longest ~285px into 320px of column
       430:  30.4px, 3 lines, longest ~330px into 372px of column

     `text-wrap: balance` is what makes three lines read as a
     stanza rather than as two long lines and a stub — it is also
     what fixes the landscape-phone rag, which broke 601px / 229px
     before this. Scoped to the phone/short queries: desktop's own
     two-line break is already even and is left exactly as shipped.

     The block still widens to 94vw with trimmed side padding —
     that is what keeps three lines from becoming four. `pos-
     bottomleft` is anchored by its BOTTOM edge, so the taller
     heading grows upward into open sky and the block's bottom, the
     footer cue under it and the safe area are all where they were.
     ============================================================ */
  @media (max-width: 480px) {
    /* THE RAIL'S LANE. `pos-bottomleft` is the site's only LEFT-ALIGNED copy
       block, and the taller heading above brings its first line up into the
       band the side navigator rests in — measured at 375x812, the heading's
       first glyph starts at x 39 and the resting mark occupies x 14..38 at
       y 465..478. One pixel apart, and only by luck.

       23-side-navigator.md §8 made exactly this call for the static tier
       ("`.chapter` gained an 11rem left gutter") on the grounds that Tier 1's
       blocks "sit out in the middle of a full-bleed scene and never come near
       the rail". That was true of a 21px two-line epilogue. It is not true of
       this one, so Tier 1 takes the same medicine at phone widths, sized to
       the resting tile (44px) plus air rather than to the expanded band —
       expanding is a deliberate, transient tap and is already a panel over the
       composition by design.

       It costs nothing: the greedy break is set by the longest word group
       (230px), so widening the left padding from 1rem to 3.2rem leaves 289px
       of column against a 230px longest line. Same four lines, same balance,
       the mark simply has the margin to itself. */
    /* Final's phone composition now belongs decisively to the lower-left.
       The rail moved to the right in the navigation redux, so the old 3.2rem
       left gutter was protecting a lane that no longer exists. Reclaim it for
       the epilogue: the wider measure gives the heading a calmer three-line
       shape, while the lower anchor leaves the field to curl over the copy
       instead of leaving both elements stranded around the frame's middle. */
    .j-block.pos-bottomleft {
      left: 0;
      bottom: 6vh;
      width: 100vw;
      padding-left: 1.5rem;
      padding-right: 1.5rem;
    }
    .j-block.pos-bottomleft .j-h { text-wrap: balance; }
    /* the hero makes the same trim to `.pill` at this width — the pair follows
       its own vocabulary rather than inventing a second phone size */
    .j-act { padding: 0.42rem 1.1rem; font-size: 0.72rem; }
    .j-actions { margin-top: 1.25rem; gap: 0.6rem; }
  }

  /* ============================================================
     THE LANDSCAPE PHONE (2026-08-07 mobile pass).

     A phone on its side is not a small desktop and not a tall
     phone: it is ~812x375, and the only scarce axis is HEIGHT.
     Everything above this point sizes the copy off WIDTH, so at
     812 the `max-width: 900px` rule handed the block 88vw = 715px
     of a 812px frame while the viewport had 375px of height to
     give — a block 246px tall, 66% of the frame, centred in it.

     That is not only a bad picture. `.j-block`'s BOX is what
     ui.js's hotspot suppression tests against, and the box is
     much wider than the text inside it (the sub is capped at
     26rem and centres). Measured at 812x375 before this rule:
     Owned projected 16 of 16 contributors inside the frame — the
     landscape frustum is wide enough for the whole arc, the one
     orientation where it is — and then SUPPRESSED 13 of them
     behind a copy box whose right two-fifths was empty padding.
     The best composition on the site for that chapter was the one
     you could interact with least.

     So the block stops taking width it has no text for. 34rem is
     the `.j-sub` measure (26rem) plus its padding, i.e. the width
     the copy actually occupies, and the heading rebalances into
     it. Height is unchanged: this buys the frame's flanks back,
     which is exactly where the arc's outer nodes live.

     Keyed on height + orientation rather than on `pointer:
     coarse`, because the shape of the frame is the problem — a
     short desktop window has it too and gets the same answer.
     ============================================================ */
  @media (max-height: 520px) and (orientation: landscape) {
    .j-block { width: min(88vw, 34rem); }
    .j-block.pos-bottomleft { width: min(88vw, 40rem); max-width: 40rem; }
    .j-block .j-h { text-wrap: balance; }
    /* the vertical rhythm pays for the short frame, not the type size:
       the copy stays at the house scale and simply sits tighter */
    .j-h { margin-bottom: 0.6rem; }
    .j-actions { margin-top: 1rem; }
    .j-block.pos-bottom { bottom: 6vh; }
    .j-block.pos-topright, .j-block.pos-topcentre { top: 8vh; }
    .j-block.pos-bottomleft { bottom: 7vh; }
    /* the fanned band is taller than a short frame; trim the feather's
       overhang so the scrim spends its height on the tiles, not the air */
    .j-rail-inner::before {
      top: calc(var(--cur, 0) * -44px - 22px - 1.2rem);
      height: calc(var(--n, 5) * 44px + 52px + 2.4rem);
    }
  }

  /* ============================================================
     THE HALF MOON (Hannah, 2026-08-13 later still)

       "Can you switch the right side navigator to be more like
        this — a half moon. You may have to rethink how the entry
        animation and how it moves. It should stay in the side on
        hover. And it should be like a half moon. The entry
        animation right now is a little bit janky or something — we
        should have a more elegant version of that. So it's just
        like the half moon kind of forms around it, with the
        current one highlighted on the left side. And currently
        there's like a black background that appears on hover, but
        it feels really janky and weird … It looks like maybe
        there's multiple layers of black boxes, like a bigger one
        and a smaller one … This should be really subtle and should
        fade in and out nicely — if we even have anything."

     THREE CHANGES, and the first one pays for the second.

     ---- 1. THE SHAPE: A HALF MOON, AND IT STAYS AT THE WALL ----
     `d9da652` drew the WHOLE circle around the button. A whole
     circle around a button 2px from the frame does not fit — its
     right-hand arc is off screen — so that build bought the room
     by STEPPING THE WHOLE CONTROL IN off the wall by 46.93px on
     every open and settling it back on every close. "It should
     stay in the side on hover" is exactly what that forbids.

     Keep the circle and the centre; inhabit only its LEFT HALF.
     Every angle the marks occupy has `cos(ang) >= 0`, so every x
     is `<= 0` relative to the hub and the two extreme marks sit at
     the button's OWN x, straight above and straight below it. The
     open control's rightmost tile edge is then `TILE/2` = 24px
     right of the hub against the closed button's own 28px half-box:
     THE OPEN MOON IS NARROWER THAN THE CLOSED BUTTON. It fits
     inside the footprint it already had, at any radius, for any
     manifest, and the control never moves again in any state.

     Deleted with the step, rather than managed: `--cl-shift` and
     every rule that carried it, and `.j-rail-menu::before`, the pad
     that existed only to answer for the footprint the stepping
     button vacated. A press cannot be lost by a control that does
     not move — which is `48b7795`'s guarantee held structurally
     rather than defended. (The `pointerdown` opening stays anyway:
     it is simply the right behaviour for a menu button.)

       the hub    the menu button, 56px, `--cl-edge` off the frame.
                  The moon's CENTRE, and motionless in every state.
       the apex   the circle's LEFTMOST point — the tip of the moon,
                  pointing into the frame. A POSITION, not an item:
                  whichever chapter is current occupies it. "The
                  current one highlighted on the left side."
       the moon   the 180deg arc about the hub, from straight up
                  through the apex to straight down. Marks sit on it
                  at a pitch of 180/(n-1) and read top-to-bottom in
                  manifest order.

         x = -rad * cos(ang)      ang 0 -> -rad, the apex
         y =  rad * sin(ang)      positive is DOWN

     ---- 2. THE FORMATION: ONE GESTURE, NOT TWO STAGES ----
     The old entry was serial — the control lurched in off the wall
     and the current mark walked out to its slot (0.30s), and only
     THEN did the others begin (a further 0.55s). Two bursts with a
     pause between them, on top of a sideways lurch: that is the
     jank, and none of it was rescuable by tuning.

     The moon FORMS instead. `--t` (the radius) and `--u` (the
     angle) run on ONE clock with ONE duration, so each mark leaves
     the button on a single curved path that lands on its own point
     of the arc — and the paths are staggered by DISTANCE ALONG THE
     ARC (`--step`: 0 for the apex, 1 for its neighbours, 2 for the
     tips), so the moon opens outward from the current section
     instead of appearing all at once.

     And the arc DRAWS. `.j-rail-arc` is two real paths, both
     starting at the apex, walked open by `stroke-dashoffset` — the
     hero's own `lead-draw` vocabulary, the same one the menu mark's
     filaments use. The drawing front leaves the current section in
     both directions and the marks arrive as it reaches them, so
     "the half moon kind of forms around it" is what the frames
     actually show rather than a description of a fade.

     One clock, one direction, no pause: `--cl-travel` is a mark's
     own journey and `--cl-cascade` is what one step of arc distance
     costs. The whole formation is `travel + 2 * cascade`.

     ---- 3. THE BACKING: ONE SURFACE, OR NONE ----
     What she was looking at was THREE dark surfaces at once —
     `.j-rail-inner::before` (a 336px radial at 0.80) and
     `::after` (a 220px radial at 0.62) both live and SUMMING to
     0.92 at the core over two different falloffs, plus every name
     pill's `rgba(9,6,2,0.85)` rectangle with a 2px radius. "A
     bigger one and a smaller one" is a literal reading of the two
     radials; "a hard line" is the pill.

     There is now exactly ONE surface on this flank, `::after`, and
     it is the same surface in both states — it simply GROWS with
     the control it protects, from the button's own footprint to the
     moon's, on the formation's clock. `::before` is gone (the ring
     block's job was to be the second one). The stops are a smooth
     falloff to zero rather than a flat core and two straight
     segments, so there is no radius at which the eye finds an edge.

     It is needed: the ride runs from a near-black sky to a lit
     ground network, and 25-navigation-redux.md §11(4) records the
     current mark disappearing outright on the epilogue's field. The
     measurement for THIS treatment is in the same file's half-moon
     entry — the glyphs' own dark rims (`.j-sym`'s stacked drop
     shadows) carry the stroke, and the surface only has to calm the
     field behind them, which is why it can be this quiet.

     The name pills lose their rectangle for the same reason and
     keep their legibility the same way: a soft elliptical wash that
     reaches zero at the box edge, so a label has a ground without
     having a box.

     ---- WHAT IS UNCHANGED ----
     The 120ms dwell, the `pointerdown` menu, `.at` beating
     `:hover`, the pointer floor, the reticle, the a11y model, and
     the COLUMN — this whole block is an override under
     `(hover: hover)`, that query's own definition negated, so touch
     keeps the behaviour it has.
     ============================================================ */
  @property --ang { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
  @property --t { syntax: "<number>"; inherits: false; initial-value: 0; }
  @property --u { syntax: "<number>"; inherits: false; initial-value: 0; }

  @media (hover: hover) {
    .j-rail:not(.j-rail-column) {
      --cl-tile: 48px;            /* a section's box — 44px is the floor */
      --cl-hub: 56px;             /* the button, and it leads the pair */
      /* THE EDGE HUG, and now it is unconditional. 2px of box puts the
         button at x 1382..1438 of 1440 and its 28px glyph at 1398..1426,
         i.e. 14px of air from glyph to frame. Nothing takes it off this
         mark any more — the open moon is narrower than the closed button. */
      --cl-edge: 2px;
      --cl-rad: 96.70px;          /* fallback; rail.js derives and writes it */
      /* how close a name pill sits to the tile it names, on whichever side of
         it the pill sits (EVERY NAME SITS ON ITS OWN MARK, below). rail.js
         knows this number as HUG, and uses it to work out which pills fit
         beside their marks and which have to leave the row.
         0.56rem, down from the 0.72 the one-vertical-line rule used. "All the
         buttons should in general probably hug their current setting a little
         bit more" is a sentence about EVERY pill, and the apex's distance is
         the one the row rule alone does not change — so it is changed here.
         Shot at 0.90 / 0.72 / 0.56 / 0.44 against the reticle the current tile
         wears, measuring the pill box's edge to the bracket: 20.5 / 18.5 / 16
         / 14 px. At 0.90 the label reads as floating beside the mark rather
         than belonging to it; at 0.44 the word is visibly crowding the
         bracket. 0.56 is the tightest setting that still reads as air. */
      --cl-hug: 0.56rem;
      /* the marks' scale while still INSIDE the button. They grow as they
         come out of it, which is the whole of what "emerge from the button"
         can mean for a drawing that has no thickness. */
      --cl-emit: 0.62;
      /* ONE mark's own journey out of the button and onto its point... */
      --cl-travel: 0.40s;
      /* ...and what one step of distance along the arc costs it. The apex
         goes first, its neighbours a cascade later, the tips a cascade after
         that, so the moon opens outward from where the visitor is. Shot at
         0.09 and 0.13 against the same travel: at 0.09 the three beats overlap
         enough that the tips are already leaving while the neighbours are
         mid-flight, and the eye reads one bloom rather than an unrolling.
         0.13 separates them — each pair visibly follows the one in front —
         and costs 80ms on a formation that still settles inside 0.66s. */
      --cl-cascade: 0.13s;
    }
    /* The anchor is the BUTTON'S CENTRE — the moon's centre, and the point
       every slot is placed from. It stands `--cl-edge` off the wall and it
       STAYS THERE: no transform, no transition, nothing to lurch. */
    .j-rail:not(.j-rail-column) .j-rail-inner {
      width: 0;
      margin-right: calc(var(--cl-edge) + var(--cl-hub) / 2);
    }

    /* ---- the tiles ----
       48px square, centred on the anchor and moved only by the transform
       below. Square and 48 rather than 44 because PL-1.4's touch minimum
       should be a floor a control clears, not one it meets. Points on an arc
       have air between them by definition, so what the cluster's tiling
       promise actually protected — an open control must not fold because the
       pointer crossed a seam — is kept by the pointer floor below. */
    .j-rail:not(.j-rail-column) .j-rail-item { width: var(--cl-tile); height: var(--cl-tile); }
    .j-rail:not(.j-rail-column) .j-rail-slot {
      top: calc(var(--cl-tile) / -2);
      right: calc(var(--cl-tile) / -2);
      --ang: var(--ang-to, 0deg);
      --t: 0;
      --u: 0;
      /* THE POLAR POSITION, ABOUT THE HUB. Two scalars ride it:

           --t  THE EMERGENCE scales the RADIUS, 0 (dead centre, inside the
                button) to full. At rest every mark is inside the control and
                the resting picture is one button, nothing else.
           --u  THE UNFURL scales the ANGLE, so 0 leaves every slot at angle
                0 — at the apex, where the current one stays — and 1 puts
                each on its own point of the arc.

         Running them on ONE clock is the change: a mark leaves the button
         and lands on its point along a single curved path, rather than
         walking out to the apex and then, after a wait, setting off round.
         The current item's angle is 0, so `-rad*cos(0)` is `-rad` for every
         value of `--u`: it is the fixed point of the formation and of every
         chapter change, and it is not special-cased anywhere. */
      --ae: calc(var(--u) * var(--ang));
      transform: translate(
        calc(-1 * var(--t) * var(--cl-rad) * cos(var(--ae))),
        calc(var(--t) * var(--cl-rad) * sin(var(--ae))));
      /* `transform` is deliberately NOT in this list: it is a pure function of
         the registered properties, and transitioning it as well would
         interpolate a matrix towards a moving target. */
      transition-property: --ang, --t, --u, opacity;
      /* THE FOLD is the formation backwards, and it closes from the TIPS IN:
         `(2 - step)` reverses the cascade, so the far marks retract first and
         the current one is the last thing to go home — the mirror of the moon
         opening outward from it. */
      transition-duration:
        0.46s, var(--cl-travel), var(--cl-travel), 0.24s;
      transition-delay:
        0s,
        calc((2 - min(var(--step, 0), 2)) * var(--cl-cascade)),
        calc((2 - min(var(--step, 0), 2)) * var(--cl-cascade)),
        calc((2 - min(var(--step, 0), 2)) * var(--cl-cascade));
      /* NEITHER THE TURN NOR THE FORMATION MAY USE THE HOUSE EASE, and the
         second half of that is this pass's finding. `0.22, 1, 0.36, 1` is an
         ARRIVAL curve — it spends 90% of the distance in the first quarter of
         the time — which is right for a thing that should already be there and
         wrong for a thing that should be seen to happen. Shot at both
         (candidates F1 and F2): on the house curve the whole moon is
         effectively formed by 240ms of a 600ms transition and the remaining
         360ms is a settle nobody can see, which is precisely the "happened at
         once" reading the cluster pass recorded for the turn. On a real
         ease-in-out every mark is caught mid-flight in the frames between,
         and the formation occupies the time it is given.
         The turn keeps its own slightly softer entry for the same reason it
         always had one. */
      transition-timing-function:
        cubic-bezier(0.42, 0.02, 0.22, 1),
        cubic-bezier(0.38, 0.04, 0.24, 1),
        cubic-bezier(0.38, 0.04, 0.24, 1),
        ease;
    }
    /* ---- AT REST THERE IS NO MARK AT ALL ----
       The column lights its current tile at rest; here every slot is dark and
       dimensionless inside the button until the control opens. This has to
       out-weigh `.j-rail-slot.now` (0,2,0) and to LOSE to the open rule below,
       so it is written at the same weight as that rule and placed before it. */
    .j-rail:not(.j-rail-column) .j-rail-slot.now { opacity: 0; }
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-slot {
      --t: 1;
      --u: 1;
      opacity: 1;
      transition-duration:
        0.46s, var(--cl-travel), var(--cl-travel), 0.26s;
      /* THE MOON OPENS OUTWARD FROM THE CURRENT SECTION. One delay, shared by
         the position and the opacity, so a mark fades up exactly as it starts
         to travel and there is no mark visible anywhere it is not going. */
      transition-delay:
        0s,
        calc(min(var(--step, 0), 2) * var(--cl-cascade)),
        calc(min(var(--step, 0), 2) * var(--cl-cascade)),
        calc(min(var(--step, 0), 2) * var(--cl-cascade));
    }
    /* Once the intro formation has landed, the rail becomes a scroll
       instrument: angle and edge visibility are direct functions of journey
       progress, not a second time-based animation chasing behind the hand. */
    .j-rail:not(.j-rail-column).j-rail-following .j-rail-slot {
      opacity: var(--rail-follow-opacity, 1);
      transition-duration: 0s;
      transition-delay: 0s;
    }
    .j-rail:not(.j-rail-column).j-rail-following
      .j-rail-slot.j-rail-recycle .j-rail-mark > .j-sym {
      transform: translateY(var(--rail-recycle-y, 0px)) scaleX(var(--j-sym-flip, 1));
    }
    .j-rail:not(.j-rail-column).j-rail-between .j-rail-name {
      opacity: 0 !important;
    }
    /* ---- RESTATING THE ANGLE IS NOT A MOVE ----
       rail.js re-canonicalises `--ang-to` while the moon is HOME — folded and
       finished folding, so `--u` is 0, `--ae` is 0 whatever the angle says,
       and nothing this property controls is on screen. Without this the
       restatement would still run the 0.46s `--ang` transition, and a visitor
       who opened the moon inside that window would catch the angle between
       two values and see the mark set off for a point it was already leaving.
       Held for exactly one frame by the class, so the turn keeps its
       transition in the only state that can show it: open.
       (`--ang` is first in the duration list; the rest are named too because
       at rest they are all already at their resting values, so zeroing them
       for a frame can have no effect — and listing them keeps this rule from
       silently depending on the order of that list.) */
    .j-rail:not(.j-rail-column).j-rail-recentre .j-rail-slot {
      transition-duration: 0s;
    }
    /* THE KEYBOARD DOES NOT WAIT FOR THE CASCADE. The first Tab into the rail
       opens it and lands on a mark that is, for the length of its own delay,
       still inside the button at opacity 0 — a focus ring on nothing. Focus is
       not a dwell and has no transit to guard against, so the focused mark is
       lit from the first frame and travels out already visible. Position is
       untouched: it still rides the same cascade as its neighbours. */
    .j-rail:not(.j-rail-column) .j-rail-slot:has(:focus-visible) {
      opacity: 1;
      /* …and it has to be lit FAST, not merely undelayed. Zeroing the delay
         alone still left the mark ramping up a 0.26s fade: measured 50ms after
         the first Tab, opacity 0.051 at the hub — undelayed and still nothing
         on screen. The focused mark takes the short fade instead, which puts
         it at ~0.5 by the time the eye arrives. */
      transition-duration:
        0.46s, var(--cl-travel), var(--cl-travel), 0.1s;
      transition-delay:
        0s,
        calc(min(var(--step, 0), 2) * var(--cl-cascade)),
        calc(min(var(--step, 0), 2) * var(--cl-cascade)),
        0s;
    }

    /* ---- the marks grow as they come out of the button ----
       The mark scales, not the tile: the box stays 48px in every state, so a
       slot is never a sub-minimum target. `--cl-emit` is the size a mark has
       while it is still inside the control; it reaches full size as it arrives
       on the arc, which is the only way a flat drawing can be said to come out
       of something. It rides the same cascade as the travel it belongs to. */
    .j-rail:not(.j-rail-column) .j-rail-slot .j-rail-mark {
      transform: scale(var(--cl-emit));
      transition: transform var(--cl-travel) cubic-bezier(0.38, 0.04, 0.24, 1)
                  calc((2 - min(var(--step, 0), 2)) * var(--cl-cascade));
    }
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-slot .j-rail-mark {
      transform: scale(1);
      transition-delay: calc(min(var(--step, 0), 2) * var(--cl-cascade));
    }

    /* ---- the hub: the moon's centre, and it leads ---- */
    .j-rail:not(.j-rail-column) .j-rail-menu,
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-menu {
      top: calc(var(--cl-hub) / -2);
      right: calc(var(--cl-hub) / -2);
      width: var(--cl-hub);
      height: var(--cl-hub);
      transform: none;
    }
    /* The button's box and its glyph both lead the pair: it is one instrument
       with a control at its centre, not a list with a sixth item. The sections
       stay at 24px — they are a list, and the thing you press is not.
       Shot 2026-08-13 at 24 / 26 / 28 / 30px in the real row: at 24 and 26 the
       list glyph is subordinate to the marks around it, which is backwards for
       the one thing on this flank a visitor presses; at 30 the bullets coarsen
       against a page drawn in hairlines. 28 is where the button leads and is
       still a drawing. */
    .j-rail:not(.j-rail-column) .j-rail-menu .j-rail-mark,
    .j-rail:not(.j-rail-column) .j-rail-menu .j-sym { width: 28px; height: 28px; }

    /* THE RETICLE WAITS FOR THE LANDING. It says "this is the one you are on",
       and a mark still in flight is not on anything yet. The apex mark's own
       travel is `--cl-travel` with no cascade (it is step 0), so that is
       exactly how long the brackets hold off: they close on the mark as it
       arrives, which reads as the mark being CLAIMED rather than dragged. */
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-slot.active .j-rck {
      transition-delay: var(--cl-travel);
    }
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-slot.active .j-rck.tr { transition-delay: calc(var(--cl-travel) + 0.05s); }
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-slot.active .j-rck.br { transition-delay: calc(var(--cl-travel) + 0.10s); }
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-slot.active .j-rck.bl { transition-delay: calc(var(--cl-travel) + 0.15s); }

    /* THE BUTTON'S HAIRLINE AND ITS HIT BRIDGE BOTH HAVE NO JOB LEFT.
       The hairline separated "sections above" from "the site below" in a
       column that no longer exists here; the 8px bridge spanned a gap the arc
       does not have; and the pad `d9da652` made of `::before` answered for a
       footprint the button vacated when it stepped in — it does not step in
       any more, so there is nothing to answer for. */
    .j-rail:not(.j-rail-column) .j-rail-menu::before,
    .j-rail:not(.j-rail-column) .j-rail-menu::after { display: none; }
    /* …and the moon paints over the button. The list is authored before the
       button, so without this the button sits on top of every mark. */
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-slot { z-index: 1; }

    /* ---- THE MOON ITSELF, AND IT DRAWS ----
       Two real paths (rail.js), both starting at the APEX: one walking up to
       the top tip, one down to the bottom. `pathLength="100"` normalises them,
       so `stroke-dasharray: 100` is the whole arm and `stroke-dashoffset: 100`
       is an arm not yet drawn — the hero's `lead-draw`, which is also how the
       menu mark's own filaments arrive.

       The front runs `travel + 2 * cascade`, i.e. the whole formation, so it
       reaches each mark's angle about when the mark does and the moon reads as
       being drawn around the button rather than as a circle fading up under
       it. Very faint: it is the thing the marks are arranged on, not a dial.

       Closing, the arms retract the way they came — the delay is what holds
       the line under the marks until they are back inside the button. */
    .j-rail:not(.j-rail-column) .j-rail-arc {
      display: block;
      position: absolute;
      right: calc(-1 * (var(--cl-rad) + 1px));
      top: calc(-1 * (var(--cl-rad) + 1px));
      width: calc(2 * (var(--cl-rad) + 1px));
      height: calc(2 * (var(--cl-rad) + 1px));
      z-index: -1;
      overflow: visible;
      pointer-events: none;
    }
    .j-rail:not(.j-rail-column) .j-rail-arc path {
      fill: none;
      stroke: rgba(217, 164, 65, 0.22);
      stroke-width: 1;
      stroke-linecap: round;
      stroke-dasharray: 100;
      stroke-dashoffset: 100;
      transition: stroke-dashoffset var(--cl-travel) cubic-bezier(0.22, 1, 0.36, 1)
                    calc(2 * var(--cl-cascade));
    }
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-arc path {
      stroke-dashoffset: 0;
      transition-duration: calc(var(--cl-travel) + 2 * var(--cl-cascade));
      /* THE FRONT TRAVELS AT A NEARLY CONSTANT ANGULAR SPEED, which is the one
         thing a drawn line has to do to read as being drawn. Every curve in
         this file's usual vocabulary ends `…, 1`, which pins the tail flat and
         front-loads everything; solved numerically against a straight line
         over seven samples, `0.45, 0.45, 0.72, 1` is the closest of the family
         (max deviation 0.12 against the house arrival curve's 0.25) while
         still landing softly. Shot as candidate F4 with a fast front: the arc
         completes as an EMPTY semicircle a beat before its own tip marks
         arrive, and a line that gets somewhere before the things it carries
         reads as two separate events. */
      transition-timing-function: cubic-bezier(0.45, 0.45, 0.72, 1);
      transition-delay: 0s;
    }

    /* ---- the pointer floor ----
       Points on an arc have air between them, and an open control must not
       fold because the pointer crossed it. A pad the size of the whole circle
       plus a tile, on the LIST, live only while the control is open — the
       6903c4a hit model exactly: nothing full-frame, nothing hit-testable
       while closed. Sized off the full circle rather than off the occupied
       points, so it stays right as the manifest changes. (Its right-hand half
       lies over the frame's own edge and is clipped by it; the moon has
       nothing there, and a pad cannot be smaller than the circle it guards
       without opening a seam at the tips.) */
    .j-rail:not(.j-rail-column) .j-rail-list::before {
      content: "";
      position: absolute;
      right: calc(-1 * (var(--cl-rad) + var(--cl-tile) / 2));
      top: calc(-1 * (var(--cl-rad) + var(--cl-tile) / 2));
      width: calc(2 * var(--cl-rad) + var(--cl-tile));
      height: calc(2 * var(--cl-rad) + var(--cl-tile));
      pointer-events: none;
    }
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-list::before { pointer-events: auto; }

    /* ---- THE CLOSED CONTROL HAS NO SECTION TARGETS ----
       The five marks are stacked at zero size INSIDE the button while closed;
       leaving them hit-testable would put five invisible tiles on top of the
       one control that is meant to be there, and a press on the button would
       navigate to the section the visitor is already in. So the closed
       instrument is exactly one 56px button — which is the whole of the
       resting picture as well — and the five links become live (at 48px each)
       the moment it opens, which on a pointer is before a click can be aimed
       at any of them. Focusability is untouched: `pointer-events` is not the
       tab order, and a Tab into the rail opens it. */
    .j-rail:not(.j-rail-column):not(.j-rail-hot):not(.j-rail-open):not(:has(:focus-visible))
      .j-rail-slot { pointer-events: none; }

    /* ---- EVERY NAME SITS ON ITS OWN MARK — 2026-08-14 ----
       "See the buttons that are at the top and bottom — they show to the left.
        They should just show above or below, because there's space right there
        for them. And all the buttons should in general probably hug their
        current setting a little bit more." — Hannah

       What stood here held every pill off to the moon's leftmost point via
       `--pillx`, so the five landed on one vertical line. That is a tidy
       picture bought by moving four of the five labels away from what they
       name — measured on `b079f84`, 23.5px for the apex, 51.8 for the +-45deg
       pair and 120.2 for the two tips. The rule and the reasoning are now in
       rail.js (WHICH SIDE OF ITS OWN MARK EACH NAME PILL SITS ON); this is
       what it writes.

       `--cl-hug` IS THE ONLY DISTANCE LEFT, and every pill on the moon is
       exactly it from its own tile, on whichever side it sits — so the five
       names are the same distance from their marks and differ only in
       direction — 23.5px on `b079f84` for the one pill that was already
       hugging, 21.0px for all five now. Its value and the four settings it
       was shot against are at `--cl-hug` above.

       No state gate. The old rule only applied `--pillx` once the moon was
       open, because a holdoff of up to a full radius is absurd on marks still
       stacked inside the button; a hug is the same number in both states, and
       one fewer thing changing at the 120ms dwell is one fewer thing that can
       be caught jumping (which is exactly how the hub's own pill went wrong,
       three placements ago). */
    .j-rail:not(.j-rail-column) .j-rail-name { margin-right: var(--cl-hug); }

    /* ---- …and the two at the ends of the arc go above and below ----
       "There's space right there for them." The tips are the only marks
       sitting at the HUB's own x, so a label hanging left of one has to clear
       a full radius to get past its +-45deg neighbour — and they are also the
       only two marks with nothing beyond them. rail.js derives which positions
       these are (it is a row-occupancy test, not a hardcoded +-90deg) and puts
       `j-pill-up` / `j-pill-dn` on the slot.

       RIGHT-ALIGNED TO THE BUTTON'S OWN EDGE, not centred on the mark. A pill
       is 94-121px wide against a 48px tile, so centring it would hang ~35px
       past the frame; `(tile - hub) / 2` = -4px carries its right edge out to
       the same 2px of frame the closed button holds, which is the alignment
       the whole flank is built on. It reaches LEFT from there, into the frame,
       under nothing.
       `top`/`bottom` and the base rule's `translateY(-50%)` are all restated
       rather than left to cascade: the base places a pill against the vertical
       MIDDLE of its tile, and these two are placed against its edges. */
    .j-rail:not(.j-rail-column) .j-rail-slot.j-pill-up .j-rail-name,
    .j-rail:not(.j-rail-column) .j-rail-slot.j-pill-dn .j-rail-name {
      right: calc((var(--cl-tile) - var(--cl-hub)) / 2);
      transform: none;
      margin-right: 0;
    }
    .j-rail:not(.j-rail-column) .j-rail-slot.j-pill-up .j-rail-name {
      top: auto;
      bottom: 100%;
      margin-bottom: var(--cl-hug);
    }
    .j-rail:not(.j-rail-column) .j-rail-slot.j-pill-dn .j-rail-name {
      top: 100%;
      bottom: auto;
      margin-top: var(--cl-hug);
    }
    /* THE HUB HAS NO PILL — 2026-08-14.
       "Let's remove the MENU text that shows when you hover over the actual
        menu button, because it just doesn't really fit in." — Hannah

       What stood here was the whole apparatus for placing it: held off to the
       moon's leftmost point it landed two radii from its own button with a
       section's mark standing between the two, so every reading of that
       picture had the pill labelling the MARK; hung off the button it drew
       across the inside of the moon; dropped below the arc it cleared both,
       and had to be below in the closed state too or it jumped at the 120ms
       dwell. Three placements, each fixing the last, and her verdict on the
       result is that it does not fit — which is the honest reading of a label
       that has to be exiled below the entire instrument to be legible at all.
       The five section names sit ON their marks; this one could not, because
       the thing it names is the thing the moon is drawn around.

       So it goes, and the affordance it was carrying is restated as motion
       instead — see the dwell gesture on `.j-rail-menu .j-sym path` above.
       `display: none` (not opacity) so it costs no box on a flank where the
       marks now place themselves against real neighbours, and so no rule
       further down can reveal an element that has no position of its own left.
       THE NAME DOES NOT GO WITH IT: `display: none` takes an element out of
       the accessibility tree, so rail.js now puts `aria-label="Menu"` on the
       button itself, where a control's name belongs.
       THE COLUMN KEEPS ITS LABEL. This rule is inside `(hover: hover)` and
       reads `:not(.j-rail-column)`, so it cannot reach the touch tier — where
       there is no hover to have complained about, where the arming tap is the
       only thing that ever reveals a name, and where six names in a single
       file is the list it has always been. */
    .j-rail:not(.j-rail-column) .j-rail-menu .j-rail-name { display: none; }
    /* A SECTION'S NAME FOLLOWS `.at`, NOT `:hover`.
       Every mark on this arc is placed by `transform`, and a transform-only
       move does not make Chrome re-run its hit test — so after a rotation
       under a stationary pointer, `:hover` is still on the mark that TRAVELLED
       AWAY. RE-MEASURED on the half moon (2026-08-13 later still): pointer
       parked on the apex, the moon turned one step by a pure transform write,
       `:hover` stayed on the departing mark while `elementFromPoint` returned
       the arriving one. `.at` is rail.js resolving `elementFromPoint` from the
       real pointer position (see THE POINTER'S OWN TRUTH); it agrees with
       `:hover` on every pointer move and is right in the one case `:hover` is
       not. So the moon reveals on `.at` and a stale `:hover` on any other slot
       is put back down. `:focus-visible` is untouched by all of this — focus
       follows the element, not the point, so a rotation cannot strand it. */
    .j-rail:not(.j-rail-column) .j-rail-slot:not(.at) .j-rail-item:hover .j-rail-name {
      opacity: 0;
      letter-spacing: 0.3em;
    }
    .j-rail:not(.j-rail-column):not(.j-rail-turn)
      .j-rail-slot.at .j-rail-item .j-rail-name {
      opacity: 1;
      letter-spacing: 0.24em;
    }
    /* …AND SO DOES THE COLOUR LIFT (Hannah, 2026-08-17: "it doesn't go into
       proper hovered mode until I move the mouse"). The lift lived on
       `:hover` alone, and `:hover` has the exact fault the name rules above
       are built around: it is only recomputed from a pointer EVENT, so a
       mark arriving under a stationary cursor — the dwell opening the fan,
       a turn settling — stayed unlit until the mouse moved. `.at` is the
       resolved truth of the same question, so the lift follows it with the
       same values as the `:hover` rules at PL-2.2 above. Echo included: it
       answers `.at` exactly as it answers a hover, because it takes a click. */
    .j-rail:not(.j-rail-column) .j-rail-slot.at .j-rail-item { color: var(--parchment); }
    .j-rail:not(.j-rail-column) .j-rail-slot.at:is(.active, .now) .j-rail-item {
      color: var(--gold-bright);
    }
    .j-rail:not(.j-rail-column) .j-rail-echo.at:not(.now) .j-rail-item {
      color: var(--parchment);
    }
    /* …and the names go out entirely while the moon turns (rail.js, THE NAMES
       SIT OUT THE TURN). Not a fade: it is a label that has stopped being
       true — and, for the length of the turn, one that cannot be recomputed
       either, since nothing is anywhere in particular. */
    .j-rail.j-rail-turn:not(.j-rail-column) .j-rail-name {
      opacity: 0;
      transition: none;
    }
    /* ONE PILL AT A TIME, AND WHY THE COLUMN'S RULE CANNOT COME ALONG.
       `.j-rail-open .j-rail-name` (the touch arming) reveals EVERY name at
       once, which a single file can do because its tiles are a file: the pills
       stack down the flank and never meet. An arc stacks five points into
       fewer effective rows, and measured on a hover-capable touch device — a
       2-in-1, the one class of machine that matches `(hover: hover)` and can
       still set `.j-rail-open` — the co-planar pills drew in one heap. So here
       the blanket reveal is withdrawn and the ACTIVE section's name stands in
       for it. Pointing and keyboard focus are untouched and still name
       whatever they are on. */
    .j-rail:not(.j-rail-column).j-rail-open .j-rail-name { opacity: 0; }
    .j-rail:not(.j-rail-column).j-rail-open .j-rail-slot.active .j-rail-name {
      opacity: 1;
      letter-spacing: 0.24em;
    }
    /* (the hub's half of this pair went with the hub's pill — an `opacity`
       for an element this geometry no longer gives a box to) */
    .j-rail:not(.j-rail-column).j-rail-open .j-rail-item:is(:hover, :focus-visible) .j-rail-name {
      opacity: 1;
    }

    /* ---- THE ONE SURFACE ----
       "There's like a black background that appears on hover, but it feels
        really janky and weird … multiple layers of black boxes, like a bigger
        one and a smaller one … This should be really subtle and should fade in
        and out nicely."

       She was reading two radials summing (0.80 core over 0.62 core = 0.92)
       across two different falloffs, which is exactly what "a bigger one and a
       smaller one" describes. There is now ONE, and it is the SAME one in both
       states — it grows from the button's footprint to the moon's on the
       formation's clock, so the darkening follows the instrument instead of a
       second surface arriving under the first.

       `::before` is switched off rather than re-tuned: its whole job in the
       ring block was to be the second surface.

       THE STOPS ARE A FALLOFF, NOT A CORE PLUS TWO SEGMENTS. The old recipe
       held a FLAT alpha to 34% and then ran two straight ramps, and a straight
       ramp between two stops has a discontinuous derivative at each end —
       which is the "hard line" that reads as an edge even when no edge is
       drawn. Five stops on a curve reach zero smoothly and there is no radius
       at which the eye finds a boundary. */
    .j-rail:not(.j-rail-column) .j-rail-inner::before { display: none; }
    .j-rail:not(.j-rail-column) .j-rail-inner::after {
      --s: calc(2 * (var(--cl-rad) + var(--cl-tile)));
      -webkit-mask-image: none;
      mask-image: none;
      right: calc(-0.5 * var(--s));
      top: calc(-0.5 * var(--s));
      width: var(--s);
      height: var(--s);
      /* The stops are authored at Final strength; ordinary chapters paint
         this one surface at 58% opacity below, reproducing the old effective
         values exactly. Raising that opacity in Final deepens the SAME wash,
         so there is no second layer or edge to appear. */
      background: radial-gradient(closest-side circle at 50% 50%,
        rgba(9, 6, 2, 0.90) 0%,
        rgba(9, 6, 2, 0.76) 22%,
        rgba(9, 6, 2, 0.48) 44%,
        rgba(9, 6, 2, 0.21) 66%,
        rgba(9, 6, 2, 0.05) 84%,
        rgba(9, 6, 2, 0) 100%);
      /* CLOSED it is sized to TWICE THE BUTTON — the one glyph on the flank
         plus a glyph's worth of falloff around it. The transform is what grows
         it, so the surface is one paint that scales rather than two that
         cross-fade: there is never a frame with two grounds in it, which is
         the whole of what she was looking at. */
      transform: scale(calc(2 * var(--cl-hub) / var(--s)));
      opacity: 0;
      transition: opacity 0.65s ease,
                  transform calc(var(--cl-travel) + 2 * var(--cl-cascade))
                    cubic-bezier(0.22, 1, 0.36, 1);
    }
    .j-rail:not(.j-rail-column).on .j-rail-inner::after { opacity: 0.58; }
    .j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
      .j-rail-inner::after { opacity: 0.58; transform: scale(1); }
    /* Final's lit field needs a little more quiet behind the line work. The
       current-slot class changes at the chapter boundary; opacity interpolation
       makes entering, reversing and leaving one continuous grade. */
    .j-rail:not(.j-rail-column):has(.j-rail-slot[data-chapter="final"].now)
      .j-rail-inner::after { opacity: 1; }

    /* ---- and the pills lose their rectangle ----
       `rgba(9,6,2,0.85)` with a 2px radius is a hard-edged black box, and on
       the flank it is the hardest edge in the whole composition — the one
       thing in her report that is a LINE rather than a wash. A label still
       needs a ground over a lit chapter, so it keeps one and stops having a
       box: an ellipse that reaches zero before the edge, plus the room to fade
       in. The type itself carries the same dark rim the glyphs do, which is
       what holds the letterforms where the wash is already thin. */
    .j-rail:not(.j-rail-column) .j-rail-name {
      /* THE ELLIPSE IS INSCRIBED IN THE BOX — `50% 50%` are radii measured
         against the box's own half-width and half-height, so the wash reaches
         zero EXACTLY at all four edges and the corners are already clear.
         (The first cut of this used `62% 108%`, which puts the vertical radius
         outside the box: the gradient was then clipped by the padding box at
         alpha 0.65 and drew the very hard top and bottom edges it was meant to
         remove. Caught by shooting it, not by reading it.)
         The stops are flat across the middle and spend their whole falloff in
         the outer third, so the letters sit on a real ground and the edge is
         nowhere. Padding is what buys that falloff its room. */
      background: radial-gradient(ellipse 50% 50% at 50% 50%,
        rgba(9, 6, 2, 0.8) 0%,
        rgba(9, 6, 2, 0.78) 46%,
        rgba(9, 6, 2, 0.58) 68%,
        rgba(9, 6, 2, 0.24) 86%,
        rgba(9, 6, 2, 0) 100%);
      border-radius: 0;
      padding: 0.52rem 1.5rem;
      /* and the type carries its own rim, exactly as the glyphs do — which is
         what holds the first and last letters where the wash is thinnest */
      text-shadow: 0 0 2px rgba(9, 6, 2, 0.95), 0 0 5px rgba(9, 6, 2, 0.7);
    }
  }

  /* A fine-pointer column fallback still follows continuous `--cur`; remove
     its transform lag once formation lands. Mobile no longer consumes --cur
     for geometry, so it keeps the tap-open transition authored above. */
  @media (pointer: fine) {
    .j-rail.j-rail-following .j-rail-slot,
    .j-rail.j-rail-following .j-rail-menu {
      transition-duration: 0s;
      transition-delay: 0s;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .j-hot, .j-hot-dot, .j-card { transition: none; }
    .j-pop, .j-pop-link { transition: none; }
    /* the rail's step-back is a state change, not a dissolve — the end state
       (copy at 0.14 while held open, full when not) is intact and instant */
    .j-copy { transition: none; }
    /* The side navigator, 2026-08-07. Every rule in it is a transition that
       ENDS at the element's expanded resting style, so switching them all off
       leaves the three states intact and instant: the rail still shows one
       symbol at rest, still shows all of them under hover / focus / tap, the
       reticle is simply already locked in, the menu's filaments already drawn,
       and the panel already arrived. Nothing here is load-bearing motion. */
    .j-rail, .j-rail-slot, .j-rail-item, .j-rail-name, .j-rail-menu,
    .j-rail-inner::before, .j-rail-inner::after, .j-rail-menu .j-sym path,
    .j-menu, .j-menu-scrim, .j-menu-x, .j-menu-item, .j-menu-mark,
    .j-menu-links a, .j-menu-ia { transition: none; }
    /* the reticle's clockwise cascade is delay, not duration — kill both */
    .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot.active .j-rck,
    .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot.active .j-rck.tr,
    .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot.active .j-rck.br,
    .j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot.active .j-rck.bl {
      transition: none;
      transition-delay: 0s;
    }
    .j-rck { transition: none; }
    /* THE MOON'S OWN RESET, and why the blanket one above cannot reach it.
       That rule is `.j-rail-slot` — one class. The moon's geometry has to
       out-rank the column's, so it is written `.j-rail:not(.j-rail-column)
       .j-rail-slot`, three; a `transition: none` at one class loses to a
       `transition-duration` at three, and MEASURED (2026-08-12) it did: two
       frames after a chapter change the marks were still a third of the way
       round. Restated at matching weight, they land on the frame. Everything
       the moon animates ends at its own resting style — `--ang` at the slot's
       point on the arc, `--t` and `--u` at 1 while open and 0 while shut, the
       mark at its own scale, the arc's arms drawn while open and undrawn while
       shut, the one surface at its open size — so switching all of it off
       leaves both states intact and instant, THE FORMATION'S CASCADE included:
       with no durations there is nothing left for the per-step delays to
       stagger, and the moon simply is where it lands.
       `.j-rail-arc path` and `.j-rail-inner::after` are named here for the
       same reason the slots are: they out-rank the blanket
       `.j-rail-inner::before/after` reset above, which is written at one
       class and never covered the arc at all. */
    .j-rail:not(.j-rail-column) .j-rail-slot,
    .j-rail:not(.j-rail-column) .j-rail-slot .j-rail-mark,
    .j-rail:not(.j-rail-column) .j-rail-arc path,
    .j-rail:not(.j-rail-column) .j-rail-inner::after {
      transition: none;
      transition-delay: 0s;
    }
    .j-rail-slot, .j-rail-menu { transition-delay: 0s; }
    .j-rail-slot.j-rail-recycle { --rail-recycle-y: 0px !important; }
    /* The dwell gesture is switched off rather than shortened, on the same
       contract as everything above it: `j-menu-rewrite` ends at the drawn
       glyph, which is the button's resting state, so with the animation gone
       the mark is simply already written. rail.js does not arm the timer under
       reduced motion either — this is the stylesheet holding its own end up,
       not a duplicate of that guard. */
    .j-rail-menu.j-rail-nudge .j-sym path { animation: none; }
    .j-rail.on .j-rail-menu .j-sym path,
    .j-rail:focus-within .j-rail-menu .j-sym path { transition-delay: 0s; }
    .j-act { transition: none; }
    /* The two 2026-08-07 entries. Both are authored so that every keyframe
       ENDS at the element's resting style (hero.css's `.co` boot keeps the
       same contract), so switching the animations off is the whole of the
       reduced-motion path — the arriving copy and the opened popover are
       simply already finished. The copy block's OPACITY envelope is not an
       animation and is deliberately left running: it is a cross-fade, not
       motion, it is the same fade the scroll path has always given copy, and
       it is the entire reason a jump no longer pops. */
    .j-block.j-arrive::before,
    .j-block.j-arrive .j-h,
    .j-block.j-arrive .j-sub,
    /* The action row, 2026-08-07. Its entry keeps the same contract (every
       keyframe ends at the resting style), so switching it off leaves the
       pill already arrived. (The glyph sequence and the `.busy` spark that
       used to be named here left with the Remix pill on 2026-08-13 — the
       crown that took its job paints nothing, so it animates nothing, and
       the re-deal's OWN reduced-motion path is in the scene: span 1, flare
       0, 320 ms, same start and end state. See chapters/owned/portraits.js.) */
    .j-block.j-arrive .j-act,
    /* The anchored card's entry, 2026-08-13 — same contract again (every
       keyframe ends at the resting style), so switching it off leaves the
       card simply arrived, at the anchored position placeCard() gave it.
       The placement itself is not motion and is deliberately left alone:
       it is where the box IS, not how it got there. */
    .j-card.j-card-enter,
    .j-card.j-detail-enter,
    .j-card.j-card-exit,
    .j-card.j-card-enter::before,
    .j-card.j-card-enter .j-card-h,
    .j-card.j-card-enter .j-card-body > :not(.j-card-h),
    .j-pop.j-pop-enter,
    .j-pop.j-detail-enter,
    .j-pop.j-pop-exit,
    .j-pop.j-pop-enter::before,
    .j-pop.j-pop-enter .j-pop-t,
    .j-pop.j-pop-enter .j-pop-s,
    .j-pop.j-pop-enter .j-pop-link { animation: none; }
    /* the sheet's own transition is more specific than the reset above, so it
       is named here: reduced motion SNAPS — the drag still follows the finger
       (direct manipulation), the release does not animate. */
    .j-card.sheet { transition: none; }
  }
