/* ============================================================
   RAIL — styles for the site-plan.md §3 component.
   Colour: the 13 semantic slots only, plus --link-gold where the
   room defines one (CLR-01). Zero raw values live here.
   Requires: styles/primitives.css + styles/tokens/<division>.css
             + styles/tokens/motion.css (durations, easings, faces,
             scale stops — this file declares none of its own)
   ============================================================ */

/* The component owns its box model rather than inheriting one.
   Card widths are token-driven (flex-basis: var(--rail-card-w)), so
   a host page without a border-box reset would silently add the
   padding and border on top and every snap offset would drift. */
.rail,
.rail *,
.rail *::before,
.rail *::after { box-sizing: border-box; }

/* ------------------------------------------------------------
   Component scope. Mobile-first: every value below is the phone
   value; the >=48rem block at the bottom is the only override.
   ------------------------------------------------------------ */
.rail {
  --rail-gutter: 1rem;
  --rail-gap: 0.75rem;
  --rail-card-w: min(76vw, 20rem);
  --rail-id-w: var(--rail-card-w);
  --rail-pad-start: var(--rail-gutter);
  --rail-card-pad: 1.25rem;
  --rail-radius: 0.75rem;

  position: relative;
  display: block;
  background: var(--surface-0);
  color: var(--text-hi);
  font-family: var(--face-body);
  /* TYP-03: no exceptions — anything here may contain a digit. */
  font-variant-numeric: tabular-nums;
  padding-block: 2.5rem;
}

/* ---------- head: arrows only. The identity card is the title. -- */

.rail__head {
  display: flex;
  justify-content: flex-end;
  padding-inline: var(--rail-gutter);
  padding-block-end: 1rem;
}

.rail__nav {
  /* Mobile-first: phones swipe. Arrows appear at >=48rem. */
  display: none;
  gap: 0.5rem;
}

.rail__arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 2.5rem;
  block-size: 2.5rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  --hover-base: var(--surface-1);
  background: var(--hover-base);
  color: var(--text-hi);
  transition: background-color var(--t-fast) var(--e-out), transform var(--t-instant) var(--e-out);
}

.rail__chev {
  inline-size: 1.125rem;
  block-size: 1.125rem;
}

/* CLR-03: states are formulas, never hand-picked colours. */
@media (hover: hover) and (pointer: fine) {
  .rail__arrow:hover:not(:disabled) { background: color-mix(in oklab, var(--hover-base), var(--hover-pole, white) 6%); }
}

.rail__arrow:active:not(:disabled) {
  transform: scale(0.965);          /* MTN-06 press */
  filter: brightness(0.92);
}

.rail__arrow:disabled {
  opacity: 0.38;                    /* CLR-03 disabled */
}

.rail__arrow:focus-visible,
.rail__track:focus-visible,
.rail__card--cta:focus-visible,
.rail__link:focus-visible {
  /* CLR-03: the ring follows the link fallback chain, so a light room
     gets its AA-safe gold instead of an accent that vanishes on paper. */
  outline: 2px solid var(--link-gold, var(--interactive));
  outline-offset: 2px;              /* MTN-06: instant, never animated */
}

/* ---------- track: the transport ------------------------------- */

.rail__track {
  position: relative;
  display: flex;
  gap: var(--rail-gap);
  overflow-x: auto;
  overflow-y: hidden;

  /* §3.4 snap points on card boundaries. */
  scroll-snap-type: x mandatory;
  scroll-padding-inline-start: var(--rail-pad-start);

  /* Smoothness is decided per-call in rail.js so that dragging can
     write scrollLeft instantly while arrows still glide. Declaring
     smooth here would make every drag frame animate. */
  scroll-behavior: auto;

  /* §3.1 the vertical axis belongs to the page: pan-y stays allowed
     and vertical overscroll still chains to the document. */
  touch-action: pan-x pan-y;
  overscroll-behavior-x: contain;

  padding-inline: var(--rail-gutter);
  padding-block: 0.5rem;
  list-style: none;

  /* ONE VANISHING POINT FOR THE WHOLE ROW (CARD-INTERACTION-CONTRACT
     §Mechanics 2). Per-card perspective would give every card its own
     eye, so a rail-wide turn would read as five separate tricks rather
     than as one surface rolling over. Perspective is not an animated
     property and costs nothing at rest. */
  perspective: 1400px;

  /* Constrained panel, so hiding the bar is in scope (CMP-07);
     peek + arrows carry the affordance. Reduced motion puts a real
     bar back, since that mode has no glide to imply movement. */
  scrollbar-width: none;
}

.rail__track::-webkit-scrollbar { display: none; }

.rail.is-dragging .rail__track { user-select: none; }

/* A live drag and a programmatic glide both need snap out of the
   way; rail.js restores it once the scroll settles. */
.rail.is-dragging .rail__track,
.rail.is-gliding .rail__track { scroll-snap-type: none; }

/* ---------- cards ---------------------------------------------- */

.rail__card {
  flex: 0 0 var(--rail-card-w);
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: var(--rail-card-pad);
  border: 1px solid var(--border);
  border-radius: var(--rail-radius);
  --hover-base: var(--surface-1);
  background: var(--hover-base);     /* CLR-02 elevation step 1 */
  text-decoration: none;
  color: inherit;
}

@media (hover: hover) and (pointer: fine) {
  /* MTN-06 hover-lift, paired with CLR-03's hover formula: a fixed
     perceptual step toward the room's opposite pole. Each element
     names its own --hover-base, so one rule serves a surface card and
     a gold-filled one alike. */
  .rail__card:not(.rail__card--identity) {
    transition: transform var(--t-fast) var(--e-out), background-color var(--t-fast) var(--e-out);
  }
  .rail__card:not(.rail__card--identity):hover {
    transform: translateY(-3px);
    background: color-mix(in oklab, var(--hover-base), var(--hover-pole, white) 6%);
  }
}

