/* The rail: the site's one persistent navigation, and the thing that says which
 * desk you are standing at.
 *
 * It is a 60px lane of icons and nothing else at rest, on the operator's ruling
 * of 2026-08-17: "very small on the site so they can easily navigate but not
 * make it look ugly". A labelled 216px column was the first build and it is the
 * wrong shape - it takes a sixth of a laptop and a whole thumb's width of a
 * phone to repeat five words that never change. The words are still here; they
 * open BESIDE the lane, over the page, when an item is pointed at or focused,
 * so nothing on the page ever moves and no name is a guess.
 *
 * Inside those 60px it is a CAPSULE and not a column, since 2026-08-23. The
 * strip it replaces ran the full height of the viewport, square at both ends,
 * with a hairline down its right side and nothing else: a panel with a blur on
 * it, against the operator's ruling of 2026-08-18 that this reads as glass. The
 * lane is the same width it always was, so nothing on any page moves; what
 * changed is that the rail is now an object sitting in the lane rather than the
 * lane itself. `nav.rail` carries the argument for each layer.
 *
 * Its own sheet for doc.css's reason and not a new one. pages.css sits close to
 * the 750 ceiling in `docs/constants.toml`, so a rule appended there breaks it,
 * and this is a component owned by one element the way that one is owned by one
 * page. This said app.css was at 720 lines and pages.css at 745 until
 * 2026-08-26, when they measured 569 and 737. A stale count here is what files
 * a SHARED rule in a page sheet: `.fnl-scroll` belongs beside `.fnl` in app.css
 * and this line argued for putting it anywhere else. Measure before believing.
 *
 * Its motion is here too, and it costs nothing to keep it out of motion.css:
 * every keyframe below is `from` only with `backwards` and no `forwards`, which
 * is the shape `head-in`, `notice-in` and `live-pulse` in that sheet already
 * use. The resting state is the element's own, so motion.css's closing
 * `animation: none !important` under `prefers-reduced-motion` lands the whole
 * rail fully drawn - no stagger, no transform, no delay - instead of stranding
 * anything invisible or at zero height. That is why this sheet is linked BEFORE
 * motion.css: that block has to stay last.
 *
 * `nav.rail` and not `.rail`, everywhere, on purpose. `templates/art/rail.html`
 * is the retired hero art and it is `.rail` too - deliberately still on disk as
 * the canary in `tests/lint/grid_cells.rs`. Nothing includes it today, so a bare
 * `.rail` here would match nothing; the day somebody puts it back it would turn
 * that drawing into a second fixed sidebar. `responsive.rs` already wrote down
 * that reusing this name is the hazard. One extra token closes it. */

:root {
  /* The LANE the rail is allowed: its width standing up the side, its height
     lying along the foot of a phone. One property because it is one
     measurement, and because the page is moved off the rail by a padding that
     reads it - the two can never disagree about how much room the thing takes.
     60px, above the 56 an icon and its target need and under the 64 past which
     this stops reading as a strip and starts reading as a column. It is the
     lane and not the capsule, which takes 52 of it and leaves the rest as
     margin either side; that is what stops the thing touching the content
     column, and it is why reshaping the rail moved nothing on any page. */
  --rail-w: 60px;
}

/* BOTH sides, since 2026-08-26. This was `padding-left` alone, which reserves
   the rail's lane out of the left only, so `.wrap`'s `margin-inline: auto`
   centred the page inside what was LEFT of the viewport rather than inside the
   viewport. Measured on /gifting at 1295: the content column ran x=80 to 1275,
   which is 80px of margin on one side and 20 on the other, and the operator
   read it as exactly what it is - "have SO much space because its not centered
   properly". Padding the right by the same lane makes it 80 and 80.
   It costs width only where the 1320px cap is not already binding: at 1920 the
   wrap is 1320 before and after and merely moves 30px left, and at 1440 it goes
   1320 to 1280. Cheaper than the alternative, which is centring on the viewport
   and letting the capsule sit over the first few pixels of the column. */
