/* 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)):not(.j-rail-preboot)
    .j-rail-slot:not(.now) .j-rail-item { pointer-events: none; }
  /* THE PREBOOT SHELL IS FLAT, SO THE FOLD DOES NOT APPLY TO IT. Nothing is
     stacked behind anything here — index.html authors all five tiles in their
     final places and no JS ever adds `j-rail-hot` to this shell, so the rule
     above would hold four of the five tiles unpressable for the whole window
     the shell is the page's only navigation. The tiles are spans rather than
     links (the shell is aria-hidden and out of the tab order — see the note
     in index.html), so their hit areas have to be turned on here rather than
     falling out of the `a.j-rail-item` pair above. Purely a HIT-MODEL change:
     no paint rule reads `.j-rail-preboot`, so first paint is byte-identical.  */
  .j-rail-preboot .j-rail-item { pointer-events: auto; }

  .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;
    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; }

  .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); }
  }
  /* NO ENTRY DRAW (2026-10-04, Hannah: "remove the entry animation on the
     menu button ... should only play upon hover"). The filaments used to
     draw themselves in as the rail first revealed; the mark is now simply
     there, drawn, from the first frame, and the hover/focus rewrite below is
     its only gesture. The dash pattern stays declared because that gesture
     animates it. */
  .j-rail-menu .j-sym path {
    stroke-dasharray: 12;
    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 the mark is declared with directly above.

     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 on the first hover/focus frame and drops the class on
     animationend, so a fresh arrival 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 1.8rem;
    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 0.9rem;
  }
  .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: center;
    gap: 1rem;
    padding: 0.62rem 0.6rem 0.55rem 0.5rem;
    color: var(--parchment);
    text-decoration: none;
    transition: color 0.3s, background 0.3s;
  }
  .j-menu-mark { flex: 0 0 auto; margin-top: 0; color: rgba(217, 164, 65, 0.66); transition: color 0.3s; }
  .j-menu-item[data-menu-section="equip"] .j-menu-mark { transform: translateY(5px); }
  .j-menu-item[data-menu-section="connect"] .j-menu-mark { transform: translateY(-1px); }
  .j-menu-item[data-menu-section="final"] .j-menu-mark { transform: translateY(-2px); }
  .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.22rem;
    font-family: "Cormorant Garamond", Georgia, "Times New Roman", serif;
    font-size: 1rem;
    line-height: 1.32;
    color: var(--muted);
  }
  .j-menu-thesis { margin-top: 0; font-size: 1.08rem; color: var(--parchment); }
  .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 (0.5rem + 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.6rem 2.9rem;
    list-style: none;
  }
  .j-menu-row {
    margin: 0 0 0.32rem;
    font-size: 0.8rem;
    line-height: 1.55;
    color: var(--muted);
  }
  .j-menu-row:last-child { margin-bottom: 0; }
  /* The whole row is the link (2026-08-20). The tint runs edge-to-edge
     across the sub's column: the negative margins reach into the list
     padding and the padding cancels them, so the text never moves. The
     hover treatment mirrors the section headers' — gold text over the same
     warm tint. */
  .j-menu-row-link {
    display: flex;
    align-items: center;
    margin: 0 -0.35rem;
    padding: 0.1rem 0.35rem;
    color: inherit;
    text-decoration: none;
    border-radius: 2px;
    transition: color 0.3s, background 0.3s;
  }
  .j-menu-row-link:is(:hover, :focus-visible) {
    color: var(--gold-bright);
    background: rgba(217, 164, 65, 0.05);
  }
  .j-menu-row-link:is(:hover, :focus-visible) .j-menu-il { color: var(--gold-bright); }
  @keyframes j-arrow-nudge {
    from { transform: translateX(0); }
    to { transform: translateX(2px); }
  }
  @keyframes j-arrow-nudge-external {
    from { transform: translate(0, 0); }
    to { transform: translate(2px, -2px); }
  }
  .j-menu-row-link:is(:hover, :focus-visible) .j-menu-ia,
  .j-menu-owned:is(:hover, :focus-visible) .j-menu-ia {
    animation: j-arrow-nudge 800ms ease-in-out infinite alternate;
  }
  .j-menu-row-link[target="_blank"]:is(:hover, :focus-visible) .j-menu-ia {
    animation: j-arrow-nudge-external 800ms ease-in-out infinite alternate;
  }
  /* the SOON badge breathes very gently while its row is hovered */
  @keyframes j-badge-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.55; }
  }
  .j-menu-row-link:is(:hover, :focus-visible) .j-menu-badge { animation: j-badge-pulse 5s ease-in-out infinite; }
  .j-menu-il {
    display: inline-flex;
    align-items: center;
    gap: 0.42rem;
    color: var(--parchment);
    font-weight: 600;
  }
  .j-menu-is { min-width: 0; }
  .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-dots { width: 17px; height: 17px; flex-basis: 17px; }
  .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-pict .j-menu-dot-disc { fill: currentColor; stroke: none; }
  .j-menu-il::after { content: " \2014"; margin-right: 0.6em; color: rgba(201, 191, 168, 0.45); font-weight: 400; }
  .j-menu-ia {
    display: inline-block;   /* transform (the hover nudge) needs a box */
    flex: 0 0 auto;
    margin-left: 0.65em;
    color: var(--gold-bright);
    text-decoration: none;
    white-space: nowrap;
  }
  /* a minor status pill on a row (2RP's "Coming soon"): the same recipe as
     the cards' accent badges — border accent/20, background accent/5,
     colour accent/60 — in the site gold, sized for a 0.8rem row. */
  .j-menu-badge {
    display: inline-block;
    flex: 0 0 auto;
    margin-left: 0.65em;
    padding: 0.07em 0.46em;
    font-size: 0.54rem;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: rgba(240, 200, 119, 0.65);
    border: 1px solid rgba(217, 164, 65, 0.28);
    border-radius: 999px;
    background: rgba(217, 164, 65, 0.06);
  }
  .j-menu-is > .j-menu-badge {
    margin-left: 0.42em;
    vertical-align: middle;
  }
  .j-menu-teaser > :is(.j-menu-il, .j-menu-is) {
    opacity: 0.42;
    filter: blur(3.5px);
    user-select: none;
  }

  /* Desktop reads as a quiet hierarchy rather than a stack of cards: every
     initiative glyph occupies the same left column as its chapter glyph, so
     the smaller marks visibly descend from the larger one above. */
  @media (min-width: 621px) {
    .j-menu-list > li { padding-block: 0.18rem 0.48rem; }
    .j-menu-sub { padding: 0 0.6rem 0.42rem 0.5rem; }
    .j-menu-row-link { margin: 0; padding-inline: 0; }
    .j-menu-il { gap: 1rem; }
    .j-menu-pict {
      width: 22px;
      height: 13px;
      flex-basis: 22px;
      justify-content: center;
    }
    .j-menu-pict svg { width: 13px; }
    .j-menu-pict-dots { height: 17px; }
    .j-menu-pict-dots svg { width: 17px; }
    .j-menu-teaser {
      min-height: 28px;
      white-space: nowrap;
    }
    .j-menu-teaser > :is(.j-menu-il, .j-menu-is) {
      opacity: 0.36;
      filter: blur(2.25px);
    }
  }

  .j-menu-owned {
    display: block;
    margin: 0 0 0.6rem -0.35rem;
    padding: 0.45rem 0.6rem 0.6rem 3.25rem;
    font-size: 0.8rem;
    line-height: 1.55;
    color: var(--muted);
    text-decoration: none;
    border-radius: 2px;
    transition: color 0.3s, background 0.3s;
  }
  .j-menu-owned:is(:hover, :focus-visible) {
    color: var(--gold-bright);
    background: rgba(217, 164, 65, 0.05);
  }
  .j-menu-owned:is(:hover, :focus-visible) .j-menu-owned-copy { color: var(--gold-bright); }
  .j-menu-owned-copy {
    margin: 0;
    color: var(--parchment);
    font-weight: 600;
  }

  .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; }
  }

  /* The phone panel is a reading surface, not the desktop outline squeezed
     into a narrow column. Each initiative gets one full-width card and its
     supporting sentence stacks beneath the title. */
  @media (max-width: 620px) and (orientation: portrait) {
    .j-menu {
      width: 100vw;
      padding: 1rem 1.5rem 2rem;
      border-left: 0;
      box-shadow: none;
    }
    .j-menu-head {
      position: sticky;
      top: -1rem;
      z-index: 3;
      gap: 0.45rem;
      margin: -1rem -1.5rem 1.15rem;
      padding: 1rem 1.5rem 0.75rem;
      background: #0b0702;
      border-bottom: 1px solid rgba(217, 164, 65, 0.16);
    }
    .j-menu-h { font-size: 0.72rem; letter-spacing: 0.24em; }
    .j-menu-list > li { padding: 0.35rem 0 0.9rem; }
    .j-menu-list > li:first-child { padding-top: 0; border-top: 0; }
    .j-menu-item { gap: 0.85rem; padding: 0.72rem 0; }
    .j-menu-list > li:first-child .j-menu-item { padding: 0.65rem 0 1rem; }
    .j-menu-list > li:first-child .j-menu-mark { width: 28px; height: 28px; }
    .j-menu-thesis {
      font-size: clamp(1.35rem, 6vw, 1.58rem);
      line-height: 1.12;
    }
    .j-menu-no { font-size: 0.64rem; color: rgba(240, 200, 119, 0.76); }
    .j-menu-name {
      font-size: 0.72rem;
      line-height: 1.25;
      color: rgba(240, 200, 119, 0.9);
    }
    .j-menu-sub { padding: 0 0 0.15rem; }
    .j-menu-row { margin: 0 0 0.3rem; font-size: 0.82rem; line-height: 1.32; }
    .j-menu-row-link {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      grid-template-rows: auto auto;
      align-items: center;
      column-gap: 0.75rem;
      row-gap: 0.14rem;
      min-height: 64px;
      margin: 0;
      padding: 0.72rem 0.85rem;
      border: 1px solid rgba(217, 164, 65, 0.16);
      border-radius: 6px;
      background: rgba(217, 164, 65, 0.025);
    }
    .j-menu-il {
      grid-column: 1;
      grid-row: 1;
      gap: 0.58rem;
      font-size: 0.88rem;
      line-height: 1.15;
    }
    .j-menu-il::after { content: none; }
    .j-menu-pict { width: 18px; height: 18px; flex-basis: 18px; }
    .j-menu-pict-dots { width: 20px; height: 20px; flex-basis: 20px; }
    .j-menu-is {
      grid-column: 1;
      grid-row: 2;
      padding-left: 1.6rem;
      font-size: 0.78rem;
      line-height: 1.35;
    }
    .j-menu-ia,
    .j-menu-row-link > .j-menu-badge {
      grid-column: 2;
      grid-row: 1 / span 2;
      align-self: center;
      margin-left: 0;
    }
    .j-menu-ia { font-size: 1rem; }
    .j-menu-badge { padding: 0.16em 0.65em; font-size: 0.58rem; }
    .j-menu-legal { margin-top: 1.25rem; }
  }

  /* --- 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: "Cormorant Garamond", 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; }
  /* Purpose's door belongs to the paragraph's closing breath. The spacing is
     intentionally a little looser than the old CTA row, leaving room for a
     future editorial sibling without turning this into a content block. */
  .j-block[data-chapter="final"] .j-actions {
    margin-top: clamp(1.75rem, 3vw, 2.25rem);
    gap: clamp(1.75rem, 3.2vw, 2.5rem);
    align-items: flex-start;
  }

  .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);
  }
  /* --- PURPOSE'S EDITORIAL DOOR (owner, 2026-08-27) ---------------------
     A statement, its explanation, then one place to read deeper. This is
     deliberately a light editorial link, not a new content section: no box,
     arrow, pill, or permanent descriptor. The quiet sub-line is retained in
     the DOM for the accessible name and revealed on hover/focus only. */
  .j-act-purpose {
    position: relative;
    pointer-events: auto;
    display: inline-flex;
    align-items: center;
    justify-content: flex-start;
    gap: 0.46rem;
    min-height: 44px;
    padding: 0.2rem 0 0.45rem;
    border: 0;
    border-radius: 0;
    color: rgba(233, 216, 181, 0.82);
    font-size: 1rem;
    font-weight: 400;
    letter-spacing: 0.015em;
    line-height: 1.25;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    text-shadow: 0 1px 10px rgba(10, 6, 2, 0.9);
    transition: color 0.28s ease;
  }
  .j-act-purpose::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    width: 1rem;
    height: 1px;
    background: rgba(224, 177, 84, 0.36);
    transition: width 0.34s cubic-bezier(0.22, 1, 0.36, 1),
                background-color 0.28s ease;
  }
  .j-purpose-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 16px;
    width: 16px;
    height: 16px;
    color: rgba(230, 189, 110, 0.72);
    font-family: Georgia, "Times New Roman", serif;
    font-size: 1rem;
    font-weight: 400;
    line-height: 1;
    transition: color 0.28s ease, filter 0.28s ease, transform 0.34s ease;
  }
  .j-purpose-icon .j-sym {
    width: 16px;
    height: 16px;
  }
  .j-purpose-copy {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    min-width: 0;
  }
  .j-act-purpose .j-act-t {
    font-size: 1rem;
    font-weight: 400;
    line-height: 1.25;
  }
  .j-act-purpose .j-act-sub {
    display: block;
    max-height: 0;
    margin-top: 0;
    overflow: hidden;
    color: rgba(201, 191, 168, 0.68);
    font-size: 0.72rem;
    font-weight: 400;
    letter-spacing: 0.01em;
    line-height: 1.3;
    opacity: 0;
    transform: translateY(-0.2rem);
    transition: max-height 0.32s ease, margin-top 0.32s ease,
                opacity 0.28s ease, transform 0.32s ease, color 0.28s ease;
  }
  .j-act-purpose:is(:hover, :focus-visible) {
    color: var(--gold-bright);
  }
  .j-act-purpose:is(:hover, :focus-visible)::after {
    width: 100%;
    background: rgba(240, 200, 119, 0.74);
  }
  .j-act-purpose:is(:hover, :focus-visible) .j-purpose-icon {
    color: var(--gold-bright);
    filter: drop-shadow(0 0 6px rgba(240, 170, 60, 0.4));
    transform: translateY(-1px);
  }
  .j-act-purpose:is(:hover, :focus-visible) .j-act-sub {
    max-height: 1.2rem;
    margin-top: 0.3rem;
    color: rgba(233, 216, 181, 0.86);
    opacity: 1;
    transform: translateY(0);
  }

  /* --- 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%); }
  /* Bottom-anchored copy RESERVES THE NAVIGATOR'S BAND (2026-08-27; owner:
     the Inspire copy ran 21px into the icons at 430x932). The max() takes
     the block's own authored seat unless the navigator's band — read from
     the same :root variables the rail itself is laid out by — would reach
     it; then the block rises just clear. Composition, not coincidence:
     the two share one declaration, so retuning the nav's seat cannot
     silently re-open the collision.

     THE AIR IS 30px, AND IT IS NOT A TASTE NUMBER (2026-08-27, second
     pass). The first cut of this rule reserved 14px and cleared the
     collision arithmetically while leaving it visually intact — measured
     on Inspire at 430x932, copy bottom 828 against a mark top of 841:
     13px, which is LESS than the body copy's own 22px line gap. Anything
     under one line's leading is read as another line of the paragraph, so
     the navigator was still inside the copy even though nothing overlapped.
     30px is comfortably past that leading and is what makes the row read as
     its own object. Measured after: 30px at 430x932 and at 1440x900.

     The blocks can afford it because bottom-anchored copy has headroom
     ABOVE — the scene, not more type. And note what this does NOT trade
     against: the hero seat is a different seat. The row rides up by
     `--nav-hero-lift` at the hero and back down to `--nav-centre-bottom`
     once detached, so hero clearance (~103px) and this air (30px) are set
     independently and do not pull on each other. */
  .j-block.pos-bottom     { left: 50%;
    bottom: max(8vh, calc(var(--nav-centre-bottom) + var(--nav-major) / 2 + 30px));
    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: max(calc(8vh + 40px),
                  calc(var(--nav-centre-bottom) + var(--nav-major) / 2 + 30px));
    }
  }
  @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; }
    /* THE EPILOGUE COPY RIDES THE PURPOSE LIFT (2026-08-30, owner on iPad Mini
       portrait: the final section's text block "sits too low"). The cause is
       structural. Every bottom-anchored block reserves the navigator's band
       with `--nav-centre-bottom + --nav-major / 2 + 30px`, but at Purpose the
       row itself rises by `--purpose-rail-lift-max` and the base seat does not
       know that, so the reservation is short by exactly that lift. Measured at
       744x1133: the block's box bottom landed 22px BELOW the row's top — a
       52px deficit against a rule that promises 30px of air.
       THE INK DID NOT TOUCH, and that is why this reads as a composition note
       and not a hit-target fault: the last sub line ended ~26px clear of the
       row. (The phone band runs a negative BOX gap on purpose for the same
       reason — its max-width 480 rule below pays part of the lift in its own
       currency, --final-composition-lift + --final-copy-nav-air, and was
       signed off on measured INK.) What the tablet band had was no rule at
       all, so it was short by the whole lift rather than by a reviewed amount.
       Paying it with the lift variable rather than a number keeps the two ends
       answerable to each other: this is the CSS twin of rail-geometry.js's
       `purposeLift`, so the copy rises by precisely what the row rises by and
       the 30px contract holds by construction. Measured after: box gap +30.0
       at 744x1133 AND at 768x1024. Shot against 0 / 48 / 72 / 96 — 72 starts
       to float the block off its bottom anchor and 96 detaches it. */
    .j-block.pos-bottomleft[data-chapter="final"] {
      bottom: max(calc(9vh + var(--purpose-rail-lift-max)),
                  calc(var(--nav-centre-bottom) + var(--nav-major) / 2 + 30px
                       + var(--purpose-rail-lift-max) + var(--purpose-up-branch)));
    }
  }
  .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: max(9vh, calc(var(--nav-centre-bottom) + var(--nav-major) / 2 + 30px)); }
  /* Desktop Final/Outro balance (2026-08-27): lower the copy and its action
     together by 50px. Keep both branches of the existing nav-clearance max()
     in step so the shift is exact at the reviewed 1440x900 frame and remains
     well-defined at other desktop heights. Phone/tablet and short-landscape
     seats retain their dedicated rules below. */
  @media (min-width: 901px) {
    .j-block.pos-bottomleft {
      bottom: max(calc(9vh - 50px),
                  calc(var(--nav-centre-bottom) + var(--nav-major) / 2 - 20px));
    }
  }
  /* Desktop Purpose starts from this same low seat at every width.
     purpose-copy-fit.js reserves only the settled circles that actually
     intersect its ink; a second narrow-desktop reservation would count the
     fork twice and lift the text far above its lower reading area. */

  /* ============================================================
     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)

     Connect keeps its two-line editorial measure. Purpose's copy now uses a
     dedicated three-line desktop stanza below, so the raised navigator has
     clear air without moving the fixed bottom-left copy seat.

     TWO LEVERS, IN THAT ORDER.

     1. COPY, for Final only. The line was rewritten to Hannah's
        sense (see content/content.js). Its current 135-character
        Purpose explanation is measured separately below; Connect's sub
        is a locked string she did not ask to change, so it was not touched.

     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; }
  /* Purpose's copy stays on its existing bottom seat, but its explanation
     gets a narrower, balanced measure so the desktop stanza reads as three
     even lines. This is intentionally desktop-only: portrait widths retain
     their natural responsive wrapping rather than forcing an awkward count. */
  @media (min-width: 901px) {
    .j-block[data-chapter="final"] .j-sub {
      max-width: 28rem;
      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 CONDENSE'S NEW AXIS (ICON-ARRIVAL, 2026-08-23). The initiative
     marker's entry gesture — housing unfolding from the centre line, the
     pictograph's grow-and-settle breath — needs a horizontal scale on three
     elements whose `transform` templates are owned by cards.css. Rather than
     out-specifying those templates (and having to chase their hover variants),
     the gesture rides the INDIVIDUAL `scale` property, which the UA composes
     with `transform` and which no rule in cards.css touches. The variables are
     written once per frame by hotspot-frame.js's paintIconTab off `h.a` — the
     chip's one truthful arrival clock — so there is no CSS animation to drift,
     replay on hover, or disagree with a dt = 0 freeze; both default to 1, so a
     chip nobody paints (and every settled frame) is byte-identical to the
     pre-ICON-ARRIVAL rendering. transform-origins are inherited from the
     cards.css rules (::after 50% 30%, svg center), so the unfold is about the
     centre line by construction. WHAT WOULD BREAK IT: a cards.css rule that
     starts setting `scale` on these elements would silently override the
     gesture — the individual property was chosen precisely because that sheet
     does not use it. */
  .j-hot--icon-tab::after,
  .j-hot--icon-tab .j-hot-dot.j-hot-ico::before { scale: var(--j-hot-shell-sx, 1) 1; }
  .j-hot--icon-tab .j-hot-dot.j-hot-ico svg { scale: var(--j-hot-ico-s, 1); }
  /* 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);
    /* 0.35, NOT the 0.30 authored on 2026-08-05, and it is the shared
       keyframes' own settle point rather than a taste change. `j-detail-arrive`
       ends at 0.35 and `j-detail-depart` starts there, so a 0.30 rest made the
       arrival's last frame step DOWN 14% and the exit's first frame step UP
       17% — the second of those breaks the monotone gate's stated contract
       ("never brighter on the way out than it was at rest",
       tools/test-detail-shell-monotone.mjs), which cannot see it because it
       reads keyframe stops and not resting declarations. `.j-card` is the
       sibling on the identical keyframes and has always rested at 0.35
       (below); `.j-pop` was the outlier. Report #30: edges must not step. */
    border: 1px solid rgba(217, 164, 65, 0.35);
    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 gives popovers
     and profile cards the same arrival without moving their hit area or
     stacking a second slide on their own interior choreography.

     THE EDGE ONLY EVER RISES (report #30, 2026-08-26). Owner: "when I hover
     over any of the items, the sections or owners, there's a weird border
     flash that disappears when I first hover. It feels unintentional."

     BOTH surfaces they name are this one keyframe. Hovering an initiative
     chip inside a section opens `.j-pop`; hovering a contributor face in
     Owned opens `.j-card`, whose TRANSIENT tier is a hover tier
     (ui/card-tier.js). Both are armed with `.j-detail-enter`, so one cause
     served two complaints — and "on the first hover" is the entry itself:
     runPopEntry/runCardEntry fire only on a FRESH reveal, so a pointer that
     leaves and comes back inside POP_HIDE_MS (160 ms) sees no entry at all
     and therefore no flash. It was never first-hover-only; it was
     fresh-reveal-only, which from a chair looks the same.

     The `border-color` stop below used to be a deliberate "gold glint":
     rgba(255, 226, 160, 0.62) at 68%, brighter and whiter than both the
     keyframe's own 100% (0.35) and the element's resting edge (0.30).
     Measured per frame in Chrome, p95 frame gap 29.9 ms: 0.12 -> 0.62 by
     t+137 ms -> 0.35 -> 0.30. Folding hue, alpha and the simultaneous
     opacity ramp together, the edge peaked at 3.32x the brightness it
     settles to and then fell away — a rise-then-fall, which is what the eye
     reads as a flash. Exactly the shape of report #20's portrait flare, in
     CSS instead of a shader, and answered the same way: strictly monotone,
     ENDPOINTS UNMOVED.

     The stop no longer states `border-color` at all, so the UA runs it 0% ->
     100% in one monotone ramp. That is deliberate over writing an
     intermediate number: there is then no value for a later edit to nudge
     back above the settle point. The lamp-up (`opacity`) and the wipe
     (`clip-path`) are untouched — the arrival still reads as an arrival; it
     just no longer overshoots. Gated by tools/test-detail-shell-monotone.mjs,
     which measures relativeLuminance x alpha x opacity across the stops of
     every keyframe in the sheet that animates an edge. */
  @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); }
    100% { opacity: 1; clip-path: inset(-80px); border-color: rgba(217, 164, 65, 0.35); }
  }
  /* The exit carried the same overshoot in reverse — 0.35 -> 0.48 at 32%, a
     2.32x flare on the way out, i.e. the panel brightened its own edge while
     leaving. Same treatment, same endpoints. */
  @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); }
    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: "Cormorant Garamond", 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: "Cormorant Garamond", 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-purpose-cta: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; }
  /* And the stacked marker states 0, for the same reason from the other
     direction: its ring wraps a tab-over-capsule silhouette, where a 2px
     round reads as a nick taken out of each corner rather than as a radius.
     The INITIATIVE marker already lands on 0 — but by accident of the
     cascade, not by saying so: cards.css writes `.j-hot.j-hot--icon-tab
     { border-radius: 0 }` at two classes, which ties the shared ring above
     and wins on order. The soon chip's own rule is one class and loses it,
     so the pair drew two different rings (measured 2026-08-31: 2px on Quark,
     0px on Arca and Tworp) — the exact family split this order set out to
     close. Stated here rather than by thickening `.j-hot--soon` itself: that
     rule also carries `color`, and raising it over the generic hover would
     take the gold-brighten off the chip to win a corner. */
  .j-hot--soon:focus-visible { border-radius: 0; }
  .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; }
    /* 11vh is the authored seat; the max() is the navigator's band, the same
       one the base rule and the ≤480 block reserve. At 481-599 portrait
       11vh alone is ~99px against a 120px band — short, by the same
       arithmetic that put the Epilogue door on the Intro bookend at 430. */
    .j-block.pos-bottomleft { left: 6vw;
      bottom: max(11vh, calc(var(--nav-centre-bottom) + var(--nav-major) / 2 + 30px)); }
    .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 { position: relative; }
    .j-menu-links a::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), (max-width: 900px) {
    .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. */
    /* …and then the navigator came BACK into this lane, from the other
       direction (2026-08-27). The row is bottom-CENTRE now, so the block no
       longer has a side to be safe on: measured at 430x932 with a bare 6vh,
       the Epilogue box ended at y 876 and its Ownership door occupied
       x 24..143 / y 806..850 while the Intro bookend's mark sat at
       x 81..115 / y 845..879 — a real 34x5.5px overlap of two things that
       both take a tap, with the rail on top. That is a hit-target fault, not
       only a picture.

       So this override joins the same reservation the base rule uses rather
       than restating a bare seat. Nothing else here changes: the full-bleed
       width and the reclaimed left gutter were the point of this block and
       they stand. Measured after: door bottom 786, mark top 842. */
    .j-block.pos-bottomleft {
      left: 0;
      bottom: max(6vh, calc(var(--nav-centre-bottom) + var(--nav-major) / 2 + 30px));
      width: 100vw;
      padding-left: 1.5rem;
      padding-right: 1.5rem;
    }
    .j-block.pos-bottomleft[data-chapter="final"] {
      bottom: max(6vh, calc(var(--nav-centre-bottom) + var(--nav-major) / 2 + 30px
                            + var(--final-composition-lift) + var(--final-copy-nav-air)
                            + var(--purpose-up-branch)));
    }
    .j-block.pos-bottomleft .j-h { text-wrap: balance; }
    /* Purpose's pair keeps its editorial scale while retaining a 44px touch
       target. Its gap stays generous without forcing a second row at 320px. */
    .j-act { padding: 0.42rem 1.1rem; font-size: 0.72rem; }
    .j-actions { margin-top: 1.25rem; gap: 0.6rem; }
    .j-block[data-chapter="final"] .j-actions {
      margin-top: 1.75rem;
      gap: 1.5rem;
    }
    .j-act-purpose {
      min-height: 44px;
      padding: 0.2rem 0 0.45rem;
      font-size: 1rem;
    }
    .j-act-purpose .j-act-t { font-size: 1rem; }
  .j-act-purpose .j-purpose-icon { font-size: 1rem; }
  }

  /* ============================================================
     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));
    }
    /* RETIRED (2026-08-26, owner: "when I scroll between sections the
       labels disappear"). This half-moon-era rule blanked every name with
       an !important whenever the rail sat between two rests — correct for
       the fan (whose pills hang mid-air during a turn), wrong for the
       centred row, whose core labels are part of the content and must hold
       through a scroll. Same FAMILY as the pill-side fault fixed earlier
       today (a retired geometry's state rule reaching the new DOM), but a
       different rule — the two are separate faults, not one. */
    /* ---- 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 hover/focus 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--soon .j-hot-dot::after` is the ember of a stacked marker, a
       pseudo-element rather than the `<i>` itself, so it needs naming here to
       get the same flattening the plain dot has always had. */
    .j-hot, .j-hot-dot, .j-hot--soon .j-hot-dot::after, .j-card { transition: none; }
    /* The initiative marker's condense (ICON-ARRIVAL): cards.css already
       flattens the entry's opacity/transform variables under reduced motion;
       this flattens the `scale` axis site.css added beside them. What
       remains is DELIBERATE, not missing: the performed beat still times
       each marker's arrival (pacing is not motion), so the six chips fade up
       one at a time, fully formed — a cadence of crossfades instead of three
       simultaneous pops. */
    .j-hot--icon-tab::after,
    .j-hot--icon-tab .j-hot-dot.j-hot-ico::before,
    .j-hot--icon-tab .j-hot-dot.j-hot-ico svg { scale: 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, .j-menu-row-link, .j-menu-owned { transition: none; }
    /* the row arrow's hover nudge is load-bearing motion — switch it off
       whole under reduced motion (the panel already arrived instantly) */
    .j-menu-row-link .j-menu-ia, .j-menu-owned .j-menu-ia { animation: none !important; }
    .j-menu-row-link .j-menu-badge { animation: none !important; }
    /* 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 hover/focus 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 replay it 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; }
  }

/* ========================================================================
   THE CENTRED JOURNEY NAVIGATOR — 2026-08-26 (owner's navigation restage)

   "I want to switch the navigation to be in the middle of the screen ...
    Inspire, Connect, and Equip [in the middle] with Equip showing Coming
    Soon when you hover over it. Intro and Epilogue should be smaller. All
    the icons should show the text below them in the intro section and then
    it should only appear upon hover."

   One five-item row, fixed at the bottom centre AT EVERY TIER: a minor
   circle (Intro), three major circles (Inspire / Equip / Connect), a minor
   circle (Epilogue), all on ONE horizontal centre line, fine dotted rules
   between neighbours, and the travelling gold ring gliding along that line
   with journey progress. This SUPERSEDES both of the geometries behind it
   in this file — the 2026-08-20 horizontal strip that docked from under
   the hero copy to the lower-left, and the mobile edge file with its
   first-tap arming (a permanently visible row has nothing to unfold, so a
   phone tap navigates directly, exactly like a desktop click). The
   half-moon/column rules further up are therefore dead at EVERY viewport
   now; they are retired in place, exactly as the horizontal strip already
   retired them for desktop, and this block neutralises them at matching or
   greater specificity.

   GEOMETRY HAS ONE OWNER. The circle diameters, the gap, and the row's
   centre line are journey/layout/rail-geometry.js's rowMetrics(); the live
   rail republishes them as the --nav-* custom properties on its root every
   viewport change, and the paint (ring x, connector fills) is computed in
   the same pixel frame. The declarations just below are the PREBOOT TWIN
   of that table — they exist so the first-paint shell (index.html), which
   has no JS writing on it, lays out identically. Keep the two in step, the
   same way the shell itself is a kept-in-step twin of the live DOM.
   ======================================================================== */
/* Declared on :root, not on .j-rail, ON PURPOSE: the navigator's geometry
   is now something other compositions must be able to compose AGAINST —
   the bottom-anchored copy blocks below reserve its band with these same
   numbers, which is what ended the independent-layout collision the owner
   reported on Inspire at phone width (copy last line 21px into the
   icons). Custom properties inherit, so the rail and its preboot shell
   read them exactly as before; rail.js's per-frame inline writes on the
   rail root still override within the rail's own subtree. */
:root {
  --nav-major: 44px;   /* hit boxes — see rowMetrics(); the DRAWN circle is --nav-ring-d */
  --nav-minor: 40px;
  --nav-gap: 36px;
  --nav-centre-bottom: 92px;
  --purpose-rail-lift-max: 34px;
  --final-composition-lift: 0px;
  --final-copy-nav-air: 0px;
  /* Room for Purpose's up-branch (Manifesto, its word above it) over the
     row: where the copy spans the width (portrait), it rises by this much
     so the branch never sits on its last line (2026-10-04). */
  --purpose-up-branch: 0px;
  --nav-fit-major: 0.9;    /* ~21.5px of ink inside a 36px circle */
  --nav-fit-minor: 0.83;   /* ~20px bookend glyphs */
  --nav-ring-d: 36px;      /* the core three's fine circles ("34-38px") */
  --nav-minor-ring-d: 28px; /* quieter bookend circles */
  --nav-connector-air: 6px; /* shared clearance around rings, dots and splits */
  /* CONTENT POSE (owner, fourth pass): at the hero the whole instrument is
     this much larger — part of the content, "the three things we're
     doing" — and it eases back to 1 as the visitor scrolls out of the
     intro. --nav-content-u is painted by rail.js (1 at the hero rest, 0
     once the intro is left, continuous in journey progress between); the
     preboot default 1 means the shell boots in the content pose. */
  --nav-content-u: 1;
  --nav-content-max: 1.28;
  /* EMBEDDED, NOT STACKED (owner, fifth pass: "move it further away from
     the content... near the threshold of the hero, not directly underneath
     its copy"), WITH ITS OWN FLOOR (owner, sixth pass: "probably a little
     bit too close to the bottom edge now"). The hero seat is derived from
     the VIEWPORT, not a desktop pixel: the row's centre line sits at
     13.5% of viewport height in the hero pose (~121px at 900 tall, the
     region the owner asked for; the reference render's own row sits at
     ~13%), never below the scrolled seat. The instrument RIDES DOWN to
     the scrolled seat as it detaches — the hero pose keeps the bigger
     clearance, the navigation pose deliberately sits nearer the edge. The
     copy separation survives by construction: the hero seat is measured
     from the BOTTOM, so it can never climb back toward the paragraph. */
  --nav-hero-lift: max(0px, calc(13.5vh - var(--nav-centre-bottom)));
}
@media (orientation: portrait) and (max-width: 900px) {
  :root {
    --nav-major: 42px;
    --nav-minor: 36px;
    --nav-gap: 28px;
    --nav-centre-bottom: 84px;
    --purpose-rail-lift-max: 30px;
    --purpose-up-branch: 48px;
    --nav-ring-d: 34px;
    --nav-minor-ring-d: 26px;
    --nav-connector-air: 5px;
    --nav-content-max: 1.22;
  }
}
@media (orientation: portrait) and (max-width: 620px) {
  :root {
    --nav-major: 40px;
    --nav-minor: 34px;
    --nav-gap: 20px;
    --nav-centre-bottom: 70px;
    /* Keep the preboot twin aligned with rowMetrics(): Purpose's complete
       navigation composition is nudged five pixels lower on phones. */
    --purpose-rail-lift-max: 40px;
    --final-composition-lift: 30px;
    --final-copy-nav-air: 12px;
    --nav-fit-major: 0.85;
    --nav-fit-minor: 0.72;
    --nav-ring-d: 30px;
    --nav-minor-ring-d: 24px;
    --nav-connector-air: 5px;
    --nav-content-max: 1.15;
  }
  .j-rail .j-rail-name,
  .j-rail:not(.j-rail-column) .j-rail-name,
  .j-rail .j-rail-soon-note {
    font-size: 0.72rem !important;
  }
}

.j-rail,
.j-rail:not(.j-rail-column) {
  position: fixed;
  inset: auto;
  left: 50%;
  right: auto;
  top: auto;
  bottom: calc(var(--nav-centre-bottom) - var(--nav-major) / 2);
  z-index: 4;
  display: block;
  width: max-content;
  height: auto;
  margin: 0;
  padding: 0;
  pointer-events: none;
  isolation: auto;
  opacity: 0;
  transition: opacity 0.45s ease;
}
.j-rail.on,
.j-rail:focus-within { opacity: 1; }
.j-rail.on.dim { opacity: 0.1; }
.j-rail-preboot {
  animation: fade-on var(--hero-entry-duration) ease 0.15s both;
}

.j-rail .j-rail-inner,
.j-rail:not(.j-rail-column) .j-rail-inner {
  position: relative;
  width: auto;
  height: auto;
  margin: 0;
  isolation: auto;
}
/* THE SCRIM — the hero copy's own idiom, extended (owner, seventh pass:
   "see the way we darken the left side under the hero text... could you
   also extend that subtle darkness to under the icons?"). This is
   .j-block::before's treatment — a soft radial ellipse in the same ink,
   fading fully to transparent, never a visible box — sized for the row
   plus its labels and SYMMETRIC about the row's centre (the band it
   replaces was the right-rail era's directional gradient, wrong for a
   centred object). Like the copy block's, it is the navigator's OWN
   legibility, owned locally rather than borrowed from a scene effect.
   Its strength rides the detachment: subtle while the group sits
   embedded in the hero's quieter ground, full over the busy root network
   once the object becomes navigation — "thin orange geometry directly
   over other thin orange geometry" is the diagnosed failure this
   prevents.

   IT HANGS ON THE ROW, NOT ON `.j-rail` — and that is the whole of the
   second fix (2026-08-27, NAV-2). Replacing the right-rail's directional
   band with a symmetric ellipse fixed the SHAPE and left the ANCHOR alone,
   so the new scrim missed the icons in the same direction the old one did.
   Measured at 1440x900, connect:

       .j-rail          box  l 720  r 1076   centre 898
       .j-rail-list     box  l 542  r  898   centre 720
       icons + labels        l 542  r 903.6  centre 722.8
       scrim, on .j-rail     l 550  r 1246   centre 898

   `.j-rail` is still the right-rail's rectangle: it begins AT the viewport
   centre and runs rightwards. What centres the row is `.j-rail-list`'s own
   `translateX(-50%)`. So a scrim centred on `.j-rail` puts its densest ink
   at x 898 — the row's right END — and spends its right half, out to
   x 1197, darkening empty scene, while Intro and half of Inspire sit past
   its left edge in ink that has already fallen to zero.

   The row is `.j-rail-list`. The ground belongs to the row, so it is the
   row's own `::before`, and then it cannot drift again: the list's single
   transform — the centring AND the detachment's lift and scale — carries
   the ground with the marks by construction, which also deletes the copy of
   that transform this rule used to keep in sync by hand. One coordinate,
   one transform, one object. (`z-index: -1` inside the list's isolated
   stacking context puts it behind every slot; `pointer-events` is forced,
   because the moon-era rule this pseudo-element inherits turns them on for
   a big invisible square whenever the rail is hot.) */
.j-rail .j-rail-list::before,
.j-rail:not(.j-rail-column) .j-rail-list::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  right: auto;
  bottom: auto;
  z-index: -1;
  width: calc(100% + var(--nav-scrim-extra, 340px));
  height: var(--nav-scrim-height, 260px);
  /* THE HERO FLOOR IS 0.72, NOT 0.55, AND THE REASON IS A MEASUREMENT
     (2026-08-27, NAV-2). The ramp was authored on the idea that the hero
     "sits in the scene's quieter ground" and so needs less. The hero's
     ground under this row is not quieter — it is the brightest ground the
     row ever sits on, because the row is seated among the organism's roots.
     Sampled per mark at 1440x900, mark mean against its OWN local ground:

         Intro    30.4 / 24.9   +5.5
         Inspire  32.0 / 28.9   +3.1
         Equip    36.0 / 29.7   +6.4
         Connect  43.8 / 45.8   -2.0
         Epilogue 44.2 / 55.0  -10.8

     The two rightmost marks were DARKER than what they sat on: not subtle,
     inverted — a hole where a mark should be, which is why Epilogue could
     not be found in the hero frame at all. (Compare the scrolled frame,
     where the same band measures 17-20 across its whole width.)

     0.72 is solved, not chosen: back out the raw ground from the composite
     at 0.55 (R = (g - 10a)/(1 - a) -> ~110 under Epilogue), then pick the
     alpha that brings the worst case under the weakest mark with the same
     headroom the working items have (110 - 100a ~ 38 at a = 0.72). It
     stays a ramp — 0.72 embedded, 1.0 as navigation — so the detachment
     still strengthens the ground, which was the point of having one. */
  /* Purpose lifts the icons to clear its copy, but lifting this whole ellipse
     exposed its lower boundary as a visible dark patch. Cancel that lift for
     the scrim itself so it continues to rise softly from below the viewport;
     ease its density down as the Purpose/Ownership composition forms. */
  opacity: calc(0.72 + 0.28 * (1 - var(--nav-content-u))
                - 0.16 * var(--purpose-scrim-u, 0));
  transform: translate(-50%,
             calc(-50% + 18px + var(--purpose-rail-lift, 0px)));
  background: radial-gradient(ellipse 58% 62% at 50% 50%,
    rgba(12, 8, 3, calc(0.7 - 0.08 * var(--purpose-scrim-u, 0))),
    rgba(10, 7, 2, calc(0.42 - 0.07 * var(--purpose-scrim-u, 0))) 52%,
    rgba(9, 6, 2, 0) 74%);
  -webkit-mask-image: none;
  mask-image: none;
  pointer-events: none !important;
}
/* The legacy geometries' furniture: the edge veil, the resting halo, the
   half-moon's drawn arc. None of it exists in the row. `content: none`
   as well as display — the right-rail era authors these with their own
   !important display and a directional gradient (a band fading LEFT,
   measured live 2026-08-27 still painting from viewport centre to the
   row's right edge), and killing the content is the one veto a later
   block always holds over a pseudo-element. */