.rail__index {
  margin: 0;
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  line-height: 1.3;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.rail__title {
  margin: 0;
  font-family: var(--face-display);
  font-size: var(--fs-display-md);
  line-height: 1.25;
  font-weight: 600;
  color: var(--text-hi);
}

/* ---------- THE TWO RAIL SLOTS ------------------------------------
   docs/reference/RAIL-SLOT-CONTRACT.md. Every rail gets exactly two
   pictures and everything else on it is text:

     S1 MARQUEE  on the identity card, above its words — the emblem
     S2 FINALE   on the gate card, above its words — the host's bow

   ONE HEIGHT PER SLOT TYPE, ACROSS ALL FOUR RAILS. That is the whole
   reason these are variables and not four sets of numbers: what makes
   a row of rails read as one system is that the same band appears in
   the same place every time, whatever is standing in it. The contents
   scale to fit the band; the band never scales to the contents.

   S2 is 288 because that is what the tallest sprite needs. A figure is
   asked to render FIGURE_TARGET px tall (rail.js), the bake paints
   85-89% of a cell, so the sprite behind a 224px figure is 253-264px —
   and the Scouter's gate carries a read-out above his head as well.
   288 clears both without a card growing round it.
   ------------------------------------------------------------------ */

.rail {
  --slot-s1-h: 128px;
  /* An emblem is bounded on BOTH axes, unlike a figure: the identity
     card is 198px of content on a desktop and 254 on a phone, and an
     emblem that grew into each would be a different emblem on each.
     One box, one drawing. */
  --slot-s1-w: 190px;
  --slot-s2-h: 288px;
}

.rail__slot {
  position: relative;
  flex: 0 0 auto;
  inline-size: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: calc(var(--rail-radius) - 2px);
}

/* THE EMBLEM CROWNS THE CARD. Cards in a flex row all take the height
   of the tallest, and the tallest is now the gate — so the identity
   card is a good deal taller than its own words need. Packed with
   them at the foot, the emblem left a third of the card empty ABOVE
   itself, which reads as a layout that ran out rather than as one that
   was composed. The auto margin puts the mark at the ceiling and the
   name at the floor, and the air between them is the poster. */
.rail__slot--marquee {
  block-size: var(--slot-s1-h);
  margin-block-end: auto;
  /* STATIC, AND UNCLIPPED, SO THE DART CAN LEAVE THE BAND. The mark is
     no longer this band's picture — it is the whole card's, dead centre
     of it — and the only thing standing between an `inset: 0` layer and
     the card's box is a positioned ancestor in between. Making this
     slot static hands that job to the card, and dropping its overflow
     lets the layer out. Nothing in a marquee needed either: the three
     vector emblems are sized to the band, and the analytics tiles carry
     their own `position: relative` for their own absolute layers. The
     FINALE keeps both — its read-out is absolute against it. */
  position: static;
  overflow: visible;
}

/* THE DART IS THE FLOOR OF EVERY IDENTITY CARD, AND IT IS CENTRED ON
   THE CARD — not on the emblem band it used to live in. One baked
   sheet, four cards: the mark turns behind NIL's piece, behind the
   analytics tiles, behind Media's beams, and on Agency's card it is
   the whole picture.

   OUT OF FLOW, AND OUT OF THE BAND. `inset: 0` resolves against the
   nearest positioned ancestor, which is the identity card (see the
   note at .rail__slot--marquee, and the `position: relative` on the
   card itself) — so the layer is the card's own box and the flex
   centres the sprite in it: equal air left and right, equal air top
   and bottom. It contributes nothing to layout, so the three emblems
   that were already placed stay exactly where they were.

   z-index: -1 IS WHAT PUTS IT BEHIND EVERYTHING. An absolutely
   positioned child paints AFTER its in-flow siblings, so a layer the
   size of the card would otherwise sit on top of the card's own words.
   The negative level puts it under all of the card's content and still
   above the card's surface, which is exactly where a floor belongs.
   The card is a stacking context already — it is positioned and it
   carries z-index 2 so content slides beneath it — so the level cannot
   escape into the rail. */
.rail__dart {
  position: absolute;
  inset: 0;
  z-index: -1;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

/* The emblem band's own width cap. NOT the dart layer: that one is the
   card's box now, and a max-inline-size on an element stretched between
   left: 0 and right: 0 does not shrink it symmetrically — it pins it to
   the start edge, which is how a centred layer ends up 49px off. */
.rail__slot--marquee > *:not(.rail__dart) { max-inline-size: var(--slot-s1-w); }

.rail__slot--finale {
  block-size: var(--slot-s2-h);
  align-items: flex-end;            /* the figure stands on the floor */
  margin-block-end: 0.25rem;
}

/* ---------- the baked sheets --------------------------------------
   A slot's picture is pixels, not a renderer. Geometry arrives as
   custom properties from data/motion-manifest.js (via rail.js), and
   the per-division keyframe tracks are generated from the same
   manifest — so the tempo on screen is the tempo that was baked.

   TWO ELEMENTS, ON PURPOSE. `.rail__slot` is the box the card gives
   the picture; `.rail__sprite` is the picture, and it is exactly ONE
   CELL. A sheet is cells laid side by side, so shrinking the
   background inside a wider box does not shrink the picture — it
   reveals the next frame down the edge.

   THE SPRITE IS SIZED, NOT FLEXED. `flex: 1 1 0` with an aspect-ratio
   and an auto inline-size collapsed the Scouter's sprite to zero
   height the moment a read-out shared the box with it — flexbox had no
   definite main size to hand it and resolved the ratio against
   nothing. An explicit height cannot do that, and it is also the only
   way to land four different figures at the same visual presence. */
.rail__sprite {
  block-size: min(100%, var(--sprite-h, 260px));
  inline-size: auto;
  max-inline-size: 100%;
  /* THE FIGURE IS CENTRED, NOT THE SHEET. A cell is framed on the
     subject's bounding box, and a figure that leans — the celly throws
     its arms out to one side — does not sit in the middle of its own
     box. Centring the element therefore left the Agency figure 10px
     right of the card's axis while every measurement said it was
     centred. rail.js computes this from the fraction the bake actually
     painted, so it is the picture that is centred. */
  transform: translateX(var(--fig-dx, 0px));
  background-image: var(--sheet);
  background-repeat: no-repeat;
  background-size: calc(var(--cells, 5) * 100%) 100%;
  background-position: 0% 50%;
  animation: var(--beat, 3.333s) step-end infinite;
}

/* A marquee sheet has no figure to size to — the emblem simply takes
   the band, and its width follows the baked aspect. */
.rail__sprite--marquee { block-size: 100%; }

/* THE DART IS STILL THE BAND'S SIZE, on a layer the size of the card.
   `block-size: 100%` was 100% of the box it sits in, and that box grew
   from a 128px band to the whole card — so the mark stretched to 358px
   tall and the max-inline-size squashed its aspect on the way. The
   height is stated instead: the same emblem, at the same size it always
   was, in the middle of a bigger box. */
.rail__dart .rail__sprite--marquee {
  block-size: var(--slot-s1-h);
  inline-size: auto;
  max-inline-size: 100%;
}

.rail__slot--finale[data-division='analytics'] .rail__sprite { animation-name: rail-motion-analytics; }
.rail__slot--finale[data-division='media']     .rail__sprite { animation-name: rail-motion-media; }
.rail__slot--finale[data-division='agency']    .rail__sprite { animation-name: rail-motion-agency; }
.rail__slot--finale[data-division='nil']       .rail__sprite { animation-name: rail-motion-nil; }

/* Named for the BAKE, not for the card: every identity card carries
   this layer, and all four play the sheet agency's turntable was baked
   into. */
.rail__dart[data-dart='agency'] .rail__sprite { animation-name: rail-marquee-agency; }

/* ---------- NIL's marquee: the piece -------------------------------
   PuzzleMake's own geometry, ported path-for-path (src/rail-slots.js).
   Gold as a FILL, which is the form the light room's rule allows —
   CLAUDE.md forbids --interactive for gold TEXT on paper and this is
   not text. The float is transform only (MTN-01.4) and never leaves
   the band. */
.rail__piece {
  /* Short of the band on purpose. The float is a transform and a
     rotation grows a bounding box, so a piece filling the band exactly
     would have its corners shaved off by the slot's own overflow at
     each end of the swing. */
  block-size: 80%;
  inline-size: auto;
  /* THE CARD LAYER'S TONE WHERE A ROOM DECLARES ONE. NIL does — its
     gold is a door's weight and this is a card's — and the fallback is
     what every other room keeps, so the token is one room's opinion
     rather than a change to the emblem rule. */
  color: var(--card-accent, var(--interactive));
  animation: rail-float 6s var(--e-inout, ease-in-out) infinite alternate;
}

@keyframes rail-float {
  from { transform: translateY(3px) rotate(-1.6deg); }
  to   { transform: translateY(-3px) rotate(1.6deg); }
}

/* ---------- ANALYTICS's marquee: the read --------------------------
   Scouter's dashboard grammar — mono label, one large number carrying
   the verdict in its colour, plain line under it. Four pairs of tiles
   cross-fade in place, because that is what a dashboard does; nothing
   slides, nothing counts up.

   TYP-02, and the lesson the chain taught: the floor goes on the
   SMALLEST element, not on the dial. --stat-em is the dial and the
   label is 0.86 of it, so the clamp is set where 0.86 of its floor is
   still over 10.5px. */
.rail__stats {
  --stat-em: clamp(13px, calc(0.055 * var(--rail-id-w, 240px)), 15px);
  position: relative;
  inline-size: 100%;
  block-size: 100%;
  /* The tiles are the one emblem that would otherwise stretch, so the
     S1 box is stated rather than inherited. */
  max-inline-size: var(--slot-s1-w);
  font-family: var(--face-display);
  font-size: var(--stat-em);
}

.rail__stat-pair {
  position: absolute;
  inset: 0;
  display: flex;
  gap: 0.4rem;
  opacity: 0;
  /* One track, N layers, each offset by its own share of the cycle.
     The negative delay is what starts a layer partway in, so the four
     of them are four windows onto one timeline rather than four
     timers. rail.js writes the keyframes from the DATA's length, so a
     fifth pair cannot leave a quarter of the loop blank. */
  animation: rail-stat-fade var(--stat-cycle, 12s) linear infinite;
  animation-delay: calc(-1 * var(--pair) * var(--stat-cycle, 12s) / var(--stat-n, 4));
}

.rail__stat {
  flex: 1 1 0;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.15em;
  padding: 0.5em 0.55em;
  border: 1px solid var(--border);
  border-radius: 3px;
  background: color-mix(in srgb, var(--surface-0) 70%, transparent);
  text-align: start;
}

.rail__stat-k {
  font-family: var(--face-mono);
  font-size: 0.86em;
  line-height: 1.25;
  letter-spacing: 0.1em;
  color: var(--text-dim);
  /* Two lines' worth whether it needs two or one. A tile is 190px of
     card split in half, so PENALTY MINS wraps and XG SHARE does not —
     and with the tiles centred, one wrapping label pushed its own
     number half a line below its neighbour's. Reserving the space
     lines every pair up on the number, which is what the eye is
     actually comparing. */
  min-block-size: 2.5em;
}

.rail__stat-v {
  font-size: 1.65em;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;      /* TYP-03 */
  color: var(--success);
}

.rail__stat[data-tone='concern'] .rail__stat-v { color: var(--danger); }

.rail__stat-n {
  font-size: 0.86em;
  line-height: 1.25;
  color: var(--text-dim);
}

/* ---------- MEDIA's marquee: the spotlight -------------------------
   Two beams converging on a pool of the room's gold. The lamps take
   currentColor; the beams and the pool take --text-hi (cream), --info
   (Media's plum, and Media's alone) and --interactive through their
   gradient stops.

   PHOTOSENSITIVITY, as a number rather than a hope: one pulse per six
   seconds against the contract's ceiling of one per two, and it is a
   soft opacity ramp on a small ellipse — a localized highlight, not a
   change of luminance across the slot. */
.rail__spot {
  block-size: 100%;
  inline-size: auto;
  max-inline-size: 100%;
  color: var(--interactive);
  overflow: visible;
}

/* transform-box defaults to view-box for an SVG child, so these
   origins are the lamps' own coordinates: each beam swings from its
   own housing rather than both from the middle. */
.rail__beam {
  transform-origin: 28px 10px;
  animation: rail-beam 5s var(--e-inout, ease-in-out) infinite alternate;
}

.rail__beam--b {
  transform-origin: 148px 10px;
  animation-duration: 6.5s;
}

.rail__flash { animation: rail-flash 6s var(--e-inout, ease-in-out) infinite; }

@keyframes rail-beam {
  from { transform: rotate(-2.6deg); }
  to   { transform: rotate(2.6deg); }
}

@keyframes rail-flash {
  0%, 84% { opacity: 0; }
  90%     { opacity: 0.3; }
  100%    { opacity: 0; }
}

/* ---------- the analytics read-out: a synced carousel -------------
   One chip-height track above the figure. The current event is
   centred; the one before peeks out under the left edge and the one
   after peeks in from the right — the rail's own peek grammar, so a
   card behaves like the thing it sits in. The mask is what makes them
   slide UNDER the card rather than stop at it.

   IN LOCKSTEP WITH THE SPRITE, by construction: rail.js generates
   this track's keyframes from the same seq array that positions the
   sheet, with the same duration and the same step-end timing, so chip
   k is centred exactly while the figure performs frame k.

   TYP-02: the version this replaced floored its dial at 10px and set
   the label to 0.56em of it — 5.6px of letterspaced type, which is not
   small, it is unreadable. The floor here is on the SMALLEST element,
   so nothing renders under about 10.6px.

   Colours cite the room: the layer carries data-room="analytics", so
   --border, --text-hi, --text-dim, --success and --surface-0 resolve
   to the values the design named, with no literal in this file. */
/* The read-out floats over the top of the box rather than sharing the
   flex line, so the sprite's height is a question about the box alone.
   The sprite then gives back the band below. */
.rail__slot--finale[data-division='analytics'] .rail__sprite {
  block-size: min(calc(100% - var(--chain-h, 54px)), var(--sprite-h, 260px));
}

.rail__chain {
  --chain-em: clamp(11.5px, calc(0.055 * var(--rail-card-w, 230px)), 13px);
  /* ABSOLUTE, NOT A PERCENTAGE. The track translates by whole chip
     widths, and a percentage translateX resolves against the track's
     own border box while a percentage flex-basis resolves against its
     content box — two different numbers wearing the same 62%. The
     strip drifted a chip and a half per cycle and slid the verdict off
     the card. One unit for both removes the question. */
  --chip-w: 11em;
  position: absolute;
  inset-block-start: 0.35rem;
  inset-inline: 0;
  overflow: hidden;
  font-family: var(--face-display);
  font-size: var(--chain-em);
  pointer-events: none;
  /* Soft edges, so a chip leaves the card instead of being cut by it. */
  mask-image: linear-gradient(90deg, transparent 0%, black 14%,
    black 86%, transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, black 14%,
    black 86%, transparent 100%);
}

.rail__chain-track {
  display: flex;
  align-items: center;
  /* Half a card in, minus half a chip: chip 0 starts centred, and each
     step of the animation slides exactly one chip. */
  padding-inline-start: calc(50% - var(--chip-w) / 2);
  animation: var(--beat, 3.333s) step-end infinite;
}

.rail__slot--finale[data-division='analytics'] .rail__chain-track {
  animation-name: rail-motion-analytics-chips;
}

.rail__chip {
  flex: 0 0 var(--chip-w);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.3em 0.4em;
  border: 1px solid var(--border);
  border-radius: 3px;
  background: color-mix(in srgb, var(--surface-0) 82%, transparent);
  text-align: center;
}

.rail__chip-k {
  font-family: var(--face-mono);
  font-size: 0.92em;
  letter-spacing: 0.1em;
  line-height: 1.2;
  color: var(--text-dim);
  white-space: nowrap;
}

.rail__chip-v {
  font-size: 1.08em;
  font-weight: 600;
  letter-spacing: 0.05em;
  line-height: 1.25;
  color: var(--text-hi);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;      /* TYP-03 */
}

.rail__chip.is-verdict {
  background: var(--success);
  border-color: var(--success);
}

.rail__chip.is-verdict .rail__chip-k { color: var(--on-interactive); opacity: 0.8; }
.rail__chip.is-verdict .rail__chip-v { color: var(--on-interactive); }

/* MTN-08, LAST IN THE SECTION ON PURPOSE. Not "slower" — stopped, and
   stopped on something designed: the first cell of every sheet is the
   pose its sequence opens from, the read-out holds the event that goes
   with it, the piece rests level, the beams stand still and the read
   shows its first pair.

   ON [data-division] AND [data-kind], to beat the (0,2,1) rules above
   that name each division's track — and placed AFTER them rather than
   before, because those are (0,2,1) too and at equal specificity the
   later rule wins. The first version of this sat above the carousel
   and lost silently: the figure froze and the read-out kept sliding.

   Every one of the eight slots is named here. A slot that is not is a
   slot that keeps moving. */
@media (prefers-reduced-motion: reduce) {
  .rail__slot[data-division] .rail__sprite {
    animation: none;
    background-position-x: 0%;
  }
  .rail__slot--finale[data-division] .rail__chain-track {
    animation: none;
    transform: none;
  }
  .rail__slot[data-division] .rail__piece,
  .rail__slot[data-division] .rail__beam,
  .rail__slot[data-division] .rail__flash {
    animation: none;
    transform: none;
    opacity: 0;
  }
  .rail__slot[data-division] .rail__piece,
  .rail__slot[data-division] .rail__beam { opacity: 1; }
  /* The layers are opacity 0 by default and the animation is what
     lifts one of them; stopping the animation would leave the tile
     stack blank, so the first pair is named as the still. */
  .rail__slot[data-division] .rail__stat-pair { animation: none; opacity: 0; }
  .rail__slot[data-division] .rail__stat-pair:first-child { opacity: 1; }
}


/* ---------- v3: THE BACK FACES YOU CAN PRESS ---------------------
   Contract v1.2. L1i and L1e put controls on a back face, which the
   flip machinery above already supports — what is new here is what
   those controls look like, and one structural fact: on these cards
   the FRONT FACE is the button, so it is the element that gets the
   press treatment and the focus ring.

   Every value below is a room token. The type floor is TYP-02's
   10.5px and the smallest thing on any of these faces is a field
   label at 0.6875rem = 11px. */

.rail__card--open .rail__face--front { outline: none; }
.rail__card--open .rail__face--front:focus-visible {
  outline: 2px solid var(--link-gold, var(--interactive));
  outline-offset: 2px;
}

/* The back faces are taller than a paragraph and shorter than a page.
   If a card is ever narrower than its content, the face scrolls
   INSIDE itself rather than growing the card — a rail whose cards are
   different heights has stopped being a rail.

   ONE AXIS, NOT BOTH, AND THAT IS THE WHOLE POINT OF THE LONGHAND.
   `overflow-y: auto` with `overflow-x: visible` is not a thing a
   browser can do, so the used value of overflow-x becomes `auto` too
   and this face is a scroll container on BOTH axes — one that has
   nothing to scroll horizontally, because its scrollWidth equals its
   clientWidth by construction. The shorthand `overscroll-behavior:
   contain` then told it to keep a horizontal wheel it could not use,
   and a sideways two-finger scroll over an open card died on the card
   instead of reaching the rail underneath it.

   THE Y HALF IS GONE TOO, AND THAT IS THE DEEPER HALF OF THE SAME
   BUG. `contain` was there to stop a pane scrolled to its end from
   carrying on into the page. Measured, it stops more than that:
   Chrome contains the axis whether or not the pane has anywhere to
   go, so a vertical two-finger scroll over an open card moved neither
   the pane nor the page — a rail eating the vertical axis, which is
   the one thing site-plan §3.1 says it may never do.

   And it was defending a case the suite now forbids: every pane of
   every card is asserted to fit inside its card at five viewports, so
   there is nothing to scroll to the end of. What is left is
   `overflow-y: auto` as the net for the day a long word in a narrow
   card overflows one — and on that day the pane scrolls first and the
   page continues after it, which is what a scrollable panel does
   everywhere else on the web and what a visitor already expects.

   So neither axis is contained here. X chains to the track, which is
   the one element on the rail that can spend it; Y chains to the
   page, which §3.1 says is the page's. */
.rail__card--open .rail__face--back {
  justify-content: flex-start;
  overflow-y: auto;
  scrollbar-width: thin;
}

.rail__card--open .rail__back-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  inline-size: 100%;
}

/* ---- L2's front: the teaser -------------------------------------
   The map's own land, outline only. It is a picture of the thing the
   press opens, at the one weight a hairline survives being drawn at
   card scale: the stroke is stated in user units against the map's
   own 1600x760 box, which is what keeps it a hairline whatever the
   card does to the box. */
/* CENTRED, NOT SEATED. `margin-block: auto` puts the world in the
   middle of what the number and the title leave, so pressing the card
   extends it outward from where it already is rather than lifting it
   off the floor of the box. */
.rail__teaser {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-block: auto;
}

/* THE WIDTH IS THE DRAWING'S. A max-block-size on an auto-height SVG
   does not crop it, it shrinks it — preserveAspectRatio keeps the
   whole world inside a shorter box and leaves the box's width empty
   either side, which is a small map in a wide gap rather than a
   teaser. The card gives it its width and the aspect gives it its
   height. */
.rail__teaser-svg {
  display: block;
  inline-size: 100%;
  block-size: auto;
}

/* THE TAKEOVER'S TOKENS, ONE STEP LOUDER FOR THE SCALE. The backdrop
   fills its land with --surface-2 and strokes the coast with --border,
   and one rung of separation is plenty across 1167px of panel. Across
   198px of card it is not: --surface-2 sits 1.10:1 from the card it is
   printed on, which is a map you find rather than a map you see.

   --surface-3 is the same family one rung further — the room's own top
   surface, the one its raised panels already use — and it doubles the
   separation to 1.24:1 without introducing a colour the room does not
   have. The coastline is untouched, and it is still what draws the
   shape; the fill is what makes the shape a continent.

   The takeover's own backdrop is not touched by any of this: these
   rules are .rail__teaser-svg's, and .pmap-land is still --surface-2.

   THE STROKE IS TRANSLATED RATHER THAN COPIED. The backdrop strokes
   0.7 map units, which across 198px is a twelfth of a pixel, so
   non-scaling-stroke pins the coastline to half a CSS pixel — the
   hairline the takeover reads as at its own scale. */
.rail__teaser-svg path {
  fill: var(--surface-3);
  stroke: var(--border);
  stroke-width: 0.5;
  stroke-opacity: 0.75;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}

.rail__teaser-cap {
  margin: 0;
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  line-height: 1.3;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--link-gold, var(--interactive));
}