body { padding-inline: var(--rail-w); }

/* The skip link is the one thing on the site already pinned to the viewport's
   left edge - `position: fixed; left: var(--step-4)` in pages.css, at z-index 40
   - so it does not move with the page and would open on top of the rail. It is
   the FIRST thing a keyboard reaches on every page. Same offset, past the rail
   instead of over it; this sheet is linked after pages.css, so equal specificity
   is enough and the rule stays with the thing that caused it. */
.skip:focus { left: calc(var(--rail-w) + var(--step-4)); }

/* A CAPSULE, not a column. Fixed to the left margin and centred on the
   viewport, hugging exactly as many items as the reader is entitled to see.
   The full-height strip it replaces was a panel with a blur on it - 60 of 889
   pixels tall, square at both ends, and separated from the page by a hairline
   that ran the whole way down. Measured 2026-08-23 before the change:
   `borderRadius: 0px`, `boxShadow: none`, `backgroundColor: rgba(0,0,0,0)`.
   A shape with no fill, no shadow and no corner is not glass; it is a ruled
   margin, and the operator's ruling of 2026-08-18 was that the rail is to read
   as glass and not as a panel.

   The footprint does not change: `--rail-w` is still 60px, so `body`'s padding
   and `.skip:focus`'s offset are untouched and the content column starts at the
   same x it always did. The capsule takes 52 of those 60 and leaves the rest as
   margin, which is what makes it read as an object sitting BESIDE the page
   rather than a strip cut out of it.

   Height is content and never the viewport: five items for a customer, seven
   for an owner. Nothing has to know which - a capsule that hugs its list cannot
   be lopsided when a row is withheld, the same argument `art/rail.html` makes
   for a grid over a hand placed fan.

   `translate` and not a `transform` for the centring, the answer `.rail-lab`
   and `.tile` already give: the property composes instead of replacing, so a
   transform anywhere below cannot fight it. */
nav.rail {
  position: fixed; top: 50%; translate: 0 -50%; left: 5px; z-index: 3;
  display: flex; flex-direction: column;
  padding: var(--step-1) 5px;
  border-radius: var(--r-pill);
  /* GLASS IS LAYERS, and the blur was only ever one of them. A gradient so the
     shape has a lit top and a shaded foot, the blur so what is behind it reads
     as colour through glass rather than as grey, one outer shadow to lift it
     off the page, and inset hairlines for the lit and shaded edges. With the
     blur alone the rail had no depth at all: nothing said where its surface
     was, so it read as a hole in the page rather than a thing on it.
     The gradient is a token and its two stops are argued there, in tokens.css,
     which is the only sheet allowed to hold a hue - `lint/css_tokens.rs` is
     what says so, and it caught this rule written out in full. */
  background: var(--glass-rail);
  border: 1px solid var(--border);
  /* The blur `.quote` already uses in pages.css. Matched rather than picked:
     two frosted surfaces at two radii read as two materials. */
  -webkit-backdrop-filter: blur(20px) saturate(150%);
          backdrop-filter: blur(20px) saturate(150%);
  /* `--shadow-lift` is the token for exactly this: a card that has left the page
     and therefore has somewhere for its shadow to fall. Four of the five layers
     come free with it - the contact shadow, the wide soft lift, the lit top
     hairline, and a glow in `--accent-rgb`, which resolves PER PAGE because the
     service classes rebind that property. So the capsule carries a faint blush
     of whichever desk you are standing at, on every page, without a rule here
     naming a single hue. The shaded foot is the one layer the token has no
     reason to carry and this shape does, so it is appended rather than the whole
     stack being written out again. */
  box-shadow: var(--shadow-lift), inset 0 -1px 0 rgba(0, 0, 0, .5);
}