.j-rail .j-rail-inner::before,
.j-rail:not(.j-rail-column) .j-rail-inner::before,
.j-rail .j-rail-inner::after,
.j-rail:not(.j-rail-column) .j-rail-inner::after {
  content: none !important;
  display: none !important;
}
.j-rail .j-rail-arc,
.j-rail:not(.j-rail-column) .j-rail-arc { display: none !important; }
/* (`.j-rail-list::before` used to be killed here alongside them. It is not
   any more: it is the row's scrim, authored above. The moon's version of it
   was an invisible hover-catcher with no paint of its own, so the rule above
   restates every box property it set rather than resetting it — there is no
   inherited appearance to suppress, only geometry to replace.) */

.j-rail .j-rail-list {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: center;
  gap: var(--nav-gap);
  margin: 0;
  padding: 0;
  list-style: none;
  /* One transform scales the whole instrument — circles, glyphs, labels,
     hairline, node — so the bookends breathe WITH the trio and the
     internal hierarchy (the weights, the two ink sizes) never re-ranks
     mid-scroll. Continuous in --nav-content-u; no transition, because the
     coordinate itself is continuous and a CSS clock here would chase it. */
  transform: translateX(-50%)
             translateY(calc(-1 * var(--nav-hero-lift) * var(--nav-content-u)))
             translateY(calc(-1 * var(--purpose-rail-lift, 0px)))
             scale(calc(1 + (var(--nav-content-max) - 1) * var(--nav-content-u)));
  transform-origin: 50% 50%;
  height: var(--nav-major);
}

