/* ============================================================================
   THE RAIL — a floating access control, replacing the site header.
   Art direction v3, ledger V3-2.
   ----------------------------------------------------------------------------
   A full-bleed photographic canvas and a solid header bar are incompatible: the
   bar cuts a hard horizontal band across every composition and re-asserts
   "this is a website" over an image that was trying to be a place. v2 had one,
   and it is visible in every screenshot as the first thing that breaks the
   frame.

   The rail is a vertical column of icon controls on the right edge. It floats
   over the canvas, expands to labels on hover and on focus, and is expanded on
   first arrival so nobody has to discover it.

   ⛔ THE ACCESSIBILITY CONTRACT, AND IT IS NOT NEGOTIABLE HERE.
   The buyers are district IT staff and special-education directors, frequently
   keyboard-only, frequently on a screen reader, frequently visiting once and
   under time pressure. Icon-only navigation is the direction; making them guess
   at glyphs is not. So:

     · every control carries REAL TEXT in the DOM — the label is the accessible
       name, never an aria-label bolted onto a bare glyph
     · collapsing is done with max-width + opacity, NEVER display:none or
       visibility:hidden, both of which remove the name from the accessibility
       tree and leave a nameless link
     · the label appears on :hover AND on :focus-within — a keyboard user must
       get exactly what a mouse user gets
     · the glyph itself is aria-hidden, because the text beside it is the name
       and a described glyph would double-announce every item

   shell.js REFUSES to emit a rail item without a label, so a nameless control
   cannot reach a page.
   ========================================================================= */

/* ── THE DISCLOSURE ───────────────────────────────────────────────────────
   Collapsed the rail is ONE control in the bottom-right corner; hovering or
   activating it opens the labelled menu upward from that corner.

   Bottom-right and not centre-right because a full-height rail sits across the
   middle of every composition — on the constellation it covered stars outright,
   and on a portrait viewport it covered them with nowhere to scroll to. A
   corner is the one place a persistent control costs the image nothing. */
/* ── HOW BIG THE FLOATING CHROME IS ───────────────────────────────────────
   26d4 ONE NUMBER, NOT TWELVE. The wordmark and the menu are the two things
   that sit on every page over the photograph, and they are sized together on
   purpose: they are a matched pair in opposite corners, and a change to one
   that misses the other makes the composition lopsided in a way that is hard
   to see and easy to ship.

   Everything either of them measures — glyph box, type size, gaps, padding,
   the label's expanded width — is expressed as a multiple of this. Change the
   1.25 and both corners move together. Hand-multiplying the individual
   declarations is how the pair drifts apart on the next adjustment.

   Founder direction 2026-08-14: +25%. */
:root { --chrome-scale: 1.25; }

.rail {
  position: relative;
  position: fixed;
  bottom: clamp(.9rem, 2.2vh, 1.8rem);
  right: clamp(.9rem, 1.8vw, 1.8rem);
  z-index: 90;
  display: flex;
  flex-direction: column-reverse;   /* the menu opens UPWARD from the button */
  align-items: flex-end;
  gap: calc(.35rem * var(--chrome-scale));
  padding: 0;
  border: 0;
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  /* A hairline and a wash, not a card: the rail must read as an instrument
     laid ON the image, not a panel cut out of it. */
}

/* THE PANEL that holds the ten links.
   ⛔ NO HARD BORDER AND NO BRIGHTNESS KNOCK. The knock was what made every
   surface read as opaque: brightness(.68) darkens whatever is behind the
   panel, so a genuinely translucent fill still looked like a solid card. The
   panel is now blur alone over a very light wash, and the edge is a soft inner
   highlight rather than a drawn line. */
/* ⛔ NO GROUND AT ALL, AND IT ARRIVES IN SEQUENCE.
   It had a fill, a blur and an inset hairline, and it switched display:none ->
   flex, so the whole stack appeared at once as a panel. Both are gone: the
   items sit directly on the sky and fade in one after another.

   ⚠ display:none CANNOT BE TRANSITIONED, which is exactly why it popped. The
   menu is now always laid out and hidden with visibility + opacity, so each
   item has a state to animate FROM. visibility is delayed on the way out so the
   fade finishes before the stack stops being hittable. */
