/* Gump console — the application shell.
 *
 * STAGE 0 of the design-fidelity pass (2026-07-31). Every rule in the shell section below is
 * transplanted VERBATIM from the v5 design bundle (`project/screens/billing.html`, the most
 * complete shell frame — it is the only one that also draws `.shell-who`). Values were pulled
 * mechanically, not retyped, so they cannot drift from the source by transcription.
 *
 * The shell lives here rather than in the extracted per-page sheets for one hard reason: the
 * extractor emits it per screen, so `page-journey.css` and `page-pixel.css` — pages the design
 * never drew — carried NO shell at all, and `.shell-who` existed in only two of fourteen sheets.
 * A shell that is on every page has to be defined once, in a sheet every page loads.
 *
 * WHAT THE PRODUCT CHANGES, and why (the design is a review document, not a running app):
 *   - `.shell` is a bordered, fixed-height PREVIEW FRAME in the bundle (`--shell-preview-h`,
 *     820px here, eight different values across eleven files). The product is the viewport, so
 *     it takes the bundle's own `--shell-h: 100dvh` token and drops the frame border/radius.
 *     dvh over vh: on mobile Safari 100vh is the height with the URL bar hidden.
 *   - `container-type: inline-size` is KEPT, because the design's own pane-collapse is a
 *     container query and it stops working without it.
 *   - The rail's ITEM LIST is extrapolated. Every frame in the bundle draws a representative
 *     rail of three or four items (billing: Dashboard, Visitors, Billing; admin: Dashboard,
 *     Visitors, Content, Platform admin) — the design never draws all nine sections anywhere.
 *     The geometry, states and icon treatment are the design's; the nine items are ours.
 *   - The below-1024 behaviour is ours (see the note at the foot).
 */

/* The hidden attribute must always win. The UA default (display:none) loses to ANY author
   display rule - so `.banner { display:flex }` was rendering banners the pages had marked
   hidden (found by the 1.2 gate: an empty "Upgrade plan" banner showed permanently). This is
   the standard reset the extracted components.css does not carry. */
[hidden] { display: none !important; }

*, *::before, *::after { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--canvas);
  color: var(--ink-body);
  font-family: var(--font-sans);
  font-size: var(--fs-md);
  line-height: var(--lh-md);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ── shell ──────────────────────────────────────────────────────────────────── */
/* design: display:flex;flex-direction:column;height:var(--shell-preview-h);background:var(--canvas);
   border:1px solid var(--hairline);border-radius:var(--r-lg);overflow:hidden;container-type:inline-size */
.shell {
  display: flex; flex-direction: column;
  height: var(--shell-h, 100dvh);
  min-height: 0;
  background: var(--canvas);
  overflow: hidden;               /* the shell itself never scrolls — only .content does */
  container-type: inline-size;    /* the design's pane collapse is a @container query */

  /* Every property the review frame sets is RESET here, explicitly, rather than left undeclared.
     Twelve page sheets carry a second `.shell` rule that centres the frame in the review page —
     `width: min(100vw - 32px, 1280 + 56 + 2)`, `margin-left: calc(50% - …)`, plus the border and
     radius — and equal specificity means the page sheet wins whatever order it loads in for any
     property this rule does not mention. Undeclared is not neutral: it rendered the console as an
     820px-tall bordered card inset 51px from the left of a 1440px window. */
  width: 100%; margin: 0; border: 0; border-radius: 0;
}

.shell-head {
  height: var(--h-header); background: var(--surface);
  border-bottom: 1px solid var(--hairline);
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 var(--sp-5); gap: var(--sp-5); flex-shrink: 0;
}
/* The design's brandmark is a <div> — it is a review frame, so it has nowhere to go. Ours is the
   link home, which means it needs the underline off and the ink stated: the shared `a` rule paints
   every anchor accent-coloured and underlined, so the wordmark rendered as underlined link text. */
.shell-brandmark {
  display: flex; align-items: center; gap: var(--sp-2);
  text-decoration: none; color: var(--ink);
}
.shell-logo {
  width: 26px; height: 26px; border-radius: var(--r-md);
  background: var(--accent-action); color: var(--accent-on-fill);
  display: grid; place-items: center;
  font-weight: var(--fw-bold); font-size: var(--fs-md);
}
.shell-name { font-size: var(--fs-md); font-weight: var(--fw-semibold); letter-spacing: var(--tr-lg) }
.shell-who {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-xs); color: var(--ink-quiet);
}
.shell-main { flex: 1; display: flex; min-height: 0 }