/* A blocked scroll attempt points to the actual controls. The five marks
   rise in sequence like a small wave, then settle exactly back onto their
   authored transforms. It never moves the row, labels, camera, or progress. */
@keyframes j-rail-navigation-wave {
  0%, 100% { transform: translateY(0) scale(var(--glyph-scale, 1)); }
  38% { transform: translateY(-7px) scale(calc(var(--glyph-scale, 1) * 1.035)); }
  68% { transform: translateY(1px) scale(var(--glyph-scale, 1)); }
}
.j-rail.j-rail-wave .j-rail-slot .j-rail-mark {
  animation: j-rail-navigation-wave 0.68s cubic-bezier(0.22, 0.75, 0.25, 1)
    calc(var(--i, 0) * 65ms) both;
}
@media (prefers-reduced-motion: reduce) {
  .j-rail.j-rail-wave .j-rail-slot .j-rail-mark { animation: none; }
  .j-rail.j-rail-wave .j-rail-slot .j-rail-mark::after {
    border-color: rgba(248, 208, 112, 0.88);
    box-shadow: 0 0 7px rgba(240, 190, 90, 0.22);
  }
}

/* ---- the slots: two circle sizes on one centre line -------------------- */
.j-rail .j-rail-slot,
.j-rail:not(.j-rail-column) .j-rail-slot,
.j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open) .j-rail-slot,
.j-rail:not(.j-rail-column).j-rail-following .j-rail-slot {
  position: relative;
  inset: auto;
  z-index: auto;
  width: var(--item-d);
  height: var(--nav-major);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 1;
  pointer-events: auto;
  transform: none !important;
  transition: none;
}
.j-rail .j-rail-slot.j-rail-major {
  --item-d: var(--nav-major);
  --glyph-fit: var(--nav-fit-major);
  --slot-ring-d: var(--nav-ring-d);
}
.j-rail .j-rail-slot.j-rail-minor {
  --item-d: var(--nav-minor);
  --glyph-fit: var(--nav-fit-minor);
  --slot-ring-d: var(--nav-minor-ring-d);
}
/* The preboot connector must use the same ring-edge geometry rowFrame()
   publishes once JS starts. The old fallback covered only `gap - air`, then
   gained 8–10px when the inline measurement arrived, producing a visible
   load-time stretch. The final slot has no segment; the penultimate slot is
   the only one whose next neighbour is minor. */