/* ---- L1i: five choices ------------------------------------------ */
.rail__choices,
.rail__events,
.rail__desc,
.rail__detail-pane,
.rail__mini {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  inline-size: 100%;
}

/* HIDDEN HAS TO WIN. `display: flex` in a rule of its own beats the
   user agent's `[hidden] { display: none }` on specificity, which is
   how a back face ends up showing its list, its form and the state
   after both at the same time. One rule, higher specificity, and the
   attribute means what it says again. */
.rail__face--back [hidden] { display: none; }

/* ---- THE ANSWER BUTTON ------------------------------------------
   THE MAP'S BUTTONS ARE THE SOURCE OF TRUTH. A door on a card and a
   zone on the placement map are the same act — one question, a short
   list, pick one — and the map's .pmap__askopt already settled what
   that looks like: the room's top surface at 82% behind it so it reads
   as a raised thing rather than a hole, a hairline in --border at
   rest, and the accent arriving on hover as both the edge and a 16%
   wash of the same fill. That recipe is lifted here, token for token
   and state for state.

   WHAT IS NOT LIFTED IS THE SCALE. A zone button is 200px wide with
   16px of padding because it is one of three on a full-band surface; a
   door is one of five in a 198px card. The type, the fill, the border
   and the two states are the map's; the box is the card's.

   ONE BLOCK, FOUR PALETTES. Every value here is a room token and every
   rail sits inside its own [data-room], so agency's doors come out
   slate and gold, NIL's paper and beige, analytics' rink and gold,
   media's espresso and gold — without a single per-room rule. */