/* ⛔ OUT OF FLOW WHEN CLOSED, OR THE HOVER TARGET IS THE WHOLE COLUMN.
   Switching from display:none to visibility:hidden is what makes the staggered
   reveal possible — there has to be a laid-out state to animate from — but a
   hidden element still OCCUPIES SPACE. So .rail's box stayed the full height of
   the open menu even when it was closed, and the invisible column sat directly
   over the About star: hovering that star opened the menu instead.

   Absolute positioning fixes both halves. The rail's box collapses to the
   button, so nothing but the button is hoverable when closed; and :hover still
   propagates from the menu to .rail because hover follows the DOM TREE, not the
   visual box, so reaching up into the open menu keeps it open. */
/* ⛔ NO DEAD GAP BETWEEN THE BUTTON AND THE MENU.
   This sat at `bottom: calc(100% + .35rem)`, and once the rail's box had
   collapsed to just the button that 0.35rem became a hover void: moving up from
   the button crossed it, .rail:hover went false, and the menu closed before the
   pointer arrived. The same shape as the card being unreachable — a control you
   have to cross open space to get to is a control that closes on the way.

   The box now starts exactly at the button's top edge and carries its visual
   separation as PADDING instead. The gap you see is inside the hover area, so
   the pointer never leaves. */
.rail-menu {
  position: absolute;
  bottom: 100%;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: calc(.1rem * var(--chrome-scale));
  padding: calc(.35rem * var(--chrome-scale)) calc(.2rem * var(--chrome-scale))
           calc(.75rem * var(--chrome-scale));
  pointer-events: none;
  border: 0;
  background: none;
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  visibility: hidden;
  transition: visibility 0s linear 480ms;
}
.rail:hover .rail-menu,
.rail:focus-within .rail-menu,
.rail.is-open .rail-menu { visibility: visible; transition-delay: 0s; }
.rail:hover .rail-menu,
.rail:focus-within .rail-menu,
.rail.is-open .rail-menu { pointer-events: auto; }

/* Each item starts folded down-right and unfolds on its own beat. --i is
   emitted per item by the shell counting from the top, and the delay inverts
   it so the reveal runs BOTTOM-UP: the item nearest the button moves first,
   which is where the reader is already looking. */
.rail-menu .rail-item,
.rail-menu .rail-sep {
  opacity: 0;
  translate: .5rem .35rem;
  transition: opacity 380ms var(--e-out), translate 440ms var(--e-out);
}
.rail:hover .rail-menu .rail-item,
.rail:focus-within .rail-menu .rail-item,
.rail.is-open .rail-menu .rail-item,
.rail:hover .rail-menu .rail-sep,
.rail:focus-within .rail-menu .rail-sep,
.rail.is-open .rail-menu .rail-sep {
  opacity: 1;
  translate: 0 0;
  /* 55ms a step rather than 32. At the old pace the whole stack was in place
     inside a third of a second, which reads as a pop with a stutter rather
     than a reveal. Ten items now unfold over about 0.9s. */
  transition-delay: calc((11 - var(--i, 0)) * 55ms);
}
/* Leaving is NOT the entrance reversed — a staggered exit reads as the menu
   being reluctant to go. Everything leaves together, quickly. */
.rail:not(:hover):not(:focus-within):not(.is-open) .rail-menu .rail-item,
.rail:not(:hover):not(:focus-within):not(.is-open) .rail-menu .rail-sep {
  transition-duration: 130ms;
  transition-delay: 0ms;
}
/* With no panel underneath, each item carries its own legibility. */
.rail-menu .rail-item { filter: drop-shadow(0 1px 3px rgba(0,0,0,.92)) drop-shadow(0 0 10px rgba(0,0,0,.7)); }
.rail-menu .rail-item:hover { background: color-mix(in srgb, var(--star) 12%, transparent); }
@media (prefers-reduced-motion: reduce) {
  .rail-menu .rail-item, .rail-menu .rail-sep { transition: opacity 1ms; translate: none; }
}