.j-rail .j-rail-slot {
  --connector-next-item-d: var(--nav-major);
  --connector-next-ring-d: var(--nav-ring-d);
}
.j-rail .j-rail-slot:nth-last-child(2) {
  --connector-next-item-d: var(--nav-minor);
  --connector-next-ring-d: var(--nav-minor-ring-d);
}
.j-rail:not(.j-rail-column) .j-rail-slot:is(.now, .active) { opacity: 1; }

/* ---- SEGMENTED HAIRLINE. Every rule stops the same optical distance from
   the neighbouring drawn rings. The shared clearance keeps the row airy and
   makes each destination read as a node rather than a bead pierced by a bar.
   (::before, the old dotted FILL layer, stays retired.) */
.j-rail .j-rail-slot::before { content: none !important; }
.j-rail .j-rail-slot::after {
  content: "";
  position: absolute;
  top: 50%;
  left: var(--connector-left,
            calc(var(--item-d) / 2 + var(--slot-ring-d) / 2
                 + var(--nav-connector-air)));
  z-index: 0;
  width: var(--connector-width,
             calc(var(--item-d) / 2 + var(--nav-gap)
                  + var(--connector-next-item-d) / 2
                  - var(--slot-ring-d) / 2
                  - var(--connector-next-ring-d) / 2
                  - var(--nav-connector-air)
                  - var(--nav-connector-air)));
  height: 1px;
  margin-top: -0.5px;
  background: rgba(220, 170, 76, 0.3);
  pointer-events: none;
}
.j-rail .j-rail-slot:last-child::after { content: none; }

/* The hero paints NO node at all (owner, seventh pass: "the dot below
   should NOT show in the hero. It shows right now and then disappears").
   The preboot shell is data-layout="mission" and carries no inline
   opacity, so without this rule the dot was PAINTED at first paint and
   then killed ~100ms later when the live rail's inline write landed —
   the same wrong-first-frame silhouette as the glyph-fit jump. The live
   rail's per-frame inline opacity (ringOpacity x dockingU) takes over
   from this rule the moment it exists, and agrees with it at the hero. */
.j-rail[data-layout="mission"] .j-rail-active-ring { opacity: 0; }
/* ---- THE NODE (owner, third pass: "a brighter ring + perhaps a tiny
   filled gold node underneath. No underline, giant bloom, extra ring.")
   One small filled gold point. It rides --active-x — the same continuous
   coordinate the old ring rode.
   IT TRAVELS ON THE LINE AND RESTS INSIDE THE MARK (2026-10-04 — Hannah:
   "the dot actually should always rest and end up in the middle of the
   section icon ... behind it ... we should only basically see it when
   we're travelling between views ... and it should travel along the
   line"). It used to hang at the label's depth under the row; it now sits
   at the connectors' own height, beneath the marks, and the rail fades it
   out wherever it is under one (rail.js clearOfMark). */
.j-rail .j-rail-active-ring {
  position: absolute;
  top: 50%;
  left: 0;
  z-index: 0;
  box-sizing: border-box;
  width: 5px;
  height: 5px;
  border: 0;
  border-radius: 50%;
  background: rgba(250, 208, 112, 0.95);
  box-shadow: 0 0 5px rgba(240, 190, 90, 0.45);
  pointer-events: none;
  transform: translate(calc(var(--active-x, 22px) - 50%), -50%);
  transform-origin: center;
  will-change: transform;
  display: block;
}

/* THE PURPOSE -> OWNERSHIP SUBTREE ---------------------------------------
   Purpose is the row's one branch. Ownership / Manifesto sit below its
   existing seat. On Ownership, every ordinary slot gathers horizontally into
   the viewport centre; the four absorbed peers retain full size while sliding
   behind Purpose, then hide at the exact covered endpoint. The vertical
   baseline never changes. None of
   this borrows the generic navigation wave.

   The eased camera ticket (or the adjacent rest interval during real scroll)
   writes every visual custom property per frame. There is no second CSS clock
   to lag behind or continue after the camera stops. Route endpoints select
   the projection, so a Purpose <-> Connect flight cannot flash Ownership just
   because its numeric path crosses that chapter's band. */
.j-rail .j-rail-list {
  top: 0;
  visibility: visible;
  transition: none;
}
.j-rail.j-rail-purpose-gathering .j-rail-list > .j-rail-slot {
  /* Each existing slot converges into the row centre on the camera's eased
     Ownership phase. This is deliberately per-item: the list, its overflow,
     its labels and its scrim remain an untouched surface. */
  transform: translateX(var(--purpose-gather-x, 0px)) !important;
  transform-origin: 50% 50%;
}
.j-rail.j-rail-purpose-gathering .j-rail-list > .j-rail-slot::after {
  /* Retract the connector from each slot's right edge as full-size cards
     pack inward. The value is painted from ownershipU every frame. */
  transform: scaleX(var(--purpose-gather-open-u, 1));
  transform-origin: left center;
}
.j-rail.j-rail-purpose-gathering .j-rail-list > .j-rail-slot:not([data-chapter="final"]) .j-rail-item {
  /* Fade peer ink a little ahead of connector retraction, without changing
     its authored size or lateral pack motion. */
  opacity: calc(var(--purpose-peer-base-opacity, 1) * var(--purpose-peer-open-u, 1)) !important;
}
.j-rail .j-rail-slot.j-rail-major { --purpose-peer-base-opacity: 1; }
.j-rail .j-rail-slot.j-rail-minor { --purpose-peer-base-opacity: 0.68; }
.j-rail.j-rail-purpose-gathering .j-rail-list > .j-rail-slot {
  z-index: 1;
}
.j-rail.j-rail-purpose-gathering .j-rail-list > .j-rail-slot[data-chapter="final"] {
  z-index: 4;
  transform: translateX(var(--purpose-gather-x, 0px))
             scale(var(--purpose-parent-scale, 1)) !important;
}
.j-rail.j-rail-purpose-gathering
  .j-rail-list > .j-rail-slot[data-chapter="final"] .j-rail-item {
  color: rgba(246, 208, 126, 1) !important;
  opacity: 1 !important;
}
.j-rail.j-rail-purpose-gathering
  .j-rail-list > .j-rail-slot[data-chapter="final"] .j-rail-mark::after {
  border-color: rgba(248, 208, 112, 0.95);
}
.j-rail.j-rail-handoff-ownership-tree
  .j-rail-list > .j-rail-slot:not([data-chapter="final"]) .j-rail-item {
  pointer-events: none !important;
}
.j-rail.j-rail-handoff-ownership-tree
  .j-rail-list > .j-rail-slot[data-chapter="final"] .j-rail-item {
  color: rgba(246, 208, 126, 1);
  opacity: 1;
}
.j-rail.j-rail-handoff-ownership-tree
  .j-rail-list > .j-rail-slot[data-chapter="final"] .j-rail-name {
  color: var(--gold-bright);
  opacity: 1;
}

.j-rail .j-rail-purpose-tree {
  position: absolute;
  left: var(--purpose-tree-x, var(--purpose-x, 0px));
  top: calc(var(--nav-major) / 2);
  z-index: 3;
  width: 0;
  height: 0;
  opacity: var(--purpose-tree-opacity, 0);
  filter: blur(var(--purpose-tree-blur, 3px));
  visibility: hidden;
  pointer-events: none;
  transform: translateY(calc(-1 * var(--purpose-rail-lift, 0px)))
             scale(var(--purpose-tree-scale, 1));
  transform-origin: 0 0;
  transition: none;
}
.j-rail.j-rail-purpose-visible .j-rail-purpose-tree {
  visibility: visible;
  pointer-events: auto;
}

/* Purpose's two children hang off Purpose itself, on the side their
   journeys go (2026-10-04): Manifesto above the row, Ownership below it, a
   little back toward Connect; gathered, they stand up-left and down-right of
   Purpose on one diagonal. The tree's origin IS Purpose's icon centre, so
   each child is placed by its own centre (rail.js paints --purpose-*-x/y)
   and each arm is a hairline from Purpose's ring to the child's, aimed along
   the line between their centres with the row's connector air at both ends.
   These dedicated strokes and children may morph; the shared row never clips
   or filters. */
.j-rail .j-rail-purpose-children {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
}
.j-rail .j-rail-purpose-children::before,
.j-rail .j-rail-purpose-children::after {
  content: "";
  position: absolute;
  left: 0;
  top: -0.5px;
  height: 1px;
  background: rgba(220, 170, 76, 0.3);
  transform-origin: 0 50%;
  pointer-events: none;
}
.j-rail .j-rail-purpose-children::before {
  width: var(--purpose-manifesto-length, 0px);
  transform: rotate(var(--purpose-manifesto-angle, -130deg))
             translateX(var(--purpose-branch-start, var(--nav-connector-air)))
             scaleX(var(--purpose-fork-u, 0));
}
.j-rail .j-rail-purpose-children::after {
  width: var(--purpose-ownership-length, 0px);
  transform: rotate(var(--purpose-ownership-angle, 130deg))
             translateX(var(--purpose-branch-start, var(--nav-connector-air)))
             scaleX(var(--purpose-fork-u, 0));
}
.j-rail .j-rail-purpose-child::before { content: none !important; }
.j-rail .j-rail-purpose-child::after { content: none; }
.j-rail:not(.j-rail-column) .j-rail-purpose-children > .j-rail-slot.j-rail-purpose-child {
  position: absolute !important;
  left: 0 !important;
  top: 0 !important;
  margin: 0 !important;
  will-change: transform;
}
.j-rail .j-rail-purpose-child .j-rail-mark {
  /* Clip only the drawn child specimen while it grows. Clipping the children
     container itself also clips the labels that deliberately hang off its
     box, even at a zero inset. */
  clip-path: circle(var(--purpose-mark-clip-radius, 50%) at 50% 50%);
}
/* (outranks the row's own `.j-rail:not(.j-rail-column):is(...) .j-rail-slot`
   transform: none !important) */
.j-rail:not(.j-rail-column) .j-rail-purpose-children > .j-rail-slot.j-rail-purpose-ownership {
  transform: translate(calc(-50% + var(--purpose-ownership-x, 0px)),
                       calc(-50% + var(--purpose-ownership-y, 0px)))
             scale(var(--purpose-child-scale, 0.34)) !important;
  transform-origin: 50% 50%;
  /* out of sight while it is still under Purpose (rail.js childReveal) */
  opacity: var(--purpose-ownership-reveal, 1);
}
.j-rail:not(.j-rail-column) .j-rail-purpose-children > .j-rail-slot.j-rail-purpose-manifesto {
  transform: translate(calc(-50% + var(--purpose-manifesto-x, 0px)),
                       calc(-50% + var(--purpose-manifesto-y, 0px)))
             scale(var(--purpose-child-scale, 0.34)) !important;
  transform-origin: 50% 50%;
  opacity: var(--purpose-manifesto-reveal, 1);
}
/* The up-branch reads upward: its word sits above its mark, where the
   down-branch's sits below (the selected dot follows the same side). */
.j-rail:not(.j-rail-column) .j-rail-slot.j-rail-purpose-child.j-rail-purpose-manifesto .j-rail-name,
.j-rail:not(.j-rail-column) .j-rail-slot.j-rail-purpose-child.j-rail-purpose-manifesto .j-rail-soon-note {
  top: auto !important;
  bottom: calc(50% + var(--item-d) / 2 + 4px) !important;
}

/* The row's existing Purpose item is the sole parent in every state. This
   legacy duplicate stays mounted only so the DOM/lifecycle contract remains
   stable; it is never rendered or exposed to assistive technology. */