.rail__choice,
.rail__event {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  inline-size: 100%;
  text-align: start;
  padding: 0.5rem 0.625rem;
  border: 1px solid var(--border);
  background: color-mix(in oklab, var(--surface-3) 82%, transparent);
  color: var(--text-hi);
  font-family: var(--face-display);
  font-weight: 600;
  font-size: 0.8125rem;          /* 13px */
  letter-spacing: 0.02em;
  line-height: 1.25;
  transition: border-color var(--t-fast) var(--e-out),
    background-color var(--t-fast) var(--e-out),
    color var(--t-fast) var(--e-out);
}

.rail__choice:hover,
.rail__event:hover {
  border-color: var(--interactive);
  background: color-mix(in oklab, var(--interactive) 16%, var(--surface-3));
}

.rail__choice:focus-visible,
.rail__event:focus-visible,
.rail__mini-back:focus-visible,
.rail__input:focus-visible {
  outline: 2px solid var(--link-gold, var(--interactive));
  outline-offset: 2px;
}

/* A DOOR ONTO DOORS (contract v1.3). It is the same button as the ones
   beside it, because it is the same kind of answer to the same
   question — the chevron is the only thing that says the next screen
   is another list, and it is the only thing that should. */
.rail__choice-label { flex: 1 1 auto; }

