/*
 * rail.css — GENERATED FILE, DO NOT EDIT.
 * 
 * Compiled from core/admin/skin/ by control-plane/scripts/generate-skin.js
 * so the operator console runs the SAME skin engine as every tenant admin
 * rather than a second copy of it. Edit core, then regenerate:
 * 
 *   node control-plane/scripts/generate-skin.js
 * 
 * Drift guard: control-plane/functions/test/skin-parity.test.js
 */
/* =============================================================
   WarHorse admin — the expanding rail
   -------------------------------------------------------------
   64px at rest, 194px open. It *pushes*: the rail is flex-shrink:0 and your
   main column is flex:1, so content compresses rather than being overlapped.
   No JavaScript.

   REQUIRES tokens.css. The rail's glass (background, blur, border, resting
   shadow) comes from .gl-rail in glass.css — this file is geometry and
   motion only, and deliberately restates none of the recipe.

   ------------------------------------------------------------- MARKUP
     <aside data-rail class="gl-rail">
       <div data-rail-head>…logo… <span data-rail-label>TENANT</span></div>
       <nav>
         <button data-rail-item aria-current="page">
           <span class="rail__icon"><svg width="15" height="15" …/></span>
           <span data-rail-label>Posts &amp; News</span>
         </button>
         …
         <div data-rail-sep></div>
         …
       </nav>
       <div data-rail-foot>…avatar… <span data-rail-label>…</span></div>
     </aside>

   THE ICON BOX IS LOAD-BEARING. `.rail__icon` is a fixed 24px flex-centred
   box and rows stay left-aligned at all times. The obvious approach —
   centring the row while collapsed — breaks it: the label is faded, not
   removed, so it still occupies width, the row centres around icon + label,
   and the icon is pushed past the clipped left edge. The collapsed rail then
   renders BLANK, which looks like a CSS load failure rather than a layout
   bug, so it costs an hour to find. The box alone does the centring: 24px
   inside 64px minus 10px of padding each side is dead centre, at both widths.

   ITEMS MUST BE FOCUSABLE. <button> or <a>, or a <div> with tabindex="0".
   The whole keyboard story here is :focus-within, and :focus-within can
   never fire inside a rail made of plain divs — you would tab straight into
   invisible labels, which is the exact failure the contract calls out.

   ACTIVE ITEM: any of aria-current, .is-active, or [data-active]. Prefer
   aria-current="page" — it is the one that also tells a screen reader.

   ------------------------------------------------------------- HOW THE
                                                                 STATE WORKS
   One inherited custom property, --open (0 or 1), drives every animated
   value through calc(). Three selectors set it to 1; nothing else in this
   file repeats a width, a radius or a transform.

   --open itself is unregistered, so it flips discretely — but the values
   derived from it are width / opacity / transform / filter, which are
   ordinary animatable properties, so the transitions below still run.

   The alternative was three copies of every rule (hover, focus-within,
   pinned), which is where a rail gets subtly different in one state and
   nobody notices for a month.
   ============================================================= */

/* :root here is not decoration. .gl-rail in glass.css sets a static
   border-radius from the same token, at the same specificity (0,2,0), and
   which of the two won would then depend on the order the page happened to
   link the stylesheets in. The :root prefix buys this rule 0,3,0 and settles
   it: rail.css owns the rail's animated radius, always. */
:root[data-skin='glass'] [data-rail] {
  --open: 0;

  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  padding: 14px 10px;
  /* Clips the labels while collapsed. This does NOT crop the rail's own
     box-shadow — overflow clips descendants, not the element's own shadow.
     The overflow:hidden warning in the contract is about ANCESTORS. */
  overflow: hidden;

  width: calc(var(--rail-w) + (var(--rail-w-open) - var(--rail-w)) * var(--open));
  border-radius: calc(var(--r-rail) + (var(--r-rail-open) - var(--r-rail)) * var(--open));

  /* The spring. 520ms with an overshoot that settles — slower than a normal
     UI transition on purpose. A 260ms ease-out was tried first and read
     mechanical. The radius is on the same curve so the panel appears to
     inflate rather than to extend; that pairing is the second of the three
     details that make this liquid. */
  transition:
    width var(--dur-rail) var(--ease-spring),
    border-radius var(--dur-rail) var(--ease-spring),
    box-shadow 0.4s ease;
}