.j-rail .j-rail-purpose-parent {
  display: none;
  position: absolute;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: var(--nav-minor);
  height: var(--nav-major);
  color: rgba(233, 216, 181, 0.88);
  text-decoration: none;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translate(-50%, -50%);
  transform-origin: 50% 50%;
  transition: color 0.24s ease;
}
.j-rail .j-rail-purpose-mark {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--nav-minor-ring-d);
  height: var(--nav-minor-ring-d);
  color: rgba(230, 189, 110, 0.9);
}
.j-rail .j-rail-purpose-mark::after {
  content: "";
  position: absolute;
  inset: 0;
  border: 0.75px solid rgba(222, 172, 78, 0.48);
  border-radius: 50%;
  background: rgba(8, 5, 2, 0.48);
  z-index: -1;
  transition: border-color 0.24s ease, box-shadow 0.24s ease;
}
.j-rail .j-rail-purpose-mark .j-sym {
  width: 22px;
  height: 22px;
  transform: translate(-0.4px, -1px);
  filter: none;
}
.j-rail .j-rail-purpose-label {
  position: absolute;
  top: calc(50% + var(--nav-minor) / 2 + 4px);
  left: 50%;
  transform: translateX(-50%);
  font-family: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  font-size: 0.66rem;
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.015em;
  white-space: nowrap;
}
.j-rail .j-rail-purpose-parent:is(:hover, :focus-visible) {
  color: var(--gold-bright);
}
.j-rail .j-rail-purpose-parent:is(:hover, :focus-visible) .j-rail-purpose-mark::after {
  border-color: rgba(248, 208, 112, 0.95);
  box-shadow: 0 0 8px rgba(240, 190, 90, 0.28);
}

.j-rail.j-rail-handoff-ownership-tree .j-rail-purpose-ownership .j-rail-item {
  color: rgba(246, 208, 126, 1);
}
.j-rail.j-rail-handoff-ownership-tree .j-rail-purpose-ownership .j-rail-mark::after {
  border-width: 1px;
  border-color: rgba(248, 208, 112, 0.95);
  box-shadow: 0 0 7px rgba(240, 190, 90, 0.28);
}
.j-rail.j-rail-handoff-ownership-tree .j-rail-purpose-ownership .j-rail-name {
  color: var(--gold-bright);
  opacity: 1;
}

/* This is the dedicated Purpose -> Ownership motion. It starts at the same
   visible seat as the row's active node, then travels down-left to Ownership.
   The row node yields during transit so there is exactly one moving point. */
.j-rail .j-rail-purpose-indicator {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 0;   /* behind the marks it rests in (see the node above) */
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgba(250, 208, 112, 0.95);
  box-shadow: 0 0 5px rgba(240, 190, 90, 0.45);
  opacity: var(--purpose-indicator-opacity, 0);
  transform: translate(calc(-50% + var(--purpose-indicator-x, 0px)),
                       calc(-50% + var(--purpose-indicator-y, 48px)));
  transition: none;
  pointer-events: none;
}
.j-rail.j-rail-handoff-ownership-transit .j-rail-active-ring,
.j-rail.j-rail-handoff-ownership-tree .j-rail-active-ring,
.j-rail.j-rail-branch-dot .j-rail-active-ring {
  opacity: 0 !important;
}

/* ---- the items --------------------------------------------------------- */
/* ONE BRIGHTNESS, TWO WRITERS, AND THE POINTER'S HALF HAS A CLOCK OF ITS OWN
   (2026-08-30 — the illumination dip).

   The journey writes --glyph-r/g/b/alpha per frame from the row's own
   coordinate; the pointer used to answer by REPLACING the whole colour with
   --gold-bright at a selector that carried :not(.now):not(.active). Two
   authorities over one channel, and the second one stops applying in the
   middle of the first one's move — so the button went DARK while it was being
   pressed. Measured at 1440x900 with the pointer parked on Connect through a
   Mission -> Connect jump: relative luminance held at 0.7947 for the move's
   first 1.13s and fell to 0.6510 on the single frame the slot gained `active`,
   i.e. the tile dimmed 18% at the moment it became the destination, then took
   another 0.66s to climb back. Letting go mid-move was worse: 0.7947 -> 0.4989
   in one frame, a 37% collapse, and a cursor flicked on and off during a long
   loop produced that step once per flick.

   --glyph-hot is that second authority, expressed in the first one's terms. It
   is a REGISTERED property, so it interpolates: the pointer's contribution
   eases in and out on its own 0.22s clock and — this is the part a colour swap
   could not do — an interruption starts from the value currently rendered,
   because that is what transitioning a registered custom property means.
   Composed with max() rather than by replacement, so the term can only ever
   BRIGHTEN: an item the journey has already lit past gold (the arrived tile
   rests at 246,208,126 against gold-bright's 240,200,119) is not pulled back
   down by a hover, which is the #34 constraint below stated as arithmetic
   instead of as a :not() pair.

   The `transition: none` note that stood here is still true and still the
   reason the ITEM's colour has no transition: the journey's half must not
   chase the ring a second time. Only the pointer's term has a clock. */
@property --glyph-hot { syntax: "<number>"; inherits: false; initial-value: 0; }
.j-rail .j-rail-item,
.j-rail:not(.j-rail-column) .j-rail-item {
  position: relative;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--item-d);
  height: var(--nav-major);
  --glyph-ink: rgb(
    max(var(--glyph-r, 202), calc(var(--glyph-r, 202) + (240 - var(--glyph-r, 202)) * var(--glyph-hot)))
    max(var(--glyph-g, 202), calc(var(--glyph-g, 202) + (200 - var(--glyph-g, 202)) * var(--glyph-hot)))
    max(var(--glyph-b, 198), calc(var(--glyph-b, 198) + (119 - var(--glyph-b, 198)) * var(--glyph-hot)))
    / max(var(--glyph-alpha, 0.72), calc(var(--glyph-alpha, 0.72) + (1 - var(--glyph-alpha, 0.72)) * var(--glyph-hot)))
  );
  color: var(--glyph-ink);
  text-decoration: none;
  cursor: pointer;
  pointer-events: auto !important;
  /* Colour is a continuous, reversible function of the live journey
     coordinate; a transition here would chase the ring a second time. */
  transition: --glyph-hot 0.22s ease;
}
/* The reduce block at the top of the mobile section already names
   `.j-rail-item`, but only at (0,1,0); the rule above outranks it, so the
   convention has to be restated here to hold. Same reasoning as that block's
   own: the term ends at the element's resting style either way, so switching
   its clock off leaves both states intact and instant. The dip this whole
   note is about does not come back with it — max() is monotone in the term,
   so a hovered peer BECOMING the current item cannot darken whether the
   change is eased or instant. What a reduced-motion visitor loses is only the
   ease on letting go mid-move. */
@media (prefers-reduced-motion: reduce) {
  .j-rail .j-rail-item,
  .j-rail:not(.j-rail-column) .j-rail-item { transition: none; }
}
.j-rail .j-rail-purpose-manifesto button.j-rail-item {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  font: inherit;
  cursor: pointer;
}
.j-rail .j-rail-purpose-manifesto button.j-rail-item:focus-visible {
  outline: 2px solid var(--gold-bright);
  outline-offset: 3px;
}

/* Manifesto rides the existing canvas and frame loop. A soft local scrim sits
   behind the reading measure while the sides and upper scene stay open. */
.j-manifesto[hidden] { display: none; }
body.j-manifesto-open .ui > :not(#site-nav, .j-rail, .hero),
body.j-manifesto-open:not(:has(.j-manifesto.is-returning)) .ui > .hero,
body.j-manifesto-open #site-nav > :not(.logo),
body.j-manifesto-open .j-copy,
body.j-manifesto-open .j-hotspots,
body.j-manifesto-open .callouts,
body.j-manifesto-open .scrim,
body.j-manifesto-open .spill {
  /* faded rather than cut (2026-10-04): the section's copy leaves as the
     camera lifts and returns as it lands, instead of popping on both edges */
  opacity: 0 !important;
  visibility: hidden !important;
  pointer-events: none !important;
  transition: opacity 0.6s ease, visibility 0s linear 0.6s !important;
}
/* Give the departing section a full reading beat during the camera lift.
   It clears just before Manifesto's first paragraph begins at two seconds. */
body.j-manifesto-open .j-copy {
  transition: opacity 1.8s cubic-bezier(0.4, 0, 0.6, 1), visibility 0s linear 1.8s !important;
}
@media (prefers-reduced-motion: reduce) {
  body.j-manifesto-open .j-copy { transition: none !important; }
}
/* Hero copy already follows the destination flight's opacity envelope.
   During descent, release it to that writer instead of hiding its arrival
   behind the chrome gate and then replaying a second fade after landing. */
/* ...and fades back in on landing. The keyframes name only their FROM
   value, so each element rises to whatever opacity its own writer (the
   copy envelope, the hero furniture) is painting this frame, and hands back
   to that writer when the animation ends. (.spill is left out: it carries
   the intro's own fill-mode animation, which a second name would restart.) */
@keyframes j-manifesto-reenter { from { opacity: 0; } }
body.j-manifesto-closing .ui > :not(#site-nav, .j-rail, .hero),
body.j-manifesto-closing #site-nav > :not(.logo),
body.j-manifesto-closing .j-copy,
body.j-manifesto-closing .j-hotspots,
body.j-manifesto-closing .callouts,
body.j-manifesto-closing .scrim {
  /* 1.6s = CHROME_FADE_MS in journey/manifesto/branch.js */
  animation: j-manifesto-reenter 1.6s cubic-bezier(0.45, 0, 0.25, 1);
}
@media (prefers-reduced-motion: reduce) {
  body.j-manifesto-closing * { animation: none; }
}
/* the row stays, gathered to Purpose's level with Manifesto lit (rail.js
   setManifesto), above the reading scrim */
body.j-manifesto-open .j-rail { z-index: 112; }
/* ...but never over the site menu (2026-10-04 — Hannah: "the menu button
   still sometimes shows in front of the sidebar menu thing"). The menu and
   its scrim sit at 6/7, under this lifted row, so a menu opened over the
   Manifesto had the row — its menu button among it — drawn on top of the
   panel. While the reading page is up they rise over the row too. */
body:is(.j-manifesto-open, .j-manifesto-closing) .j-menu-scrim { z-index: 113; }
body:is(.j-manifesto-open, .j-manifesto-closing) .j-menu { z-index: 114; }

/* The logo stays at the top left through the Manifesto (2026-10-03 —
   Hannah: "keep the logo in the top left"). Everything else in the page
   chrome is hidden above; the .ui layer itself rises over the reading scrim
   so the mark is never dimmed by it, and a press on it goes home through the
   branch's own eased Return (main.js -> journey.flyTo -> returnTo). */
body.j-manifesto-open .ui { z-index: 111; }
/* keep the reading column clear of the row at the bottom of the frame —
   ALL of it, up to the dot's seat above the Manifesto's own mark
   (2026-10-04, Hannah: check the extended navigation against the text on
   mobile and tablet). A flat 8.5rem cleared the row but not the branch
   standing on it: on a phone the last paragraph ran under the Manifesto
   mark and its word, and at 1024x768 the dot sat on "structure.". The
   reach is the rail's own geometry (journey/rail.js): the row's seat and
   Purpose's lift, the up-arm (0.95 of a major slot, OWN_REACH_Y), the
   seat above the mark (half a slot + 26px), then 24px of air. */
body.j-manifesto-open .j-manifesto {
  --manifesto-pad: clamp(1rem, 4vw, 4rem);
  --manifesto-reserve: max(var(--manifesto-pad),
    calc(var(--nav-centre-bottom) + var(--purpose-rail-lift-max)
         + var(--nav-major) * 1.45 + 26px + 24px));
  padding-bottom: var(--manifesto-reserve);
}
/* ...and on a desktop it sits a little lower (2026-10-04 — Hannah: "the
   alignment of the manifesto on desktop, i think it should be pushed down a
   little"). Centred in the space the reserve above leaves, the column rode
   high: 132px of dark over it at 1440x900 against 315px under it. It now
   drops by up to 6vh (~54px there), but only into slack it actually has:
   the shift is capped at half the room left under the centred panel (its
   own height is the translate's 50%), so the reserve over the rail is never
   entered and a short window simply keeps the column where it was. */
@media (min-width: 1025px) {
  body.j-manifesto-open .j-manifesto-panel {
    translate: 0 clamp(0px,
      calc((100dvh - var(--manifesto-pad) - var(--manifesto-reserve)) / 2 - 50%), 6vh);
  }
}

/* THE WORDS EXHALE BEFORE THE DESCENT: on Return the reading column
   dissolves upward into the dark — blur, lift, fade — while the camera
   begins to sink, and the scrim lets go a beat later, so the flight down
   lands on an open scene instead of cutting away from a page of text. */
.j-manifesto-panel { transition: opacity 1.1s ease, filter 1.1s ease, transform 1.4s cubic-bezier(0.3, 0.6, 0.2, 1); }
.j-manifesto::before { transition: opacity 1.6s ease 0.35s; }
.j-manifesto.is-returning .j-manifesto-panel {
  /* Match ordinary copy's one-second departure (ui/bands.js DEPART_S).
     The camera and reading shade retain their longer return clock. */
  transition: opacity 1s cubic-bezier(0.4, 0, 0.6, 1),
              filter 1s cubic-bezier(0.4, 0, 0.6, 1),
              transform 1.4s cubic-bezier(0.3, 0.6, 0.2, 1);
  opacity: 0;
  filter: blur(6px);
  transform: translateY(-0.9rem);
  pointer-events: none;
}
.j-manifesto.is-returning::before { opacity: 0; transition: opacity var(--manifesto-return-s, 1.6s) cubic-bezier(0.4, 0, 0.6, 1); }
/* ...and it ARRIVES gradually too (2026-10-04 — Hannah: pressing Manifesto
   "immediately darkens... jolty and unintentional"). The root leaves
   `display: none` on the press, which a transition cannot start from, so the
   reading shade used to land at full strength on the first frame — the left
   half of the scene dropped from 60 to 22 (of 255) in one frame. It now rises
   with the lift, under the words, on an animation whose `backwards` fill
   leaves the Return's own transition free to take it away again. */
@keyframes j-manifesto-shade-in { from { opacity: 0; } }
.j-manifesto.is-open::before {
  animation: j-manifesto-shade-in 2.4s cubic-bezier(0.45, 0, 0.3, 1) 0.15s backwards;
}
@media (max-width: 720px) {
  /* Let the departure establish itself before the full-width reading shade
     arrives. The first words and shade then grow together during ascent. */
  .j-manifesto.is-open::before {
    animation-delay: 1.4s;
    animation-duration: 3.2s;
  }
}
@media (prefers-reduced-motion: reduce) {
  .j-manifesto.is-open::before { animation: none; }
}
.j-manifesto {
  position: fixed;
  z-index: 110;
  isolation: isolate;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: clamp(1rem, 4vw, 4rem);
  pointer-events: none;
  color: #f2e4c3;
}
.j-manifesto::before {
  content: "";
  position: fixed;
  z-index: -1;
  inset: 0 auto 0 0;
  width: min(72vw, 70rem);
  background: linear-gradient(90deg,
    rgba(8, 7, 5, 0.76) 0%,
    rgba(8, 7, 5, 0.64) 54%,
    rgba(8, 7, 5, 0.18) 83%,
    transparent 100%);
  pointer-events: none;
}
.j-manifesto-panel {
  box-sizing: border-box;
  width: min(37rem, 46vw);
  display: flex;
  flex-direction: column;
  max-height: min(78dvh, 52rem);
  overflow: hidden;
  overscroll-behavior: contain;
  padding: clamp(0.25rem, 1.2vw, 1.25rem) 0;
  pointer-events: auto;
}
.j-manifesto-panel h1 {
  flex-shrink: 0;
  margin: 0 0 1.25rem;
  font: 400 clamp(2rem, min(3.5vw, 6.2vh), 3.4rem)/1.04 "Cormorant Garamond", Georgia, serif;
  white-space: nowrap;
}

/* ================================================================
   WRITTEN IN LIGHT (2026-10-03 — Hannah: the text "feels very Windows
   2000s"; it should feel like something being revealed — "the feeling of
   magic scrolls" — and dramatic as the whole thing rises). Nothing slides
   or pops: each glyph of the title and each word of the body is INSCRIBED
   — it surfaces out of a blur as warm gold light, then cools to the
   parchment the page reads in, staggered (--w) so a sentence seems to be
   written across the dark rather than switched on. Only opacity, filter,
   colour and text-shadow animate, on a handful of spans at a time.
   ================================================================ */
/* A slower 110 ms word cadence and 3 s ink reveal. Paragraph timing in
   manifesto/pose.js preserves the existing breath between passages. */
.j-manifesto { --stagger: 110ms; --glyph-stagger: 70ms; }

.j-manifesto-glyph {
  display: inline-block;
  opacity: 0;
  white-space: pre;
}
.j-manifesto.title-shown .j-manifesto-glyph {
  animation: j-manifesto-condense 2.6s cubic-bezier(0.2, 0.7, 0.2, 1) both;
  animation-delay: calc(var(--w) * var(--glyph-stagger));
}

.j-manifesto-lines {
  position: relative;            /* the lines' offsetTop is measured from here */
  flex: 0 1 auto;                /* its text's height, shrinking only to fit */
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  overflow: hidden;              /* a reading box, never a scroller */
  padding-top: 0.2rem;
}
/* the column rose: the oldest lines dissolve through the top edge */
.j-manifesto-lines.is-lifted {
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 3.2rem);
  mask-image: linear-gradient(180deg, transparent 0, #000 3.2rem);
}

.j-manifesto-line {
  flex-shrink: 0;
  margin: 0;
  color: rgba(242, 228, 195, 0.86);
  font-size: clamp(0.95rem, min(1.2vw, 2.05vh), 1.18rem);
  line-height: 1.62;
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.62);
  pointer-events: none;
  transform: translateY(var(--lift, 0px));
  transition: transform 1.8s cubic-bezier(0.3, 0.6, 0.2, 1);
}
.j-manifesto-word { opacity: 0; }
.j-manifesto-line.shown .j-manifesto-word {
  animation: j-manifesto-ink var(--word-reveal-duration, 3s) cubic-bezier(0.22, 0.6, 0.25, 1) both;
  animation-delay: calc(var(--w) * var(--stagger) - var(--reveal-lateness, 0s));
}

/* The closing name keeps its bold weight throughout its warm reveal. */
.j-manifesto-word.is-name { font-weight: 600; letter-spacing: 0.01em; }
.j-manifesto-line.shown .j-manifesto-word.is-name {
  animation-name: j-manifesto-name;
  animation-duration: 4.2s;
}
@keyframes j-manifesto-name {
  0% {
    opacity: 0;
    filter: blur(7px);
    color: rgba(255, 214, 140, 0.95);
    text-shadow: 0 0 0 rgba(255, 190, 90, 0);
  }
  14% {
    opacity: 1;
    filter: none;
    color: #ffe2a2;
    text-shadow: 0 0 18px rgba(255, 186, 84, 0.85), 0 0 4px rgba(255, 210, 130, 0.5);
  }
  58% {
    color: #f6cf82;
    text-shadow: 0 0 12px rgba(255, 186, 84, 0.45), 0 1px 12px rgba(0, 0, 0, 0.5);
  }
  100% {
    opacity: 1;
    filter: none;
    color: rgba(246, 226, 184, 0.96);
    text-shadow: 0 1px 12px rgba(0, 0, 0, 0.62);
  }
}

@keyframes j-manifesto-ink {
  0% {
    opacity: 0;
    filter: blur(7px);
    color: rgba(255, 214, 140, 0.95);
    text-shadow: 0 0 0 rgba(255, 190, 90, 0);
  }
  30% {
    opacity: 1;
    filter: blur(1.2px);
    color: rgba(255, 226, 168, 1);
    text-shadow: 0 0 16px rgba(255, 186, 84, 0.75), 0 1px 12px rgba(0, 0, 0, 0.4);
  }
  100% {
    opacity: 1;
    filter: none;
    color: rgba(242, 228, 195, 0.86);
    text-shadow: 0 1px 12px rgba(0, 0, 0, 0.62);
  }
}
@keyframes j-manifesto-condense {
  0% {
    opacity: 0;
    filter: blur(10px);
    transform: translateX(calc((var(--w) - 4.5) * 0.12em)) scale(1.06);
    color: #ffd98a;
    text-shadow: 0 0 0 rgba(255, 190, 90, 0);
  }
  35% {
    opacity: 1;
    filter: blur(1.5px);
    color: #ffe6b0;
    text-shadow: 0 0 28px rgba(255, 186, 84, 0.7);
  }
  100% {
    opacity: 1;
    filter: none;
    transform: none;
    color: #f2e4c3;
    text-shadow: 0 2px 18px rgba(0, 0, 0, 0.62);
  }
}

.j-manifesto-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: normal;
  border: 0;
}