.rail__choice--group { justify-content: space-between; }

.rail__choice-more {
  flex: 0 0 auto;
  font-family: var(--face-mono);
  font-size: 1rem;
  line-height: 1;
  color: var(--link-gold, var(--interactive));
}

/* ---- L1d: the description, and the quietest way in ---------------
   The pane grows to the face so the foot link's auto margin has
   something to push against: the argument sits at the top, the way to
   answer it sits at the bottom, and nothing floats in between them
   however long or short the paragraph turns out to be. */
.rail__desc {
  flex: 1 1 auto;
  gap: 0.5rem;
}

/* THE BODY HAS TO REACH THE FLOOR for the foot link to sit on it. The
   back face is a flex column and its body is auto-height, so a link
   with `margin-block-start: auto` inside an auto-height parent lands
   directly under the paragraph instead — which puts it at a different
   height on every card, because these five descriptions are 81 to 249
   characters long. Scoped to L1d: L1i's and L1e's bodies hold panes
   that replace each other, and growing theirs would move the success
   state's centre. Same pattern as .rail__link and the identity card's
   own auto margin. */
.rail__card--open[data-layout='L1d'] .rail__back-body { flex: 1 1 auto; }

/* NOT A BUTTON, ON PURPOSE (contract v1.4). The selling is the star,
   so the intake wears the room's eyebrow: mono, tracked, at label
   size, in the room's own gold through the fallback chain that keeps
   it AA on paper. It IS a <button> in the markup — it reveals a pane
   and navigates nowhere — and every one of the button's own defaults
   is undone here so that nothing about it reads as a target. */
.rail__foot-link {
  align-self: flex-start;
  margin-block-start: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.25rem 0;
  border: 0;
  background: transparent;
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  line-height: 1.3;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--link-gold, var(--interactive));
  text-decoration: underline;
  text-underline-offset: 0.25em;
  transition: color var(--t-fast) var(--e-out);
}

.rail__foot-link:hover {
  color: color-mix(in oklab, var(--link-gold, var(--interactive)),
    var(--hover-pole, white) 20%);
}

.rail__foot-link:focus-visible {
  outline: 2px solid var(--link-gold, var(--interactive));
  outline-offset: 2px;
}

/* The arrow travels on hover, 2px, which is the whole animation this
   card has. MTN-01.4: transform only, and it never leaves the line. */
.rail__foot-chev {
  transition: transform var(--t-fast) var(--e-out);
}

.rail__foot-link:hover .rail__foot-chev { transform: translateX(2px); }

@media (prefers-reduced-motion: reduce) {
  .rail__foot-link:hover .rail__foot-chev { transform: none; }
}

/* ---- L1e: the events -------------------------------------------- */
.rail__event-date {
  flex: 0 0 auto;
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.1em;
  color: var(--link-gold, var(--interactive));
  font-variant-numeric: tabular-nums;
}

.rail__event-name { flex: 1 1 auto; }

/* A CLOSED EVENT IS STILL AN EVENT. It is a span, not a button — no
   press, no hover, no focus stop — and it says so in words rather
   than only in colour. */
.rail__event.is-closed {
  color: var(--text-dim);
  border-style: dashed;
}