/* ── rail (56px, icon-only, aria-labelled) ──────────────────────────────────── */
.rail {
  width: var(--w-rail); background: var(--surface);
  border-right: 1px solid var(--hairline);
  display: flex; flex-direction: column; align-items: center;
  padding: 10px 0; gap: 3px; flex-shrink: 0;
}
.rail-item {
  width: 36px; height: 34px; border-radius: var(--r-sm);
  display: grid; place-items: center;
  color: var(--ink-quiet); text-decoration: none;
}
.rail-item:hover { background: var(--neutral-bg); color: var(--ink) }
.rail-item[aria-current="page"] { background: var(--accent-tint); color: var(--accent-text-on-tint) }
.rail-spacer { margin-top: auto }
/* Ours: the icon inside a rail item. The design's items contain a bare 16px <svg>; giving it a
   class lets the sign-out BUTTON match the anchors without duplicating their geometry. */
.rail-icon { width: 16px; height: 16px; flex: 0 0 auto }
button.rail-item { background: none; border: 0; padding: 0; cursor: pointer; font: inherit }
button.rail-item:hover { background: var(--neutral-bg); color: var(--ink) }

/* ── the rail expands (ours; the design's rail is icon-only and fixed) ──────── */
/* An icon-only rail with nine destinations asks the operator to learn nine glyphs. The design
   never had to answer that — it draws three or four items per frame and is a document, not
   something anyone navigates twice a day. Two additions, both requested by the owner:
     • every item carries a `title`, so hovering names it (markup, not CSS);
     • a toggle expands the rail to show the labels, and the choice is remembered.
   Collapsed is the default and remains pixel-identical to the design's 56px rail, so nothing
   above this point changes for anyone who never touches the control. */
.rail-label {
  display: none;
  font-size: var(--fs-sm); font-weight: var(--fw-medium);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Two selectors for one state, deliberately. `html[data-rail="open"]` is set by the blocking
   script in <head>, so the rail is already the right width at first paint; `.shell.is-rail-open`
   is set by app.js and is what the toggle flips at runtime. Without the first there is a visible
   width jump on every navigation; without the second the control does nothing until reload. */
:root[data-rail="open"] .rail,
.shell.is-rail-open .rail {
  width: var(--w-rail-open, 196px);
  align-items: stretch;
  padding: 10px var(--sp-3);
}
:root[data-rail="open"] .rail-item,
.shell.is-rail-open .rail-item {
  width: auto; height: 34px;
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 0 10px;
}
:root[data-rail="open"] .rail-label,
.shell.is-rail-open .rail-label { display: block }
:root[data-rail="open"] .rail form,
.shell.is-rail-open .rail form { display: contents }

/* The toggle sits at the head's leading edge, over the rail column, so the control that changes
   the rail's width lines up with the thing it changes. */
.shell-headleft { display: flex; align-items: center; gap: var(--sp-2) }
.rail-toggle { flex: 0 0 auto }
.rail-toggle .rail-icon { transition: transform .12s ease }
:root[data-rail="open"] .rail-toggle .rail-icon,
.shell.is-rail-open .rail-toggle .rail-icon { transform: rotate(180deg) }
@media (prefers-reduced-motion: reduce) { .rail-toggle .rail-icon { transition: none } }

/* ── pane (204px secondary nav, per screen) ─────────────────────────────────── */
.pane {
  width: var(--w-pane); background: var(--surface);
  border-right: 1px solid var(--hairline);
  display: flex; flex-direction: column; flex-shrink: 0; overflow-y: auto;
}
.pane-title {
  padding: 14px 14px 8px;
  font-size: var(--fs-xs); letter-spacing: var(--tr-micro); text-transform: uppercase;
  color: var(--ink-quiet); font-weight: var(--fw-semibold); margin: 0;
}
.pane-list {
  list-style: none; margin: 0; padding: 0 var(--sp-3);
  display: flex; flex-direction: column; gap: 1px;
}
.pane-link {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  font-size: var(--fs-sm); font-weight: var(--fw-medium);
  border-radius: var(--r-sm); padding: 8px 10px;
  color: var(--ink-body); text-decoration: none;
}
.pane-link:hover { background: var(--neutral-bg); color: var(--ink) }
.pane-link[aria-current="page"] {
  background: var(--accent-tint); color: var(--accent-text-on-tint);
  font-weight: var(--fw-semibold);
}
.pane-count { font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--ink-quiet) }
/* On the CURRENT row the count sits on the accent tint, not on the surface, and the design's
   quiet grey measures 4.32:1 there - under the 4.5 AA threshold for 10px text. Its sibling
   label already switches to the on-tint ink for exactly this reason; the count was the one
   child that kept the surface colour after its background changed underneath it. Measured by
   contrast-gate on /content, the first screen to render a pane count at all. */