/* The page carries no controls (branch.js, NO CONTROLS ON THE PAGE): the
   reading region itself takes focus, without a ring — Escape leaves it. */
.j-manifesto-panel:focus { outline: none; }

@media (max-width: 720px) {
  .j-manifesto { align-items: flex-end; justify-content: center; padding: 0.75rem; }

  .j-manifesto::before {
    inset: 22% 0 0;
    width: 100%;
    background: linear-gradient(0deg,
      rgba(8, 7, 5, 0.88) 0%,
      rgba(8, 7, 5, 0.72) 65%,
      transparent 100%);
  }

  .j-manifesto-panel {
    width: min(100%, 38rem);
    max-height: min(68dvh, 38rem);
    padding: 0.75rem 0.25rem max(0.5rem, env(safe-area-inset-bottom));
  }

  .j-manifesto-panel h1 { font-size: 2rem; margin-bottom: 0.75rem; }

  .j-manifesto-line { font-size: 1rem; line-height: 1.52; }

}

@media (prefers-reduced-motion: reduce) {
  .j-manifesto-line { transition: none; }
  .j-manifesto-glyph, .j-manifesto-word { opacity: 1; }
  .j-manifesto.title-shown .j-manifesto-glyph,
  .j-manifesto-line.shown .j-manifesto-word { animation: none; }
}

/* THE TWO TIERS OF CHROME. The CORE THREE sit in fine circles; Intro and
   Outro keep their existing symbols inside noticeably smaller circles so
   all five destinations read as controls without flattening the hierarchy. */
.j-rail .j-rail-slot .j-rail-mark,
.j-rail:not(.j-rail-column) .j-rail-slot .j-rail-mark,
.j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
  .j-rail-slot .j-rail-mark {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--item-d);
  height: var(--item-d);
  box-sizing: border-box;
  border: 0;
  border-radius: 50%;
  background: transparent;
  box-shadow: none;
  transform: scale(var(--glyph-scale, 1));
  transform-origin: center;
  transition: none;
}
/* Each circle is a pseudo so its drawn diameter is free of the hit box. */
.j-rail .j-rail-slot:is(.j-rail-major, .j-rail-minor) .j-rail-mark::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--slot-ring-d);
  height: var(--slot-ring-d);
  margin: calc(var(--slot-ring-d) / -2) 0 0 calc(var(--slot-ring-d) / -2);
  box-sizing: border-box;
  border: 0.65px solid rgba(222, 172, 78, 0.38);
  border-radius: 50%;
  background: rgba(8, 5, 2, 0.5);
  z-index: -1;
  transition: border-color 0.3s ease, box-shadow 0.3s ease;
  pointer-events: none;
}
/* ONE clear active signal on the ring: it brightens. (The second half of
   the signal is the small gold node underneath — see THE NODE below.)
   `.at` rides with :hover — the row moves by transform, and a transform
   move does not re-run the browser's hit test; see rail.js, THE POINTER'S
   OWN TRUTH. */
.j-rail .j-rail-slot:is(.j-rail-major, .j-rail-minor).active .j-rail-mark::after {
  border-width: 1px;
  border-color: rgba(248, 208, 112, 0.95);
  box-shadow: 0 0 7px rgba(240, 190, 90, 0.28);
}
.j-rail .j-rail-slot:is(.j-rail-major, .j-rail-minor)
  .j-rail-item:is(:hover, :focus-visible) .j-rail-mark::after,
.j-rail .j-rail-slot:is(.j-rail-major, .j-rail-minor).at
  .j-rail-item .j-rail-mark::after {
  border-width: 0.85px;
  border-color: rgba(244, 202, 110, 0.85);
}
.j-rail .j-rail-slot .j-sym {
  --glyph-optical-x: 0px;
  --glyph-optical-y: 0px;
  width: 24px;
  height: 24px;
  opacity: 1;
  filter: drop-shadow(0 0 2px rgba(9, 6, 2, 0.9))
          drop-shadow(0 0 5px rgba(
            var(--glyph-r, 202),
            var(--glyph-g, 202),
            var(--glyph-b, 198),
            var(--glyph-glow, 0.02)
          ));
  transform: translate3d(
               calc(var(--glyph-optical-x) * var(--glyph-fit, 1)),
               calc(var(--glyph-optical-y) * var(--glyph-fit, 1)),
               0
             ) scale(var(--glyph-fit, 1)) !important;
  transform-origin: center;
  transition: none !important;
}
/* Optical, rather than mathematical, parity: offsets centre the PAINTED
   silhouettes where a mark's authored geometry is asymmetric inside the
   shared 22-unit viewBox. The bookends are intentionally mass-centred, not
   bbox-centred: Mission's ground/stipe put its stroke centroid at y=13.33;
   Final's shared ground and unequal specimens put it at x=11.41, y=14.02.
   Their corrections below cancel those measured offsets at every glyph-fit
   and hero scale. Equip's high foreshortened bowl still takes a small drop. */
.j-rail .j-rail-slot .j-sym-mission {
  --glyph-optical-y: -2.5px;
  width: 23px;
  height: 23px;
}
.j-rail .j-rail-slot .j-sym-inspire { width: 24px; height: 24px; }
.j-rail .j-rail-slot .j-sym-equip {
  --glyph-optical-y: 1px;
  width: 24.5px;
  height: 24.5px;
}
.j-rail .j-rail-slot .j-sym-connect { width: 24px; height: 24px; }
.j-rail .j-rail-slot .j-sym-owned { width: 22.5px; height: 22.5px; }
.j-rail .j-rail-slot .j-sym-final {
  --glyph-optical-x: -0.5px;
  --glyph-optical-y: -3.25px;
  width: 24px;
  height: 24px;
}
/* The retired half-moon recycled one off-edge symbol by translating its SVG
   vertically as progress advanced; the row keeps every item in a permanent
   slot, so that legacy transform must never reach a glyph here. */
.j-rail .j-rail-slot.j-rail-recycle .j-rail-mark > .j-sym,
.j-rail:not(.j-rail-column) .j-rail-slot.j-rail-recycle .j-rail-mark > .j-sym,
.j-rail:not(.j-rail-column).j-rail-following
  .j-rail-slot.j-rail-recycle .j-rail-mark > .j-sym {
  transform: translate3d(
               calc(var(--glyph-optical-x) * var(--glyph-fit, 1)),
               calc(var(--glyph-optical-y) * var(--glyph-fit, 1)),
               0
             ) scale(var(--glyph-fit, 1)) !important;
}

/* ---- colour: current, hover, and the pointer's resolved truth ---------- */
/* The same composition as the base item, with the arrived tier's fallbacks.
   It has to be the same one: this pair outranks the base rule, so a colour
   written here WITHOUT the pointer's term is what used to drop a hovered
   button 18% of its luminance on the single frame it gained `active` — the
   move's own destination going dark under the finger that asked for it. */
.j-rail .j-rail-slot:is(.now, .active) .j-rail-item,
.j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
  .j-rail-slot:is(.now, .active) .j-rail-item {
  --glyph-ink: rgb(
    max(var(--glyph-r, 240), calc(var(--glyph-r, 240) + (240 - var(--glyph-r, 240)) * var(--glyph-hot)))
    max(var(--glyph-g, 200), calc(var(--glyph-g, 200) + (200 - var(--glyph-g, 200)) * var(--glyph-hot)))
    max(var(--glyph-b, 119), calc(var(--glyph-b, 119) + (119 - var(--glyph-b, 119)) * var(--glyph-hot)))
    / max(var(--glyph-alpha, 1), calc(var(--glyph-alpha, 1) + (1 - var(--glyph-alpha, 1)) * var(--glyph-hot)))
  );
  color: var(--glyph-ink);
}
/* The retired fan gave the nav-less Final slot a quieter "echo" voice at
   high specificity; in the row Epilogue is a peer, and rail.js no longer
   assigns the class — this pair stands only against a stale DOM. */