/* THE BUTTON. Same translucency as the panel, so opening does not change the
   material — only the size. */
/* ⛔ THE BUTTON IS A FLOATING GLYPH, NOT A CHIP. It carried a filled, bordered
   box and read as a control pasted onto the photograph. Legibility over an
   arbitrary ground now comes from a drop-shadow on the mark itself — a soft
   dark halo that follows the glyph's own shape — rather than from a rectangle
   drawn behind it. That is the difference between something floating on the
   image and something sitting on top of it. */
.rail-toggle {
  display: flex;
  align-items: center;
  gap: calc(.5rem * var(--chrome-scale));
  padding: calc(.35rem * var(--chrome-scale)) calc(.2rem * var(--chrome-scale));
  border: 0;
  background: none;
  border-radius: var(--r-interactive);
  color: color-mix(in srgb, var(--star) 92%, transparent);
  filter: drop-shadow(0 1px 3px rgba(0,0,0,.85)) drop-shadow(0 0 12px rgba(0,0,0,.6));
  font: inherit;
  cursor: pointer;
  transition: color var(--t-hover) var(--e-out);
}
.rail-toggle:hover { color: var(--star); }
.rail-toggle:focus-visible { outline: var(--hair-strong) solid var(--waterline); outline-offset: 2px; }
/* The label is REAL TEXT and always in the DOM — it is the button's accessible
   name. Only its box collapses, never its existence. */
.rail-toggle-label {
  font-family: var(--f-data);
  font-size: calc(var(--m-tick) * var(--chrome-scale));
  letter-spacing: .04em;
  max-width: 0;
  opacity: 0;
  overflow: hidden;
  white-space: nowrap;
  transition: max-width var(--t-nav) var(--e-out), opacity var(--t-nav) var(--e-out);
}
.rail:hover .rail-toggle-label,
.rail:focus-within .rail-toggle-label,
/* The expanded width scales too, or the larger type is simply clipped by a
   box that did not grow with it. */
.rail.is-open .rail-toggle-label { max-width: calc(8rem * var(--chrome-scale)); opacity: 1; }
/* One glyph or the other, never both.
   🪤 These were written as bare `.rail-ico-shut` and LOST to `.rail-ico
   { display: block }` further down the file — equal specificity, later wins,
   so the button rendered the hamburger and the cross side by side. Scoped to
   .rail so the intent beats the base rule regardless of source order. */
.rail .rail-ico-shut { display: none; }
.rail.is-open .rail-ico-open { display: none; }
.rail.is-open .rail-ico-shut { display: block; }

.rail-item {
  display: flex;
  align-items: center;
  gap: calc(.55rem * var(--chrome-scale));
  padding: calc(.5rem * var(--chrome-scale)) calc(.5rem * var(--chrome-scale));
  border-radius: calc(var(--r-interactive) - 4px);
  color: color-mix(in srgb, var(--star) 82%, transparent);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--t-hover) var(--e-out), color var(--t-hover) var(--e-out);
}
.rail-item:hover { background: color-mix(in srgb, var(--star) 9%, transparent); color: var(--star); }
.rail-item:focus-visible { outline: var(--hair-strong) solid var(--waterline); outline-offset: 2px; }

.rail-ico {
  flex: none;
  width: calc(20px * var(--chrome-scale));
  height: calc(20px * var(--chrome-scale));
  display: block;
}
.rail-ico * { vector-effect: non-scaling-stroke; }

/* THE LABEL. Present in the DOM at all times; only its BOX collapses.
   max-width + opacity keeps it in the accessibility tree — display:none would
   not, and that is the difference between a named link and a nameless one. */