/* No scroller here, deliberately, and it is the label that decides it:
   `overflow-y: auto` computes `overflow-x` to `auto` as well, which would clip
   every name off at the capsule's edge - the one thing this shape depends on,
   and now the lit tab as well, which hangs outside the box on the other side.
   Seven items at 44px plus the capsule's own 10 is 318px, so the list only
   reaches the ends of a viewport under about 320px tall, which is a phone held
   sideways with the browser chrome up - and seven items is an owner, who is one
   person. It overflows visibly there rather than becoming unreachable. */
nav.rail ul { list-style: none; margin: 0; padding: 0; }

.rail-i {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 40px; min-height: 44px;
  border-radius: var(--r-pill);
  /* `--text-2` and not `--text-3`, since 2026-08-23, and the capsule is the
     reason. A translucent fill raises the local background under the icon, so
     the SAME ink loses contrast the moment the glass arrives: `--text-3` reads
     6.24:1 on the bare page ramp and about 5.4:1 on the composite above, which
     is under the floor and under what it had. Moving one step up the ramp buys
     it back with headroom and costs nothing else, because the current item is
     told by four things and only one of them is ink. */
  color: var(--text-2); text-decoration: none;
  /* Hover LIFTS, current is pressed IN, and they are opposite on purpose - see
     the `[aria-current]` rule. Still nothing that moves: the rail is beside
     every page all of the time, and a thing that answers the pointer with a
     slide or a scale stops being navigation and starts being the subject. */
  transition: color 120ms ease-out, background 120ms ease-out, box-shadow 120ms ease-out;
  animation: rail-in 128ms cubic-bezier(.16,.84,.44,1) backwards;
}
.rail-i:hover, .rail-i:focus-visible {
  color: var(--text-1);
  background: rgba(255, 255, 255, .06);
  box-shadow: var(--shadow-raised);
}
/* A ring, not just a colour. `:focus-visible` shared the hover rule alone until
   2026-08-23, and a keyboard caller arriving on an icon-only item was told so by
   a shift from `--text-2` to `--text-1` - a change of ink on a 40px target, which
   is the one signal somebody with low vision or a colour deficiency is least
   likely to catch. `--accent` resolves per service here as everywhere else. */
.rail-i:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Where you are, told THREE ways, because one of them is a colour and a colour
   is the signal a reader may not have.

   1. Ink, as `.site-foot nav a[aria-current]` already answers it in app.css:
      the page's own text colour and a little weight.
   2. DEPTH, and in the opposite direction to hover. Hover raises an item out of
      the glass with `--shadow-raised`; the current one is pushed INTO it with
      `--shadow-inset`. Two states that differ by which way the light falls are
      answerable at a glance and without hue - it is the difference between a
      key you could press and the one already held down.
   3. The tab below, which is the loud one.

   The fill is the accent at .12 through `--accent-rgb`, so it is the desk's own
   hue on every page and no rule here names one. */
.rail-i[aria-current="page"] {
  color: var(--text-1); font-weight: 500;
  background: rgb(var(--accent-rgb) / .12);
  box-shadow: var(--shadow-inset);
}

/* The lit tab: a short bar of the capsule's own left edge, in the service hue,
   at exactly the current item's height. `left: -6px` is the capsule's padding
   plus its border, so the tab lands ON that edge rather than beside it - the
   hairline is lit over that stretch instead of gaining a second line, which is
   the argument the 1px spine this replaces made for `right: -1px`.
   It is 3px and it glows, where the spine was 1px and did not. That spine was
   the second signal on paper and almost nothing on a screen: one device pixel
   at the far edge of a 60px strip, which is not a mark somebody finds without
   being told it is there.
   Rounded on the outer side only, so it reads as light escaping the capsule
   rather than as a separate pill floating next to it. */
.rail-i[aria-current="page"]::after {
  content: ""; position: absolute; left: -6px; top: 50%;
  width: 3px; height: 18px; margin-top: -9px;
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: var(--accent);
  box-shadow: 0 0 10px rgb(var(--accent-rgb) / .75);
  animation: spine-in 320ms 180ms cubic-bezier(.16,.84,.44,1) backwards;
}