.j-rail:is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
  .j-rail-slot.j-rail-echo:not(.now) .j-rail-item {
  color: rgba(
    var(--glyph-r, 202),
    var(--glyph-g, 202),
    var(--glyph-b, 198),
    var(--glyph-alpha, 0.72)
  );
}
.j-rail .j-rail-slot:is(.now, .active) .j-rail-mark,
.j-rail:not(.j-rail-column):is(.j-rail-hot, :has(:focus-visible), .j-rail-open)
  .j-rail-slot:is(.now, .active) .j-rail-mark {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
}
/* THE POINTED-AT MARK (owner reports #34 and its 2026-08-27 sequel "when I
   hover over the currently selected icon it turns white too"). #34's
   constraint still holds: the active mark rests at the palette's brightest
   gold, so HUE cannot be its hover signal — but #34's parchment answer is
   rejected. The redesign: PEERS lift to gold (they rest dimmer, so hue
   reads); the ACTIVE item answers with a small NON-HUE gesture instead — a
   +4% scale of ring and glyph together, with its ring held at full
   brightness (the rule below outranks the generic hover ring, which would
   otherwise DIM an already-active ring from 0.95 to 0.85). `.at` rides
   with :hover and :focus-visible in every rule of this family, so keyboard
   and pointer stay visually equal (PL-2.2), and a transform-moved row
   still answers a stationary cursor (rail.js, THE POINTER'S OWN TRUTH).

   THE HUE HALF OF THIS IS NOW ARITHMETIC, not a pair of selectors. "Peers lift
   to gold, the active item does not" was expressed as `:not(.now):not(.active)`
   on the rule that replaced the colour — which meant the lift ENDED when a
   peer became active, in the middle of the move that was making it active.
   --glyph-hot composes with max() instead: a peer resting below gold is lifted
   to it, an item the journey has already lit past gold is left alone, and
   nothing has to know which of the two it is. The +4% scale below is
   unchanged, and is still the active item's whole answer.

   THE SELECTOR LIST IS LONG ON PURPOSE. Three older rules up in the retired
   half-moon's block still paint this row's items by replacement on `.at` —
   parchment for a peer, gold for the current one — at specificities up to
   (0,6,0). Raising --glyph-hot alone left those winning, and the row went
   near-white on hover: report #34's own symptom, restored by deleting the
   rule that had been outranking it. Every one of those selectors is matched
   here, at the same specificity and later in the file, so the composed ink is
   what reaches the pixel by the ordinary cascade rather than by !important. */
.j-rail:not(.j-rail-column) .j-rail-slot .j-rail-item:is(:hover, :focus-visible),
.j-rail:not(.j-rail-column) .j-rail-slot.at .j-rail-item,
.j-rail:not(.j-rail-column) .j-rail-slot.at:is(.active, .now) .j-rail-item,
.j-rail:not(.j-rail-column) .j-rail-echo.at:not(.now) .j-rail-item {
  --glyph-hot: 1;
  color: var(--glyph-ink);
}
.j-rail:not(.j-rail-column) .j-rail-slot:is(.now, .active)
  .j-rail-item:is(:hover, :focus-visible) .j-rail-mark,
.j-rail:not(.j-rail-column) .j-rail-slot.at:is(.now, .active)
  .j-rail-item .j-rail-mark {
  transform: scale(calc(var(--glyph-scale, 1) * 1.04));
}
.j-rail:not(.j-rail-column) .j-rail-slot.j-rail-major.active
  .j-rail-item:is(:hover, :focus-visible) .j-rail-mark::after,
.j-rail:not(.j-rail-column) .j-rail-slot.j-rail-major.active.at
  .j-rail-item .j-rail-mark::after {
  border-color: rgba(248, 208, 112, 0.95);
  box-shadow: 0 0 7px rgba(240, 190, 90, 0.28);
}
/* THE WEIGHTS (owner, third pass): active core = 100%, inactive core =
   60-70% (GLYPH_COLOURS' future/past alphas carry that), bookends =
   35-45%. The bookends' extra step down is taken here, on the item, so
   one rule covers glyph and label together — and comes off the moment
   the bookend is the active chapter or is being pointed at. */
.j-rail .j-rail-slot.j-rail-minor .j-rail-item { opacity: 0.68; transition: opacity 0.3s ease; }
.j-rail .j-rail-slot.j-rail-minor.active .j-rail-item,
.j-rail .j-rail-slot.j-rail-minor.at .j-rail-item,
.j-rail .j-rail-slot.j-rail-minor .j-rail-item:is(:hover, :focus-visible) { opacity: 1; }
.j-rail .j-rail-ret { display: none !important; }

/* ---- the labels --------------------------------------------------------
   EDITORIAL, NOT UI (owner's design review, 2026-08-26: "The spaced
   uppercase INSPIRE / EQUIP / CONNECT beneath circles feels like a product
   dashboard rather than the editorial/organic world established by the
   typography"). The word is set in the site's own serif voice, sentence
   case, small and quiet, seated beneath its glyph.

   WHO SHOWS A LABEL, AND WHEN — the two-tier hierarchy is carried by the
   words as much as the sizes:
     · the CORE THREE (Inspire / Equip / Connect) keep their labels at all
       times, at partial presence; active, hover, focus or `.at` lifts one
       to full, and the active word answers in gold.
     · the BOOKENDS (Intro / Outro) stay visible but smaller and quieter,
       so they frame the primary three without disappearing as controls. */
.j-rail .j-rail-name,
.j-rail:not(.j-rail-column) .j-rail-name,
.j-rail:not(.j-rail-column) .j-rail-slot:is(.j-pill-up, .j-pill-dn) .j-rail-name {
  display: block;
  position: absolute;
  top: calc(50% + var(--item-d) / 2 + 4px);
  right: auto;
  bottom: auto;
  left: 50%;
  margin: 0;
  padding: 0 !important;
  transform: translateX(-50%);
  border: 0 !important;
  border-radius: 0;
  background: none !important;
  box-shadow: none !important;
  color: var(--parchment);
  font-family: "Cormorant Garamond", Georgia, "Times New Roman", serif !important;
  font-style: normal !important;
  font-weight: 400 !important;
  font-size: 0.76rem !important;
  line-height: 1 !important;
  letter-spacing: 0.02em !important;
  text-transform: none;
  text-shadow: 0 1px 6px rgba(5, 3, 1, 0.9);
  opacity: 0;
  white-space: nowrap;
  transition: opacity 0.45s ease, color 0.35s ease;
  pointer-events: none;
}
/* Core labels are always visible; active or pointed-at lifts one to full.
   Bookend labels stay silent in the hero, where their ring and symbol carry
   the click affordance, then appear in persistent navigation. */
.j-rail .j-rail-slot.j-rail-major .j-rail-name { opacity: 0.72; }
.j-rail .j-rail-slot.j-rail-minor .j-rail-name {
  font-size: 0.66rem !important;
  letter-spacing: 0.015em !important;
}
.j-rail .j-rail-slot.active .j-rail-name { color: var(--gold-bright); }
.j-rail .j-rail-item:is(:hover, :focus-visible) .j-rail-name,
.j-rail .j-rail-slot.at .j-rail-item .j-rail-name,
.j-rail .j-rail-slot.j-rail-major.active .j-rail-name {
  opacity: 1;
}
.j-rail[data-layout="chapter"] .j-rail-slot.j-rail-minor .j-rail-name { opacity: 0.48; }
.j-rail[data-layout="chapter"] .j-rail-slot.j-rail-minor.active .j-rail-name { opacity: 0.85; }
.j-rail[data-layout="chapter"] .j-rail-slot.j-rail-minor
  .j-rail-item:is(:hover, :focus-visible) .j-rail-name,
.j-rail[data-layout="chapter"] .j-rail-slot.j-rail-minor.at .j-rail-item .j-rail-name {
  opacity: 1;
}
.j-rail[data-layout="mission"] .j-rail-slot.j-rail-minor .j-rail-name {
  opacity: 0;
}
.j-rail[data-layout="mission"] .j-rail-slot.j-rail-minor
  .j-rail-item:is(:hover, :focus-visible) .j-rail-name,
.j-rail[data-layout="mission"] .j-rail-slot.j-rail-minor.at .j-rail-item .j-rail-name {
  opacity: 1;
}
/* Purpose's children use the same editorial label specimen as Inspire, even
   though their equal-sized circle uses the minor hit-box geometry. Their
   hierarchy comes from the branch, not from smaller or dimmer typography. */
.j-rail .j-rail-slot.j-rail-purpose-child .j-rail-item { opacity: 1; }
.j-rail .j-rail-slot.j-rail-purpose-child .j-rail-name,
.j-rail .j-rail-slot.j-rail-purpose-child .j-rail-soon-note {
  font-family: "Cormorant Garamond", Georgia, "Times New Roman", serif !important;
  font-size: 0.76rem !important;
  line-height: 1 !important;
  letter-spacing: 0.02em !important;
}
.j-rail .j-rail-slot.j-rail-purpose-child .j-rail-name {
  color: var(--parchment);
  opacity: 1 !important;
}
.j-rail.j-rail-handoff-ownership-tree
  .j-rail-slot.j-rail-purpose-ownership .j-rail-name {
  color: var(--gold-bright);
  opacity: 1;
}
/* THE LABEL ANSWERS THE POINTER WITH ITS GLYPH (owner, 2026-08-27: "when
   you hover over one of the items it should change the colour of the label
   too, not just the icon"). One item, one colour change — the word and the
   mark move together or the hover reads as two half-answers.

   It needs its OWN rule rather than inheriting the item's hover gold: the
   name pins `color: var(--parchment)` above, and it must, because the item's
   RESTING colour is the dim neutral→gold ramp rail.js writes per frame — a
   label inheriting that would be unreadable at the row's cold end.

   THE ACTIVE ITEM IS EXCLUDED, deliberately. Its label already rests at
   `--gold-bright`; there is no brighter gold to move to, which is the same
   constraint that made the active mark's hover answer non-chromatic (the
   +4% scale above). Turning it any other hue is precisely the fault owner
   report #34's sequel named. So the active item's word does not answer —
   the scale does, for the whole item at once. */
@media (hover: hover) and (pointer: fine) {
  .j-rail:not(.j-rail-column) .j-rail-slot:not(.now):not(.active)
    .j-rail-item:hover .j-rail-name,
  .j-rail:not(.j-rail-column) .j-rail-slot:not(.now):not(.active).at
    .j-rail-item .j-rail-name { color: var(--gold-bright); }
}
.j-rail:not(.j-rail-column) .j-rail-slot:not(.now):not(.active)
  .j-rail-item:focus-visible .j-rail-name { color: var(--gold-bright); }

/* ---- Equip's answer ----------------------------------------------------
   The placeholder's label swaps to its note while pointed at — hover or
   focus on fine pointers, `.at` when the row turns under a parked cursor,
   and the timed `.j-rail-note` flash a touch sets. The note takes the
   label's exact seat and voice so the swap reads as the item answering,
   not as a badge appearing. */
.j-rail .j-rail-soon-note {
  display: block;
  position: absolute;
  top: calc(50% + var(--item-d) / 2 + 4px);
  left: 50%;
  transform: translateX(-50%);
  color: var(--gold-bright);
  font-family: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  font-style: italic;
  font-weight: 400;
  font-size: 0.76rem;
  line-height: 1;
  letter-spacing: 0.02em;
  text-shadow: 0 1px 6px rgba(5, 3, 1, 0.9);
  opacity: 0;
  white-space: nowrap;
  transition: opacity 0.45s ease;
  pointer-events: none;
}
.j-rail .j-rail-slot .j-rail-soon-item:is(:hover, :focus-visible) .j-rail-soon-note,
.j-rail .j-rail-slot.at .j-rail-soon-item .j-rail-soon-note,
.j-rail .j-rail-slot.j-rail-note .j-rail-soon-item .j-rail-soon-note {
  opacity: 1;
}
.j-rail .j-rail-slot .j-rail-soon-item:is(:hover, :focus-visible) .j-rail-name,
.j-rail .j-rail-slot.at .j-rail-soon-item .j-rail-name,
.j-rail .j-rail-slot.j-rail-note .j-rail-soon-item .j-rail-name {
  opacity: 0 !important;
}

/* PURPOSE'S TOP ROW SPEAKS ONLY WHEN ASKED. The stage is projected by the
   camera handoff, not toggled at the first visible tree pixel. `leaving`
   keeps labels in their normal below-mark seat while they fade out; `above`
   changes seats only after that fade; `gathering` keeps the above seat quiet
   while peers pack behind Purpose. The direct-child selectors deliberately
   leave Ownership and Manifesto's branch labels untouched. */