.rail-label {
  font-family: var(--f-data);
  font-size: calc(var(--m-tick) * var(--chrome-scale));
  letter-spacing: .02em;
  max-width: 0;
  opacity: 0;
  overflow: hidden;
  transition: max-width var(--t-nav) var(--e-out), opacity var(--t-nav) var(--e-out);
}
/* Inside the menu the labels are always shown: the menu only exists when it is
   open, so collapsing them again would be a second disclosure inside the first. */
.rail-menu .rail-label { max-width: calc(12rem * var(--chrome-scale)); opacity: 1; }

/* The current page is marked by the ACCENT ON THE GLYPH plus aria-current, not
   by colour alone — and never by the semantic accents, which mean statute,
   policy, student data, flow and refusal everywhere else on this site. */
.rail-item[aria-current="page"] { color: var(--star); }
.rail-item[aria-current="page"] .rail-ico { color: var(--waterline); }

/* A separator before the terminal action, so "enter the app" is not read as the
   tenth destination in a list of nine. */
.rail-sep { height: 0; border: 0; border-top: var(--hair) solid
            color-mix(in srgb, var(--star) 14%, transparent);
            margin: calc(.35rem * var(--chrome-scale)) calc(.3rem * var(--chrome-scale)); }
.rail-item-act { color: var(--star); }
.rail-item-act .rail-ico { color: var(--phosphor); }

/* ── THE MARK ─────────────────────────────────────────────────────────────
   Small, top-left, floating. The page must still say what it is without a bar
   across it. This is the element that persists across page transitions. */
/* Bottom-left, diagonally opposite the menu control. The two persistent chrome
   elements now sit in opposite corners and the whole middle of every
   composition is clear — which on the constellation means clear of stars. */
.mark {
  position: fixed;
  bottom: clamp(.9rem, 2.2vh, 1.8rem);
  left: clamp(1rem, 2.4vw, 2.2rem);
  z-index: 90;
  font-family: var(--f-data);
  font-size: calc(.92rem * var(--chrome-scale));
  letter-spacing: .04em;
  color: var(--star);
  text-decoration: none;
  /* Floating, like the menu control opposite it — no chip, no border. Both are
     FIXED and travel over every section, so legibility comes from a shadow that
     follows the letterforms rather than from a box drawn behind them. */
  padding: calc(.35rem * var(--chrome-scale)) calc(.2rem * var(--chrome-scale));
  filter: drop-shadow(0 1px 3px rgba(0,0,0,.85)) drop-shadow(0 0 12px rgba(0,0,0,.6));
}
.mark:focus-visible { outline: var(--hair-strong) solid var(--waterline); outline-offset: 3px; }

/* ── NARROW ───────────────────────────────────────────────────────────────
   A vertical rail of ten items does not fit a phone. It becomes a horizontal
   bar along the bottom edge — thumb-reachable, and it stops competing with the
   composition. It scrolls horizontally rather than wrapping, because a wrapping
   nav bar changes height and shifts the page under the reader. */
/* ── NARROW ───────────────────────────────────────────────────────────────
   The corner disclosure is already the right shape for a phone, so it stays a
   corner disclosure — it does NOT become a full-width bottom bar. That bar was
   what covered the constellation in portrait with nowhere to scroll to, and a
   bar that spans the viewport is the widest possible way to occlude an image.
   The open menu is capped in height and scrolls internally if it has to. */
@media (max-width: 700px) {
  .rail { bottom: max(.9rem, env(safe-area-inset-bottom)); }
  .rail-menu { max-height: 68svh; overflow-y: auto; }
  /* The phone's larger tap padding scales with everything else — left fixed
     it would shrink RELATIVE to the glyph and quietly undo the touch target. */
  .rail-toggle { padding: calc(.7rem * var(--chrome-scale)) calc(.8rem * var(--chrome-scale)); }
}

/* Reduced motion: the rail still expands, it just does not animate doing it.
   Removing the expansion entirely would take the labels away from exactly the
   readers most likely to need them. */
@media (prefers-reduced-motion: reduce) {
  .rail-label, .rail-item { transition: none; }
}