/* The name, and the reason the strip can be this narrow. Absolutely positioned
   out of flow and OVER the page, so opening one moves nothing: the content
   column never reflows and there is nothing for the document to scroll
   sideways to. It is a real word from the footer's own vocabulary on the
   house's own glass, not a tooltip drawn by the browser.
   Hover AND `:focus-visible`, because a keyboard never hovers, and this is the
   only place the name exists once the strip is icon-only.
   `opacity` and not `visibility` or `display`: the name has to stay in the
   accessibility tree at rest, since it is the anchor's accessible name.
   `translate` and not `transform`, the answer `.tile` already gives in
   motion.css: the property composes instead of replacing, so the slide cannot
   fight the centring. */
.rail-lab {
  position: absolute; left: 100%; top: 50%;
  margin-left: var(--step-2);
  padding: var(--step-1) var(--step-3);
  /* Opaque, and it takes no glass at all. A 28px pill
     is too small for a 20px blur to read as a material, and this one opens over
     BODY TEXT rather than over the page background - `.quote`'s translucency is
     right for a 400px card on a gradient and wrong for a word laid on a
     sentence. `--surface-2` is the raised surface already in the palette; the
     stronger seam is what separates it from whatever it lands on. */
  border: 1px solid var(--border-str); border-radius: var(--r-md);
  background: var(--surface-2);
  box-shadow: var(--shadow-raised);
  color: var(--text-1); font-size: 14px; line-height: 1.4; white-space: nowrap;
  opacity: 0; pointer-events: none;
  translate: -4px -50%;
  transition: opacity 120ms ease-out, translate 120ms ease-out;
}
.rail-i:hover .rail-lab,
.rail-i:focus-visible .rail-lab { opacity: 1; translate: 0 -50%; }

/* Reading order, not all at once, and the same `:nth-child` stagger the bento
   and the steps use in motion.css - stated in the sheet rather than set from an
   index in the markup, because this page sends no nonce and an inline style is
   dropped with nothing to say it went.
   28ms apart against a 128ms tween puts the fifth and last public item down at
   exactly 240ms. The two owner items run past that, and only an owner sees
   them. */
@keyframes rail-in { from { opacity: 0; transform: translateX(-6px); } }
nav.rail li:nth-child(2) .rail-i { animation-delay: 28ms; }
nav.rail li:nth-child(3) .rail-i { animation-delay: 56ms; }
nav.rail li:nth-child(4) .rail-i { animation-delay: 84ms; }
nav.rail li:nth-child(5) .rail-i { animation-delay: 112ms; }
nav.rail li:nth-child(6) .rail-i { animation-delay: 140ms; }
nav.rail li:nth-child(7) .rail-i { animation-delay: 168ms; }

/* The tab opens from its own centre, in both directions, rather than dropping
   from one end. `scaleY` alone, which takes the default `50% 50%` origin, so
   there is no `transform-origin` to be undone.
   180ms of delay is what puts it after the items instead of underneath them.
   `spine-in-x` is the same figure turned, for the bar at the foot of a phone -
   the answer `.steps::before` already gives itself at its own breakpoint.
   The names are unchanged from when this was a 1px spine: renaming a keyframe
   is a rename in two sheets and a diff nobody can read against the incident. */
@keyframes spine-in { from { transform: scaleY(0); } }
@keyframes spine-in-x { from { transform: scaleX(0); } }

/* On a phone the capsule lies down along the foot instead of standing up the
   side. 60px of a 390px screen is a sixth of the width and the operator's ruling
   was that it must not eat the viewport; the same 60px along the bottom costs
   nothing horizontal, and a bar at the foot is where a thumb already is.
   It stays a capsule when it turns: inset from all three edges and still
   `--r-pill`, so it is the same object seen lying down rather than a second
   piece of chrome that only phones get. Everything below is what turning it
   costs - the axis, the alpha, and which edge the tab hangs off. */