/* ---------------------------------------------------------------- open */

/* Pointer devices. Gated so that under (hover: none) this rule does not
   exist at all — no sticky-hover rail that expands on first tap and stays
   expanded until you tap something else. */
@media (hover: hover) {
  [data-skin='glass'] [data-rail]:hover {
    --open: 1;
    box-shadow: var(--shadow-rail-open), var(--glass-inset);
  }
}

/* Keyboard, and the explicit pin. Available on every device, including
   touch — this is the rail's only expansion path there, driven by focus
   when an item is tapped, or by a page setting data-open itself.
   data-open="false" reads as closed, so a pin control can toggle a string. */
[data-skin='glass'] [data-rail]:focus-within,
[data-skin='glass'] [data-rail][data-open]:not([data-open='false']) {
  --open: 1;
  box-shadow: var(--shadow-rail-open), var(--glass-inset);
}

/* ---------------------------------------------------------------- head / foot */
[data-skin='glass'] [data-rail] [data-rail-head] {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 2px 9px 14px;
  overflow: hidden;
}

/* The head's brand mark. A fixed SQUARE that no tenant stylesheet can
   deform: every tenant's public site.css ships an `img { max-width: 100% }`
   reset, the admin links /site.css, and in the collapsed 64px rail the
   head's content box is narrower than the mark — without these overrides
   the reset squeezes the logo's width while its height stays put, and the
   brand renders as a squished sliver. Apps may restate the size (the CMS
   grows its mark with --open); the square-and-unsquashable part is the
   engine's guarantee. */
[data-skin='glass'] [data-rail] [data-rail-head] img {
  width: 24px;
  height: 24px;
  max-width: none;
  max-height: none;
  flex-shrink: 0;
  object-fit: contain;
}

[data-skin='glass'] [data-rail] nav {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  /* A 15-item nav in a short window would otherwise be clipped with no way
     to reach the bottom items. Scrollbar hidden — a gutter inside a 64px
     rail is worse than the scroll being implicit. */
  overflow-y: auto;
  scrollbar-width: none;
}
[data-skin='glass'] [data-rail] nav::-webkit-scrollbar { width: 0; height: 0; }

[data-skin='glass'] [data-rail] [data-rail-foot] {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 9px 0;
  margin-top: 8px;
  border-top: 1px solid var(--hairline);
  overflow: hidden;
}

[data-skin='glass'] [data-rail] [data-rail-sep] {
  margin: 8px 9px;
  height: 1px;
  flex-shrink: 0;
  background: var(--hairline);
}

/* ---------------------------------------------------------------- items */
[data-skin='glass'] [data-rail] [data-rail-item] {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
  /* Rows are left-aligned at BOTH widths. See the header. */
  padding: 8px 9px;
  padding-left: calc(9px + 2px * var(--open));
  border-radius: 10px;
  overflow: hidden;

  /* NOT decoration. width:100% + 9px side padding only fits the rail if the
     padding is INSIDE the width. The CMS happens to inherit border-box from
     the tenant reset, but WarChest and WarHorn build their rows with
     `button.nv { all:unset }`, and all:unset reverts box-sizing to
     content-box at a specificity their own `* { border-box }` reset cannot
     beat. Their item boxes then measure 100% + 18px — wider than the rail's
     content box at BOTH widths — and the active/hover pill is hard-cropped
     flat against the rail's right edge (owner report, 2026-08-27; measured
     live: 60px pill in a 42px slot collapsed, 190px in 172px open). Declared
     here, on the same rule that declares the width, so the geometry cannot
     depend on which app is hosting the rail. */
  box-sizing: border-box;

  /* Reset, so a <button> works as a rail item without the page restyling it. */
  width: 100%;
  border: 0;
  background: transparent;
  text-align: left;
  font: inherit;
  font-family: var(--font-chrome);
  font-size: var(--t-body);
  color: var(--mid);
  cursor: pointer;
  text-decoration: none;

  transition:
    background 0.2s ease,
    color 0.2s ease,
    padding-left var(--dur-rail) var(--ease-spring);
}