.pane-link[aria-current="page"] .pane-count { color: var(--accent-text-on-tint) }
/* A pane item that OPENS something rather than navigating is a <button>, and a button carries
   the UA's border, background and centred text. Same treatment `button.rail-item` already gets
   in the rail: the element differs, the affordance must not. Content's "Image library" is the
   first of these - the design marks it as a panel precisely because it is not a route. */
/* pane-note — design v5 (visitors.html), verbatim. The pane's footer sentence. Emitted by the
   extractor into page-visitors.css only, and the pane is shell furniture used by more than one
   screen, so it belongs beside the rest of the pane rules. */
.pane-note {
  margin-top: auto; padding: var(--sp-4) 14px;
  border-top: 1px solid var(--hairline-soft);
  font-size: var(--fs-xs); line-height: 1.55; color: var(--ink-quiet);
}
button.pane-link {
  background: none; border: 0; font: inherit; cursor: pointer;
  text-align: left; width: 100%;
}

/* ── content ────────────────────────────────────────────────────────────────── */
.content {
  flex: 1; overflow-y: auto; min-width: 0;
  overscroll-behavior: contain;
  scrollbar-gutter: stable;      /* ours: no 15px shift when content grows past one screen */
  /* Pane anchors glide instead of jumping. It goes HERE, on the scroll container, because
     .content is what scrolls — the window never does, so scroll-behavior on :root or html would
     animate nothing. Doing it in CSS rather than with a click handler is deliberate: the anchor
     stays a real anchor, so the browser keeps the URL hash, back/forward, and — the part a JS
     preventDefault silently costs — moving the sequential focus point to the target, which is
     what stops a keyboard user tabbing from a pane link and landing back at the top of the page. */
  scroll-behavior: smooth;
}
/* Landing flush against the container's top edge reads as a clipped section. --sp-5 is the value
   the design already uses for exactly this (billing's .regionhead carries scroll-margin-top:
   var(--sp-5)), so both panes agree rather than each inventing an offset. Every pane target on
   both screens is a .card with an id, which is what makes this one rule enough. */
.content .card[id] { scroll-margin-top: var(--sp-5); }

/* Smooth scrolling is motion, and motion is something people switch off for real reasons —
   vestibular disorders among them. The OS setting must win; the anchor still works, it just
   arrives instantly. */
@media (prefers-reduced-motion: reduce) {
  .content { scroll-behavior: auto; }
}
/* The content column is CAPPED and CENTRED. `--w-content-max` (1280px) is the design's own
   token and this is the standard responsive shape: fill the column up to the cap, then sit in
   the middle of whatever is left.

   Recorded because I got here twice. The cap's value is exactly the design's frame width (1338)
   minus its rail (56) minus its borders, so inside the bundle it can never bite and every screen
   there shows content filling edge to edge. I read that as "the design means fill", removed the
   cap, and at 1920 every table stretched the full width - which the owner correctly called out
   as worse than the dead space it replaced. The token is a cap; the frame is just narrow enough
   that the bundle never demonstrates it.

   `max-width` is STATED rather than inherited: twelve page sheets carry their own
   `.content-inner` and shell.css loads last, so a property this rule omits is whatever the page
   sheet says, not neutral. `width: 100%` keeps it filling below the cap. */