@media (max-width: 640px) {
  /* The bar floats clear of the bottom edge now, so the page has to be padded
     off it by its height AND both margins, not just its height. Content that
     ends exactly at `--rail-w` would sit under the last 8px of glass. */
  body { padding-inline: 0; padding-bottom: calc(var(--rail-w) + var(--step-4)); }
  .skip:focus { left: var(--step-4); }
  nav.rail {
    top: auto; translate: none;
    inset-inline: var(--step-2); bottom: var(--step-2);
    height: var(--rail-w);
    flex-direction: row; padding: var(--step-1);
    /* The same material, same two stops, one alpha that answers a different
       backdrop - and both are written down together in tokens.css so neither
       can be moved without the other being read. The measurement that decides
       the number is there with them. */
    background: var(--glass-rail-bar);
  }
  /* Five equal columns, each allowed to shrink. A name makes an item wider
     than an icon did, and this bar has no scroller by design, so five
     natural-width items would run past a 320px viewport with the last one
     unreachable. `flex: 1` with `min-width: 0` turns the row into a grid that
     always fits and lets the longest name ellipsize instead. */
  nav.rail ul { display: flex; width: 100%; }
  /* On the LI and not on `.rail-i`: the list item is the flex CHILD here and
     the anchor is inside it, so `flex: 1` on the anchor distributes nothing --
     measured live, items came out 37-53px wide instead of equal. */
  nav.rail li { flex: 1; min-width: 0; display: flex; }
  .rail-i {
    flex: 1; min-width: 0; width: auto;
    padding-inline: var(--step-1);
    flex-direction: column; gap: var(--step-1);
  }
  /* The tab turns with the bar: a segment of its TOP edge, centred over the
     item. `top: -5px` is the bar's own padding plus its border, the same
     arithmetic `left: -6px` does standing up. Both ends stay inside the pill:
     the item's rounded corner sits well within the bar's, so nothing pokes out
     of the capsule at the first or last item. */
  .rail-i[aria-current="page"]::after {
    left: 50%; top: -5px; bottom: auto;
    width: 22px; height: 3px; margin: 0 0 0 -11px;
    border-radius: 0 0 var(--r-sm) var(--r-sm);
    animation-name: spine-in-x;
  }
  /* THE NAME IS ALWAYS ON HERE, and it stops being a tooltip.

     A tooltip is opened by a pointer, and this is the breakpoint where there
     is no pointer. `:hover` does not fire on a touch screen, and browsers
     deliberately do not match `:focus-visible` for a touch focus - so the one
     place the name exists was unreachable and the bar was five unnamed icons.
     You had to tap an item to find out what it was, and tapping navigates.
     (Screen readers were never the ones affected: `opacity: 0` keeps the text
     in the accessibility tree, which is why it is opacity and not `display`.)

     It sheds the pill along with the tooltip. `--surface-2`, a border and a
     shadow are what separate a word laid OVER body text from the sentence
     underneath it; in flow under its own icon there is nothing to separate it
     from, and five raised pills in a 60px bar would be exactly the panel the
     rail is not allowed to be. Plain text instead, and `color: inherit` so the
     name follows the item through `--text-2` at rest and `--text-1` at
     `[aria-current]` on the one rule that already decides that. The current
     item then reads as a single mark: tab, icon, name. */
  .rail-lab {
    position: static;
    margin: 0; padding: 0;
    border: 0; background: none; box-shadow: none;
    opacity: 1; translate: none; transition: none;
    color: inherit;
    font-size: 11px; line-height: 1;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  }
  /* The desktop reveal still matches here and would shift a static box by half
     its own height. Neutralised rather than deleted: it is correct above. */
  .rail-i:hover .rail-lab,
  .rail-i:focus-visible .rail-lab { translate: none; }
}