[data-skin='glass'] [data-rail] [data-rail-item]:hover { background: var(--nav-item-hover); }

/* The raised white pill IS the active signal — no coloured edge marker.
   One active item at a time; drive it from the existing data-view switching. */
[data-skin='glass'] [data-rail] [data-rail-item][aria-current]:not([aria-current='false']),
[data-skin='glass'] [data-rail] [data-rail-item].is-active,
[data-skin='glass'] [data-rail] [data-rail-item][data-active]:not([data-active='false']) {
  background: var(--nav-active);
  box-shadow: var(--shadow-nav-active);
  font-weight: 700;
  color: var(--ink);
}

/* THE FIXED-WIDTH ICON BOX. Do not make this width auto, do not let it
   shrink, and do not centre the row instead. */
[data-skin='glass'] [data-rail] .rail__icon {
  width: 24px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
[data-skin='glass'] [data-rail] .rail__icon svg { display: block; }

/* ---------------------------------------------------------------- labels */

/* The third detail, and the one that sells it: labels surface OUT OF BLUR.
   Each starts blur(2px) scale(.94) and sharpens as it slides in. Text
   emerging *through* glass, not sliding in from off-screen. A plain opacity
   fade is not a substitute — it was tried, and it reads as a drawer.

   Labels are always in the DOM. They fade; they are never inserted or
   removed. Nothing reflows and a screen reader always has the text. */
[data-skin='glass'] [data-rail] [data-rail-label] {
  white-space: nowrap;
  transform-origin: left center;
  opacity: var(--open);
  transform: translateX(calc(-10px * (1 - var(--open)))) scale(calc(0.94 + 0.06 * var(--open)));
  filter: blur(calc(2px * (1 - var(--open))));
  transition:
    opacity 0.22s ease,
    transform 0.46s var(--ease-spring),
    filter 0.28s ease;
}

/* The stagger: 20ms + n*16ms down the list, so the labels arrive as a wave
   rather than as a block. Only on the way OPEN — closing is immediate,
   because a staggered close reads as lag.

   :is() carries :hover here even though the state rules above gate it to
   (hover: hover). A transition-delay with nothing transitioning is inert, so
   the redundancy costs nothing and keeps this to 16 rules instead of 48. */
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(1) [data-rail-label] { transition-delay: 36ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(2) [data-rail-label] { transition-delay: 52ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(3) [data-rail-label] { transition-delay: 68ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(4) [data-rail-label] { transition-delay: 84ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(5) [data-rail-label] { transition-delay: 100ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(6) [data-rail-label] { transition-delay: 116ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(7) [data-rail-label] { transition-delay: 132ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(8) [data-rail-label] { transition-delay: 148ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(9) [data-rail-label] { transition-delay: 164ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(10) [data-rail-label] { transition-delay: 180ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(11) [data-rail-label] { transition-delay: 196ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(12) [data-rail-label] { transition-delay: 212ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(13) [data-rail-label] { transition-delay: 228ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(14) [data-rail-label] { transition-delay: 244ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(15) [data-rail-label] { transition-delay: 260ms; }
[data-skin='glass'] :is([data-rail]:hover, [data-rail]:focus-within, [data-rail][data-open]:not([data-open='false'])) nav > *:nth-child(16) [data-rail-label] { transition-delay: 276ms; }

/* ---------------------------------------------------------------- focus */

/* Glass has weak edge definition; keyboard users need the ring here MORE
   than on a flat UI, not less. Inset because the rail clips its children —
   a 2px offset ring on a full-width item would be cropped by the rail edge. */
[data-skin='glass'] [data-rail] [data-rail-item]:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: -2px;
}

/* ---------------------------------------------------------------- motion off */
@media (prefers-reduced-motion: reduce) {
  [data-skin='glass'] [data-rail],
  [data-skin='glass'] [data-rail] [data-rail-item],
  [data-skin='glass'] [data-rail] [data-rail-label] {
    transition: none;
  }
  /* Sharpen immediately too — a permanently blurred label is worse than an
     unanimated one, and blur() is not motion, it is just illegible. */
  [data-skin='glass'] [data-rail] [data-rail-label] { filter: none; }
}