.content-inner {
  padding: 20px 24px 40px;
  display: flex; flex-direction: column; gap: var(--sp-4);
  max-width: var(--w-content-max);
  width: 100%;
  margin-inline: auto;
}` reads like a readability cap, but the
   number gives it away: 1280px is exactly the design's own frame width (1338) minus its rail
   (56) minus its two borders. Inside the bundle the cap can never bite, so what every screen
   there actually shows is content FILLING its column edge to edge. Reproducing the rule instead
   of the result put ~600px of dead canvas to the right of every table on a 1920px monitor
   (owner, 2026-07-30); centring it merely split the same dead space in two.
   The design does cap for readability - on the PROSE, not the container: `.screen-sub` is
   76ch, `.banner-text` wraps, `.clamp2` truncates. Those are untouched, so long-line
   readability is still handled where the design handles it. */
.content-inner {
  padding: 20px 24px 40px;
  display: flex; flex-direction: column; gap: var(--sp-4);
  /* `max-width: none` STATED, not omitted. Twelve page sheets carry the design's own
     `.content-inner { max-width: var(--w-content-max) }`, so deleting the declaration here left
     theirs in force and the column stayed pinned at 1280px - measured, x=196 w=1280 at a 1920
     viewport. Same trap as the `.shell` reset above: a property this rule does not mention is
     not neutral, it is whatever the page sheet says. */
  max-width: none;
}

/* Banners (quota, suppression, announcement) sit above the content and scroll with it —
   `div.app-banners` inside `.content-inner` is the design's own placement. */
.app-banners { display: flex; flex-direction: column; gap: var(--sp-3) }
.app-banners:empty { display: none }

/* ── the design's own pane collapse ─────────────────────────────────────────── */
/* Verbatim from the bundle, including the container query it is written against. Below the
   pane-collapse width the pane keeps its links but drops to rail width and hides its labels,
   titles and counts — so the secondary nav survives as icons instead of eating the content. */
@container (max-width: 1180px) {
  .pane { width: var(--w-rail); overflow: hidden }
  .pane-title, .pane-count { display: none }
  .pane-link { justify-content: center; padding: 8px 0 }
  .pane-link .pane-label { display: none }
  /* Ours: an expanded rail below the collapse width would take 196px from a content column the
     design has already decided is too narrow for a pane. The rail follows the pane down. */
:root[data-rail="open"] .rail,
  .shell.is-rail-open .rail { width: var(--w-rail); align-items: center; padding: 10px 0 }
  :root[data-rail="open"] .rail-item,
  .shell.is-rail-open .rail-item { width: 36px; display: grid; place-items: center; padding: 0 }
  :root[data-rail="open"] .rail-label,
  .shell.is-rail-open .rail-label { display: none }
}

/* ── skip link ──────────────────────────────────────────────────────────────── */
/* The design has no skip link. With a rail plus a pane before the content, a keyboard or
   screen-reader user otherwise traverses both on every page - and in a multi-page app that is
   every single navigation, not once per session as it was in the SPA. */
.skip-link {
  position: absolute; left: var(--sp-4); top: -100px; z-index: 100;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--hairline); border-radius: var(--r-md);
  box-shadow: var(--shadow-raised);
}
.skip-link:focus { top: var(--sp-4); }

/* ── below the supported width (module 5.1, Decision 5) ─────────────────────── */
/* KEPT as ours rather than adopting the bundle's version, and the reason is concrete: the
   design hides the content at `@container (max-width:1000px)` while also declaring
   `.shell { min-width: var(--w-min-supported) }` (1024px). Together those mean the shell
   refuses to shrink and the PAGE scrolls sideways instead — which is the one thing
   breakpoint-gate exists to prevent. A fixed panel at the same 1024px floor says the same
   thing to the user without shearing the layout. The AUTH documents are exempt and responsive
   to 360px (auth.css); they never carry this shell. */
.too-narrow { display: none; }
@media (max-width: 1023px) {
  .too-narrow {
    display: grid; place-items: center; text-align: center;
    position: fixed; inset: 0; z-index: 200;
    background: var(--canvas); color: var(--ink-body);
    padding: var(--sp-6);
  }
  .too-narrow-inner { max-width: 36ch; display: flex; flex-direction: column; gap: var(--sp-3); }
  .too-narrow-title { font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--ink); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}