.j-rail.j-rail-purpose-labels-leaving .j-rail-list > .j-rail-slot
  > .j-rail-item > :is(.j-rail-name, .j-rail-soon-note) {
  top: calc(50% + var(--item-d) / 2 + 4px);
  bottom: auto;
  opacity: var(--purpose-label-fade, 0) !important;
  transform: translateX(-50%);
  transition: opacity 0.40s ease, color 0.30s ease;
}
.j-rail:is(.j-rail-purpose-labels-above, .j-rail-purpose-labels-gathering)
  .j-rail-list > .j-rail-slot > .j-rail-item > :is(.j-rail-name, .j-rail-soon-note) {
  top: auto;
  bottom: calc(50% + var(--item-d) / 2 + 5px);
  opacity: 0;
  transform: translate(-50%, 4px);
  transition: opacity 0.40s ease, color 0.30s ease,
              transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.j-rail.j-rail-purpose-labels-above .j-rail-list > .j-rail-slot
  > .j-rail-item:is(:hover, :focus-visible) > .j-rail-name,
.j-rail.j-rail-purpose-labels-above .j-rail-list > .j-rail-slot.at
  > .j-rail-item > .j-rail-name {
  opacity: 1;
  transform: translate(-50%, 0);
}
.j-rail.j-rail-purpose-labels-above .j-rail-list > .j-rail-slot
  > .j-rail-soon-item:is(:hover, :focus-visible) > .j-rail-soon-note,
.j-rail.j-rail-purpose-labels-above .j-rail-list > .j-rail-slot.at
  > .j-rail-soon-item > .j-rail-soon-note,
.j-rail.j-rail-purpose-labels-above .j-rail-list > .j-rail-slot.j-rail-note
  > .j-rail-soon-item > .j-rail-soon-note {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* Gathering has no top-row labels. Keep this authoritative over resting,
   active, touch and incidental hover rules, including the first frames of
   a child selection that interrupts an unfinished Purpose arrival. */
.j-rail:is(.j-rail-purpose-labels-gathering, .j-rail-purpose-child-selected)
  .j-rail-list > .j-rail-slot > .j-rail-item > :is(.j-rail-name, .j-rail-soon-note) {
  opacity: 0 !important;
}

/* A direct Inspire/Connect -> Purpose flight can carry a stationary pointer
   across a moving mark. That crossing is not an intentional hover: keep the
   top-row labels quiet until the camera ticket retires. Child subtree labels
   are outside this direct-child selector. */
.j-rail.j-rail-purpose-arriving .j-rail-list > .j-rail-slot
  > .j-rail-item > :is(.j-rail-name, .j-rail-soon-note) {
  opacity: 0 !important;
  transition: none;
}

/* A bookend orbit is one route with two endpoints, not a tour of the chapter
   coordinates it crosses numerically. Quiet the complete top-row label layer
   for the lap; source subtree labels continue fading with their camera-paced
   tree, and the destination's legitimate label state breathes in only after
   the wrap ticket retires. Direct-child selectors keep Ownership/Manifesto
   outside this rule. */
.j-rail.j-rail-wrap-progress .j-rail-list > .j-rail-slot
  > .j-rail-item > :is(.j-rail-name, .j-rail-soon-note) {
  opacity: 0 !important;
  transition: none;
}
.j-rail.j-rail-wrap-progress .j-rail-list > .j-rail-slot.j-rail-major
  > .j-rail-item > .j-rail-name {
  opacity: calc(0.72 * var(--wrap-core-label-u, 0)) !important;
}

/* Touch has no synthetic hover answer inside Purpose's navigable top row. */
@media (hover: none), (pointer: coarse) {
  /* A tap may retain :hover indefinitely. Preserve the resting/selected
     labels and explicit keyboard focus, without carrying a hover highlight. */
  .j-rail .j-rail-slot.j-rail-major:not(.active) .j-rail-item:not(:focus-visible) .j-rail-name { opacity: .72; }
  .j-rail[data-layout="chapter"] .j-rail-slot.j-rail-minor:not(.active):not(.j-rail-purpose-child)
    .j-rail-item:not(:focus-visible) .j-rail-name { opacity: .48; }
  .j-rail[data-layout="chapter"] .j-rail-slot.j-rail-minor.active:not(.j-rail-purpose-child)
    .j-rail-item:not(:focus-visible) .j-rail-name { opacity: .85; }
  .j-rail[data-layout="mission"] .j-rail-slot.j-rail-minor:not(.j-rail-purpose-child)
    .j-rail-item:not(:focus-visible) .j-rail-name { opacity: 0; }
  /* A touch release may leave the link in the browser's synthetic :hover
     state. Intro is a silent bookend on phones, so that emulated hover must
     not make its label linger after the return journey lands. */
  .j-rail[data-layout="mission"] .j-rail-list
    > .j-rail-slot[data-chapter="mission"] > .j-rail-item > .j-rail-name {
    opacity: 0 !important;
    transition: none !important;
  }
  .j-rail.j-rail-purpose-labels-above .j-rail-list > .j-rail-slot
    > .j-rail-item > :is(.j-rail-name, .j-rail-soon-note) {
    opacity: 0 !important;
    transition: none !important;
  }
  .j-rail .j-rail-purpose-manifesto .j-rail-name {
    opacity: 1 !important;
  }
}

/* (The underline/dot active marker is gone — owner's design review,
   2026-08-26: "No huge glow and no underline." The active statement is the
   glyph's own brightness, the fine halo, and the gathered light.) */

/* ---- the site-menu control: top-right, IN PARITY WITH THE LOGO ----------
   (owner, 2026-08-27: "make sure that the menu button and icon are
   precisely the same size and distance from the top and edge on mobile").
   EDGE parity is exact and uncontested: the menu's inset from the right
   equals the logo's from the left (54.4px desktop, 6vw phone) — measured
   both sides, both frames.

   VERTICAL PARITY WAS OF THE TOP EDGES AND IS NOW OF THE CENTRES — on
   phones. The reasoning that chose tops is kept below because it is still
   the reasoning that governs DESKTOP, and because the number it produced
   was wrong in a way worth naming:

   · THE RESIDUAL BELOW IS MEASURED OFF THE WRONG OBJECT. "7.8px on phone"
     is the gap between the ring's centre and the .logo BOX's centre, and
     the box is 28.4px while the drawing inside it is 34px — the mark
     deliberately overflows its box (see hero.css's .logo note, which is
     what holds navBottom). Measured on the rendered 430x932 and 390x844
     frames from the actual lit pixels, both marks' INK: tops identical,
     centres 5.0px apart, bottoms 10.0px apart. The real residual was 5.0.
   · SO THE OWNER'S 2026-08-30 REPORT AND THIS BLOCK AGREE, and only the
     direction of the last correction changed: the mark "reads slightly too
     high" beside the ring. The phone fix is in hero.css's ≤620 `nav` rule
     — 5px of padding moved from the bottom of the row to the top, which
     lands the two INK centres on one line and leaves navBottom untouched.
     Shot at 0 / 5 / 8 / 10px: 0 is the reported fault, 8 and 10 carry the
     mark visibly BELOW the ring (10 is bottom-edge parity), 5 is the only
     one where neither object reads as the higher.
   · The ring is still not shrunk, and "the same size" is still honoured as
     the touch box rather than the ink. Only the seat moved.

   The three things that decided TOP parity, which still describe desktop:

   · The owner's own reference render is the tie-breaker, because it holds
     both marks in one picture. Measured off it (nav-target-with-labels,
     1672x941, scale 1.161): the menu ring is 50px -> 43.1px at 1440, the
     logo 36x47 -> 31x40.5, and BOTH TOPS SIT ON y=34 -> 29.3. The ring is
     deliberately the larger object there; so it is not shrunk here, and
     "the same size" is honoured as the touch box (44px), not the ink.
   · "The same distance from the top" reads most directly as the top edge's
     distance from the viewport edge — which is what the render shows.
   · The owner also reported the menu FEELS higher while its centre sat
     20.6px LOWER. Whatever produces that (the ring is the heavier, closed,
     higher-contrast mark, and both objects are read against the top edge),
     the correction it asks for is downward — and top alignment moves the
     ring DOWN from centre parity. Two independent signals agree.

   SO THE DESKTOP OFFSET IS NOT ZERO, on purpose: the menu's centre finishes
   5.5px below the logo's, measured against the .logo BOX. That residual is
   just the ring being the taller object with its top on the same line —
   the previous cut drove it to zero against the logo's LINE BOX (whose
   extra 5-7px is invisible descender space) and put the ring's top 2px
   above the logo's, the wrong way for every signal above. Desktop is
   UNCHANGED by the 2026-08-30 phone pass and has had no report of its own.

   The mobile block lives HERE, after the base rule, because an earlier
   ≤620 override was silently losing this cascade — measured live with the
   desktop inset on a 430px frame. */
.j-rail .j-rail-menu,
.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 {
  position: fixed;
  inset: auto;
  /* 2.1rem = 33.6px = the logo ink's own top at 1440x900. */
  top: 2.1rem;
  right: 3.4rem;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid rgba(217, 164, 65, 0.82);
  border-radius: 50%;
  background: rgba(9, 6, 2, 0.2);
  color: rgba(230, 189, 110, 0.92);
  opacity: 1;
  pointer-events: auto !important;
  transform: none !important;
  transition: color 0.25s, border-color 0.25s, background 0.25s,
              box-shadow 0.25s;
}
.j-rail .j-rail-menu::before,
.j-rail:not(.j-rail-column) .j-rail-menu::before,
.j-rail .j-rail-menu::after { display: none !important; }
.j-rail .j-rail-menu .j-rail-mark,
.j-rail:not(.j-rail-column) .j-rail-menu .j-rail-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 21px;
  height: 21px;
  border: 0;
  border-radius: 0;
  background: none;
}
.j-rail .j-rail-menu .j-sym,
.j-rail:not(.j-rail-column) .j-rail-menu .j-sym {
  width: 21px;
  height: 21px;
}
.j-rail .j-rail-menu .j-sym-menu {
  transform: translateX(0.5px);
  transform-origin: center;
}
.j-rail .j-rail-menu .j-rail-name { display: none !important; }
.j-rail .j-rail-menu:is(:hover, :focus-visible) {
  color: var(--gold-bright);
  border-color: var(--gold-bright);
  background: rgba(217, 164, 65, 0.065);
  box-shadow: 0 0 22px rgba(230, 175, 70, 0.2);
}

/* THE TABLET BAND GETS ITS OWN SEAT (2026-08-30, round 2 of the owner's
   parity report). hero.css's ≤900 portrait pass gave the nav its own
   padding (1.6rem), which floats the logo ink's centre at 46.0px — but the
   ring kept falling through to the DESKTOP rule above, whose 2.1rem seat
   was computed against the desktop nav. Measured ink at 744x1133, 768x1024
   AND 820x1180: logo centre 46.0, ring centre 56.0 — a 10px stagger on a
   frame whose centred composition (CTA gone, copy on the axis) is exactly
   where a stagger reads worst. Portrait is centre-parity territory (the
   phone block below made that call first, on the owner's own report), so
   the ring's BOX centre goes to the logo's INK centre: 46 - 44/2 = 24px.
   Landscape ≤900 keeps the desktop nav padding, so it keeps the desktop
   seat — hence orientation: portrait here, same band as hero.css's rule. */
@media (min-width: 621px) and (max-width: 900px) and (orientation: portrait) {
  .j-rail .j-rail-menu,
  .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: 24px;
  }
}

@media (max-width: 620px) and (orientation: portrait) {
  .j-rail .j-rail-menu,
  .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 {
    /* logo INK top 20.8px at 430x932 — the ring's top sits on it. */
    top: 20.8px;
    right: 6vw;
  }
  .j-rail .j-rail-menu .j-rail-mark,
  .j-rail:not(.j-rail-column) .j-rail-menu .j-rail-mark,
  .j-rail .j-rail-menu .j-sym,
  .j-rail:not(.j-rail-column) .j-rail-menu .j-sym {
    width: 20px;
    height: 20px;
  }
  .j-rail .j-rail-menu .j-sym-menu { transform: none; }
}

/* The retired edge file dimmed chapter copy and chips whenever the rail's
   open state was latched (`body.j-rail-on`, ≤720px rules further up). The
   centred row is permanently formed and never stands over the copy, so the
   class no longer means occlusion — at any width. */
body.j-rail-on .j-copy,
body.j-rail-on .j-hotspots { opacity: 1; }

/* Final's rail scrim must sit behind the chapter copy at every viewport.
   Keeping this stacking order consistent is harmless for the other chapters
   and preserves menu/card overlays at their higher committed tiers. */
.j-copy { z-index: 5; }

@media (prefers-reduced-motion: reduce) {
  .j-rail,
  .j-rail .j-rail-list,
  .j-rail .j-rail-purpose-tree,
  .j-rail .j-rail-purpose-parent,
  .j-rail .j-rail-purpose-indicator,
  .j-rail .j-rail-mark,
  .j-rail .j-rail-name,
  .j-rail .j-rail-soon-note,
  .j-rail .j-rail-under,
  .j-rail .j-rail-menu { transition: none !important; }
}

/* ============================================================
   LANE C (2026-08-30 polish §3B.2) — THE CLOSING TITLE BREATHES
   LIKE THE OPENING ONE.
   The panel's two serif theses bookend the outline. The opening
   statement holds a clear band of its own (nothing follows it
   inside its section), while the closing thesis had Manifesto and
   Ownership rows packed straight under its descender line — the
   same authored paddings, read tighter because content follows
   instead of a hairline. A little extra air under the final
   title's own block restores the matched, optically-even band.
   ============================================================ */
.j-menu-list > li:last-child .j-menu-item { padding-bottom: 0.82rem; }
@media (max-width: 620px) and (orientation: portrait) {
  .j-menu-list > li:last-child .j-menu-item { padding-bottom: 1rem; }
}

/* ============================================================
   LANE C (2026-08-30 polish §3B) — THE PANEL BELONGS TO THE
   SAME WORLD AS THE SCENE IT COVERS.
   Cohesion, not decoration: the information architecture, the
   density, the opaque ground and every claim in the .j-menu
   block above are untouched. What changes is material —

     · the scene's amber light leaks a few rem past the seam, so
       the panel edge reads as a boundary IN the world rather
       than a sheet on top of it;
     · the cards' own mycelial network lies under the panel's
       ground at sub-liminal contrast (a near-opaque wash over
       the same SVG the popovers grow behind) — grain, spores
       and mesh, felt more than seen. The panel stays fully
       opaque: the wash bottoms out over a solid base colour,
       so the 2026-08-09 "OPAQUE, deliberately" decision holds;
     · the hairline dividers keep their weight but stop being
       laser-straight in brightness — alpha wanders along each
       line like a filament, two phases so adjacent rules never
       ripple in step;
     · the marks take the initiative picts' stroke weight and
       amber bloom, one drawn-glyph voice for the whole outline
       (the dark scene-contrast rim stays — it is invisible on
       the panel ground and still earns its keep during the
       open/close fade over the scene).
   ============================================================ */
.j-menu {
  background:
    linear-gradient(to right,
      rgba(240, 175, 70, 0.05),
      rgba(240, 175, 70, 0.016) 4rem,
      rgba(240, 175, 70, 0) 8.5rem),
    linear-gradient(rgba(11, 7, 2, 0.93), rgba(11, 7, 2, 0.955)),
    url('../assets/cards/mycelial-underlay.svg') center / cover no-repeat,
    #0b0702;
}
.j-menu-list > li {
  border-image: linear-gradient(90deg,
    rgba(217, 164, 65, 0.03), rgba(217, 164, 65, 0.2) 11%,
    rgba(217, 164, 65, 0.09) 34%, rgba(217, 164, 65, 0.24) 52%,
    rgba(217, 164, 65, 0.07) 72%, rgba(217, 164, 65, 0.19) 89%,
    rgba(217, 164, 65, 0.04)) 1;
}
.j-menu-list > li:nth-child(even) {
  border-image: linear-gradient(90deg,
    rgba(217, 164, 65, 0.05), rgba(217, 164, 65, 0.16) 16%,
    rgba(217, 164, 65, 0.25) 31%, rgba(217, 164, 65, 0.08) 54%,
    rgba(217, 164, 65, 0.21) 77%, rgba(217, 164, 65, 0.05) 95%,
    rgba(217, 164, 65, 0.12)) 1;
}
/* The phone panel is 100vw and the cover-fit network scales with it, so the
   same wash reads a step brighter there; take it further down and let the
   seam-light go with the seam (there is no scene beside a full-width sheet). */
@media (max-width: 620px) and (orientation: portrait) {
  .j-menu {
    background:
      linear-gradient(rgba(11, 7, 2, 0.952), rgba(11, 7, 2, 0.968)),
      url('../assets/cards/mycelial-underlay.svg') center / cover no-repeat,
      #0b0702;
  }
}
.j-menu-mark {
  filter: drop-shadow(0 0 4px rgba(240, 170, 60, 0.32));
}
.j-menu-mark .j-sym path { stroke-width: 1.1; }
/* the close ring answers like any lit node: a soft bloom, not a new shape */
.j-menu-x:focus-visible { box-shadow: 0 0 14px rgba(240, 170, 60, 0.22); }
@media (hover: hover) and (pointer: fine) {
  .j-menu-x:hover { box-shadow: 0 0 14px rgba(240, 170, 60, 0.22); }
}