.rail__event-shut {
  flex: 0 0 auto;
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.rail__detail-when {
  margin: 0;
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  line-height: 1.3;
  letter-spacing: 0.1em;
  color: var(--link-gold, var(--interactive));
  font-variant-numeric: tabular-nums;
}

/* ---- the mini intake -------------------------------------------- */
.rail__field {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.rail__field-label {
  font-family: var(--face-mono);
  font-size: 0.6875rem;          /* 11px — TYP-02's floor is 10.5 */
  line-height: 1.3;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.rail__input {
  inline-size: 100%;
  padding: 0.375rem 0.5rem;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-hi);
  font-family: var(--face-display);
  font-size: 0.8125rem;
  line-height: 1.3;
}

.rail__input:hover { border-color: var(--text-dim); }

.rail__mini-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-block-start: 0.25rem;
}

/* AND THE WAY BACK IS THE MAP'S .pmap__change: the quietest control it
   has, dim, borderless except for the rule under the word, and it
   answers a hover by getting brighter rather than by growing a box. */
.rail__mini-back {
  align-self: center;
  padding: 0.375rem 0 0.125rem;
  border: 0;
  border-block-end: 1px solid var(--border);
  background: none;
  color: var(--text-dim);
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  transition: color var(--t-fast) var(--e-out);
}

.rail__mini-back:hover { color: var(--text-hi); }

/* THE SUBMIT IS THE MAP'S .pmap__go, which is the one button on that
   component that COMMITS rather than chooses: no border, the accent as
   the fill, --on-interactive as the only text colour that survives on
   it. Its focus ring is --text-hi rather than the accent, because an
   accent ring on an accent fill is not a ring. */
.rail__mini-send {
  padding: 0.4375rem 0.9375rem;
  border: 0;
  background: var(--interactive);
  color: var(--on-interactive);
  font-family: var(--face-display);
  font-weight: 600;
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
  white-space: nowrap;
  transition: background-color var(--t-fast) var(--e-out);
}

.rail__mini-send:hover {
  background: color-mix(in oklab, var(--interactive), var(--hover-pole, white) 8%);
}

.rail__mini-send:focus-visible {
  outline: 2px solid var(--text-hi);
  outline-offset: 2px;
}

.rail__mini-send[aria-busy='true'] { opacity: 0.7; }

.rail__mini-error {
  margin: 0;
  font-size: 0.75rem;            /* 12px */
  line-height: 1.35;
  color: var(--danger);
}

/* THE WAY THROUGH, ON ITS OWN LINES. Three regions, because a visitor
   who cannot use the form should not also have to guess which of three
   people is theirs. The label is dim and the address is the room's
   link: the region is a signpost, the address is the thing to press. */
.rail__fallback {
  display: block;
  margin-block-start: 0.5rem;
  color: var(--text-dim);
}

.rail__fallback-list {
  display: grid;
  gap: 0.375rem;
  margin-block-start: 0.25rem;
}

/* THE REGION SITS ABOVE ITS ADDRESS, not beside it. Side by side, the
   longest label and the longest address are wider than a 198px card
   and the address is one unbroken token — so it clipped rather than
   wrapped, and the address you could not read was the first one. */
.rail__fallback-line {
  display: flex;
  flex-direction: column;
}

.rail__fallback-region {
  font-family: var(--face-mono);
  font-size: 0.6875rem;          /* 11px — TYP-02's floor is 10.5 */
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.rail__fallback-mail {
  /* An address has no spaces to break at, so it is told where it may. */
  overflow-wrap: anywhere;
  color: var(--link-gold, var(--interactive));
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.rail__ref {
  font-family: var(--face-mono);
  font-size: 0.6875rem;          /* 11px — TYP-02's floor is 10.5 */
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* THE HONEYPOT IS OFF SCREEN, NOT DISPLAY:NONE. A field that is not
   rendered is a field a robot can see is bait; a field that is one
   pixel outside the clip is one it fills in. Never in the tab order,
   never announced. */
.rail__hp {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- the state at the end of all three doors --------------------- */
.rail__done {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  flex: 1 1 auto;
  text-align: center;
}

.rail__done-mark {
  display: grid;
  place-items: center;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  border-radius: 50%;
  background: var(--interactive);
  color: var(--on-interactive);
  font-size: 1.125rem;
  line-height: 1;
}

.rail__done-line {
  margin: 0;
  font-family: var(--face-display);
  font-size: 0.9375rem;
  line-height: 1.35;
  color: var(--text-hi);
}

/* ---- the gate's back --------------------------------------------
   The description is an EMPTY SLOT by design (contract v1.2): the
   element is here and styled, and the words are the operator's. It
   collapses to nothing until it has some, so a gate with no
   description is a gate with a button under its name rather than a
   gate with a gap in it. */
.gate__blurb {
  margin: 0;
  font-size: var(--fs-body-sm);
  line-height: 1.5;
  color: var(--text-dim);
}

.gate__blurb:empty { display: none; }

.gate__go { align-self: flex-start; margin-block-start: 0.25rem; }

/* The destination the front's chevron points at, kept reachable
   through the turn. Mono and dim, under the button rather than beside
   it: leaving the site is the quieter of the two things this face
   offers, and it should look like it. */
.gate__out {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  margin-block-start: 0.5rem;
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  line-height: 1.3;
  letter-spacing: 0.1em;
  color: var(--link-gold, var(--interactive));
  text-decoration: none;
  border-block-end: 1px solid currentColor;
}

.gate__out:hover { color: var(--text-hi); }

.gate__out:focus-visible {
  outline: 2px solid var(--link-gold, var(--interactive));
  outline-offset: 2px;
}

.gate__out-chev { font-size: 0.875rem; line-height: 1; }

.rail__card--cta.rail__card--open .rail__back-body {
  justify-content: center;
  flex: 1 1 auto;
}

/* ---------- pinned identity card (§3 anatomy) ------------------- */

.rail__card--identity {
  flex-basis: var(--rail-id-w);
  /* POSITIONED AT EVERY WIDTH. Above the narrow breakpoint this card
     is sticky, which positions it anyway; below it, it was static, and
     a static card is not the containing block the dart layer needs —
     the layer would have resolved against the track instead and
     centred itself on the whole rail. Declared here so the box the
     dart centres on is the card at any width; the sticky rule further
     down still wins where it applies. */
  position: relative;
  z-index: 2;                        /* content slides beneath it */
  background: var(--surface-2);      /* CLR-02: pinned = one step up */
  scroll-snap-align: start;
  justify-content: flex-end;
}

/* .rail__eyebrow retired: the identity card's brand line moved to the
   top of the box and wears .rail__index, the same face and size the
   numbered cards carry "01" in. */

.rail__division {
  margin: 0;
  font-family: var(--face-display);
  font-size: var(--fs-display-lg);
  line-height: 1.15;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--text-hi);
}

.rail__thesis {
  margin: 0;
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--text-dim);
  max-inline-size: 24ch;
}

/* The identity card's second line, where a division declares one. The
   eyebrow's face and tracking, one step dimmer than the thesis above
   it: it is a caption on the thesis rather than a second thesis, and
   the mono face is what says so at a glance. */
.rail__sub {
  margin: 0;
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  line-height: 1.3;
  /* ONE LINE, WHICH IS WHY THIS IS NOT THE EYEBROW'S 0.18em. The
     eyebrow is one word; this is four, and at the eyebrow's tracking
     "FOR ATHLETES AND SCHOOLS" measures 206px in a 198px column and
     breaks. A second line is not free here: the identity card packs
     its words to the foot, so every line pushes the block up into the
     dart layer behind it — at 0.18em the top of the eyebrow crossed
     the dart's lower edge by 3px. 0.12em measures 190px and the card
     keeps 12px of clearance. */
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* ---------- proof slot (§4 optional asset ref) ------------------ */

.rail__proof {
  margin: 0.25rem 0 0;
  padding: 0.75rem;
  border: 1px dashed var(--border);
  border-radius: 0.5rem;
  background: var(--surface-2);
}

.rail__proof-label {
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  line-height: 1.3;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.rail__proof-ref {
  display: block;
  margin-block-start: 0.25rem;
  font-family: var(--face-mono);
  font-size: var(--fs-body-sm);
  line-height: 1.5;
  color: var(--info);
}

/* ---------- inline gold link ------------------------------------
   CLAUDE.md: on the NIL (light) room gold TEXT must be --link-gold,
   never --interactive. Dark rooms never define --link-gold, so the
   fallback resolves to the room's own accent automatically.
   ---------------------------------------------------------------- */

.rail__link {
  margin-block-start: auto;
  font-size: var(--fs-body-sm);
  line-height: 1.5;
  font-weight: 600;
  color: var(--link-gold, var(--interactive));
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* ---------- CTA card (§2: closes every rail) -------------------- */

.rail__card--cta {
  --hover-base: var(--interactive);
  background: var(--hover-base);
  border-color: var(--interactive);
  color: var(--on-interactive);      /* CLR-03: never --surface-0 */
  justify-content: space-between;
}

.rail__card--cta .rail__title { color: var(--on-interactive); }

.rail__cta-action {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-block-start: auto;
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  line-height: 1.3;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--on-interactive);
}

.rail__cta-action .rail__chev {
  inline-size: 0.875rem;
  block-size: 0.875rem;
}

/* ---------- screen-reader status -------------------------------- */

.rail__status {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- L1: THE CARD TURNS OVER -------------------------------
   docs/reference/CARD-INTERACTION-CONTRACT.md.

   THE ROTATION IS ON `.rail__faces`, NEVER ON `.rail__card`. The
   hover-lift (MTN-06) and the MTN-04 entrance already write the card's
   transform; a third writer would win silently and take one of them
   with it. Two elements, two jobs — and the card stays free to lift
   and press while its faces turn.

   Both faces occupy one grid cell, so the card's height is the taller
   of the two and does not change when it turns — a flip that resized
   its card would move every snap point on the rail.

   DIRECTION HAS A DURATION (MTN-01.2: exits faster than entrances).
   The resting rule carries the RETURN — 150ms — and the flipped rule
   carries the turn — 250ms. Both on --e-inout, which MTN-03 assigns to
   moves that happen on screen. */
.rail__faces {
  display: grid;
  grid-template-areas: 'stack';
  flex: 1 1 auto;
  min-block-size: 0;
  transform-style: preserve-3d;
  transition: transform var(--t-fast) var(--e-inout);
}

.rail__card--flip.is-flipped .rail__faces {
  transform: rotateY(180deg);
  transition-duration: var(--t-base);
}

.rail__face {
  grid-area: stack;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.rail__face--back { transform: rotateY(180deg); }

/* THE PAINT HALF OF "HIDDEN". `inert` (set by rail.js) is what takes a
   face out of the tab order and out of the accessibility tree;
   backface-visibility stops it being painted mid-turn. This is the
   third statement: the face that is leaving stays visible for the
   whole rotation and only then goes, so nothing blinks out from under
   the turn. Zero-duration transitions on `visibility` are the only way
   to say "after 250ms" about a property that cannot tween. */
.rail__face--front {
  visibility: visible;
  transition: visibility 0s linear 0s;
}

.rail__face--back {
  visibility: hidden;
  transition: visibility 0s linear var(--t-fast);
}

.rail__card--flip.is-flipped .rail__face--front {
  visibility: hidden;
  transition-delay: var(--t-base);
}

.rail__card--flip.is-flipped .rail__face--back {
  visibility: visible;
  transition-delay: 0s;
}

/* THE BACK FACE IS COMPOSED, NOT FILLED. The index holds the ceiling
   where the front's does, the sentence sits at the foot, and the rule
   and the label are what stop 121-156 characters floating in the
   middle of 480px of card. */
.rail__face--back { justify-content: space-between; }

.rail__face--back .rail__index { margin-block-end: auto; }

.rail__back-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.rail__back-label {
  margin: 0;
  font-family: var(--face-mono);
  font-size: var(--fs-label);
  line-height: 1.3;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--link-gold, var(--interactive));
}

.rail__rule {
  display: block;
  block-size: 1px;
  inline-size: 100%;
  background: var(--border);
}

.rail__detail {
  margin: 0;
  font-size: var(--fs-body-sm);
  line-height: 1.55;
  color: var(--text-dim);
}

/* A card that answers a click says so. CMP-01's six states, on the one
   component that gained a press: hover is the rail's existing lift,
   focus is the ring below, pressed is MTN-06's 75ms scale — and it
   sits on the CARD, a layer above the faces, so the press does not
   fight the rotation. Cursor comes from styles/cursor.css, which
   already sends [role='button'] the mark; nothing is declared here. */
.rail__card--flip:focus-visible {
  outline: 2px solid var(--link-gold, var(--interactive));
  outline-offset: 2px;
}

.rail__card--flip:active {
  transform: scale(0.965);
  transition-duration: var(--t-instant);
}

@media (hover: hover) and (pointer: fine) {
  .rail__card--flip:active { transform: translateY(-3px) scale(0.965); }
}

/* MTN-01.6, and the contract's §7: the turn collapses to an instant
   face swap. The card still ends on the same designed still — the same
   back face, the same composition — it simply gets there without the
   rotation. Placed after the rules above, at equal specificity, for
   the reason the slot section records. */
@media (prefers-reduced-motion: reduce) {
  .rail__faces,
  .rail__card--flip.is-flipped .rail__faces,
  .rail__face,
  .rail__card--flip.is-flipped .rail__face {
    transition: none;
  }
  .rail__card--flip:active { transform: none; }
}

/* ---------- L2: the takeover --------------------------------------
   docs/reference/CARD-INTERACTION-CONTRACT.md §L2. The trigger turns
   the whole rail over in one moment and a surface settles over the
   band. The surface is a SIBLING of the track, never a descendant of a
   card: a card that can turn is a transformed, preserve-3d ancestor,
   which makes it the containing block for everything inside it — the
   trap the audit measured on the map's own bottom sheet.

   It is sized to the track it covers, in px written by rail.js from
   the track's own offsetTop and offsetHeight, so "the band" is
   measured rather than guessed at through a stack of paddings. */
.rail__takeover {
  position: absolute;
  inset-inline: var(--rail-gutter);
  inset-block-start: var(--takeover-top, 0px);
  block-size: var(--takeover-h, 0px);
  z-index: 3;                        /* over the pinned identity card */
  display: flex;
  flex-direction: column;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--rail-radius);
  background: var(--surface-1);      /* CLR-02: raised, in a dark room */
  /* MTN-07: born in place. It does not slide in from anywhere, because
     it did not come from anywhere — it is the band, turned over. */
  opacity: 0;
  transform: scale(0.985);
  transition: opacity var(--t-base) var(--e-out), transform var(--t-base) var(--e-out);
}

.rail__takeover[hidden] { display: none; }

.rail__takeover.is-open {
  opacity: 1;
  transform: none;
}

.rail__takeover-close {
  position: absolute;
  inset-block-start: 0.5rem;
  inset-inline-end: 0.5rem;
  z-index: 4;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  --hover-base: var(--surface-2);
  background: var(--hover-base);
  color: var(--text-hi);
  transition: background-color var(--t-fast) var(--e-out), transform var(--t-instant) var(--e-out);
}

@media (hover: hover) and (pointer: fine) {
  .rail__takeover-close:hover {
    background: color-mix(in oklab, var(--hover-base), var(--hover-pole, white) 6%);
  }
}

.rail__takeover-close:active { transform: scale(0.965); }

.rail__takeover-close:focus-visible {
  outline: 2px solid var(--link-gold, var(--interactive));
  outline-offset: 2px;
}

.rail__takeover-body {
  flex: 1 1 auto;
  min-block-size: 0;
  display: flex;
  flex-direction: column;
}

/* THE MODE SWITCH. `inert` on the track (set by rail.js) is what
   actually removes it from hit-testing, focus and the accessibility
   tree; this is the visible half — the band underneath goes quiet so
   the surface reads as the only live thing on the rail. */
.rail.is-takeover .rail__track {
  filter: saturate(0.55);
  opacity: 0.5;
  transition: opacity var(--t-base) var(--e-out), filter var(--t-base) var(--e-out);
}

@media (prefers-reduced-motion: reduce) {
  .rail__takeover,
  .rail.is-takeover .rail__track { transition: none; }
  .rail__takeover { transform: none; }
  .rail__takeover-close:active { transform: none; }
}

/* ---------- MTN-04 entrance -------------------------------------
   Staged by rail.js only when the rail first meets the viewport,
   and only when motion is allowed. Transform + opacity only
   (MTN-01.4). 250ms per item, 50ms apart, capped at 6 items by the
   --rail-i the component writes.
   ---------------------------------------------------------------- */

.rail.is-staged .rail__card {
  opacity: 0;
  transform: translateY(12px);
}

.rail.is-staged.is-entering .rail__card {
  opacity: 1;
  transform: none;
  transition: opacity var(--t-base) var(--e-out), transform var(--t-base) var(--e-out);
  transition-delay: calc(var(--rail-i, 0) * 50ms);
}

/* ---------- MTN-01.6 reduced motion -----------------------------
   The rail becomes a plain horizontally scrollable strip: no glide,
   no stagger, no hover travel, and a real scrollbar so the strip
   advertises itself without any motion at all.
   ---------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .rail__card,
  .rail__arrow,
  .rail.is-staged.is-entering .rail__card {
    transition: none;
    transition-delay: 0s;
  }

  .rail.is-staged .rail__card {
    opacity: 1;
    transform: none;
  }

  .rail__card:hover { transform: none; }

  .rail__arrow:active:not(:disabled) { transform: none; }

  .rail__track {
    scroll-behavior: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border) var(--surface-0);
  }

  .rail__track::-webkit-scrollbar {
    display: block;
    block-size: 0.5rem;
  }
}

/* The class mirrors the media query so rail.js and CSS can never
   disagree about which mode is live. */
.rail--reduced .rail__card,
.rail--reduced .rail__arrow {
  transition: none;
  transition-delay: 0s;
}

.rail--reduced .rail__card { opacity: 1; transform: none; }

.rail--reduced .rail__track {
  scrollbar-width: thin;
  scrollbar-color: var(--border) var(--surface-0);
}

.rail--reduced .rail__track::-webkit-scrollbar {
  display: block;
  block-size: 0.5rem;
}

/* ---------- >=48rem: the only breakpoint ------------------------
   §3.5 says the pattern is stronger on mobile, so the phone layout
   is the base. Two things change on a wider viewport: arrows appear
   (there is a pointer to use them with), and the identity card pins
   so it can keep the visitor oriented mid-rail. On a phone it stays
   an ordinary first card — pinning 76vw of a small screen would
   cost more than the orientation is worth.
   ---------------------------------------------------------------- */

@media (min-width: 48rem) {
  .rail {
    --rail-gutter: 2rem;
    --rail-gap: 1rem;
    --rail-card-w: 17rem;
    --rail-id-w: 15rem;
    --rail-pad-start: calc(var(--rail-gutter) + var(--rail-id-w) + var(--rail-gap));
    padding-block: 4rem;
  }

  .rail__nav { display: flex; }

  .rail__card--identity {
    position: sticky;

    /* Zero, NOT --rail-gutter. A sticky inset resolves against the
       flex container's content box, which the track's padding-inline
       has already moved in by one gutter — so a gutter-sized inset
       pins the card at 2x the gutter, one gutter right of its own
       resting position, and its right edge then covers the leading
       edge of card 01 at every snap stop. Zero pins it exactly where
       it sits at rest: flush with the gutter, clear of card 01 by
       --rail-gap. The containing block starts at the gutter, so it
       can never travel further left than that. */
    inset-inline-start: 0;

    /* Pinned, so it is never the thing you navigate to; rail.js
       folds its stop into the first content card's. */
    scroll-snap-align: none;
  }

  /* CARDS SLIDE UNDER THE PINNED CARD, and the leading gutter must not
     show slivers of them going past — that reads as a rendering fault
     rather than as depth.

     THIS USED TO BE A PAINTED CURTAIN, a 100vw block of --surface-0
     hung off the identity card's leading edge, and it was right until
     the drum arrived. Then the sections stopped painting their own
     ground (spine.css: `.spine.has-drum .railsection { background:
     transparent }`) and --surface-0 stopped being what is behind a
     rail — so the curtain became a slab of the wrong colour standing
     in the leftmost 32px of every rail on the spine, near-white paper
     over NIL's drum on one band and a grey block over the wall on the
     other three.

     A MASK CANNOT BE THE WRONG COLOUR, because it paints nothing. The
     leading gutter is erased from the track's own compositing, so what
     shows there is whatever is actually behind the rail: the drum on
     the spine, the section's surface on a deep page. Same job, no
     opinion about the ground.

     LEADING EDGE ONLY. The trailing gutter is where the next card
     arrives from, and fading that would hide the thing the visitor is
     scrolling toward. */
  .rail__track {
    -webkit-mask-image: linear-gradient(to right,
      transparent 0 var(--rail-gutter), #000 var(--rail-gutter));
    mask-image: linear-gradient(to right,
      transparent 0 var(--rail-gutter), #000 var(--rail-gutter));
  }
}
