/* ULTRABENCH — base: tokens, reset, shell layout. No external fonts, no CDN. */

:root {
  --bg:            #0b0d11;
  --bg-raised:     #11141a;
  --bg-inset:      #0e1116;
  --line:          #1e232c;
  --line-strong:   #2b323e;
  --text:          #e6e9ef;
  --text-dim:      #98a1b0;
  --text-faint:    #7d8798;
  /* The leaderboard table keeps its original muted tone: its design is frozen, and at
     3.9:1 on --bg-raised it is a known sub-AA exception (UB-246). */
  --text-faint-board: #6b7484;
  --accent:        #54d6b0;
  --accent-dim:    #1d3a34;
  --warn:          #e0b23c;
  --danger:        #e8646b;
  --info:          #6aa9f0;
  /* Tints that sit on or beside the accent and status hues. */
  --on-accent:        #06231c;   /* text on a solid --accent fill */
  --accent-soft:      #a9d9c9;   /* the second trust tier, vendor-reported evidence */
  --accent-soft-line: #22322e;
  --warn-line:        #3a3116;
  --info-line:        #1c2c40;
  --danger-line:      #3a1c1f;
  --bg-raised-rgb:    17 20 26;  /* --bg-raised as channels, for translucent fades */
  --shadow-pop:       0 10px 28px rgb(0 0 0 / .45);  /* a panel lifted over the page */
  --shadow-lift:      0 -12px 28px rgb(0 0 0 / .55);  /* the same, for a panel opening upward */
  /* Trend lines on /time. The best line is the text ink and its fitted trend a warm grey, both
     neutral, so no vendor's dot colour reads as them: the fit's ink is ΔE2000 ≥ 19.6 from every
     vendor palette slot (≥ 12.7 under deutan and protan simulation) and 4.5:1 on --bg-inset;
     --text-dim, its first ink, all but matched the palette's slate (#9aa7bd, ΔE 3.2). The
     open/closed pair is blue/orange, validated all-pairs on --bg-inset (dataviz validator,
     dark): CVD ΔE 25.8, normal-vision ΔE 30.4, contrast ≥ 3:1. */
  --trend-best:   var(--text);
  --trend-open:   #4a90e8;
  --trend-closed: #e0662e;
  --trend-fit:    #887880;

  /* Type scale: base, components and views size text only from this set. */
  --fs-2xs:      10px;
  --fs-xs:       11px;
  --fs-s:        12px;
  --fs-m:        13px;
  --fs-base:     14px;
  --fs-stat-sm:  15px;
  --fs-l:        16px;
  --fs-stat:     17px;
  --fs-title-sm: 18px;
  --fs-title:    20px;

  --radius:  6px;
  --radius-s: 4px;
  --gap:     16px;
  /* One gutter for the whole shell: header (the wordmark's left edge), views and footer
     all pad with it, so the board and the logo stay aligned at every width. The gutter is
     --pad-x, widened to the device's safe-area inset on a notched screen (index.html sets
     viewport-fit=cover); the shell pads with --pad-l / --pad-r, never --pad-x directly. */
  --pad-x:   12px;
  --pad-l:   max(var(--pad-x), env(safe-area-inset-left, 0px));
  --pad-r:   max(var(--pad-x), env(safe-area-inset-right, 0px));
  /* The prose measure: ≈ 75 characters of running text (1ch is the width of the zero, ≈ 1.12
     average characters in the system sans; 640 px at 15 px on macOS, ≈ 560 px on Segoe UI and
     Roboto, whose zero is narrower). Every page's paragraphs stop here; tables, charts and
     grids take the full width. */
  --measure: 68ch;
  /* The pinned footer's height (design 18 §4.2): core/footer-pin.js writes the measured
     height while the footer is pinned and 0 where it scrolls with the page; this is the value
     before JS runs. The body reserves it, and the docs rails and the scatter's first screen
     stop above it. */
  --footer-h: 36px;

  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  /* One scheme for the whole app: native controls, autofill, the UA canvas and system
     colours render dark, and every checkbox, radio and range takes the accent. */
  color-scheme: dark;
  accent-color: var(--accent);
}

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

/* No `height: 100%` here: it makes <body> a fixed-height containing block, which
   silently kills `position: sticky` on .app-header past the first viewport. The
   `min-height: 100vh` + column flex below is what actually holds the shell open. */
html { height: 100%; }
/* Fragment jumps and focus scrolls stop clear of the pinned footer (design 18 §4.2). */
html { scroll-padding-bottom: calc(var(--footer-h) + 16px); }
/* The page's scrollbar keeps its room whether or not a page is long enough to need it, so a
   route that loads taller than the screen and lands shorter never shifts the layout sideways
   on a classic (Windows, Linux) scrollbar (UB-650). Overlay scrollbars take no room either way. */
html { scrollbar-gutter: stable; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--fs-base);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  /* The pinned footer's room, so the page's last line scrolls clear of it. Border-box (the
     reset above), so min-height still holds a short page open without a scrollbar. */
  padding-bottom: var(--footer-h);
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
h1, h2, h3 { margin: 0; font-weight: 600; line-height: 1.25; }
h1 { font-size: var(--fs-title); }
h2 { font-size: var(--fs-l); }
h3 { font-size: var(--fs-m); color: var(--text-dim); font-weight: 600;
     text-transform: uppercase; letter-spacing: .06em; }
p { margin: 0 0 10px; }
code, .mono, .num { font-family: var(--mono); font-variant-numeric: tabular-nums; }

.skip-link {
  position: absolute; left: -9999px; top: 0; padding: 8px 12px;
  background: var(--accent); color: var(--on-accent); z-index: 100;
}
.skip-link:focus { left: max(8px, env(safe-area-inset-left, 0px)); top: 8px; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A route heading holds focus after a navigation so a screen reader announces the page
   (UB-396); it is never a control. After a keyboard navigation it still shows a cue, as an
   accent underline under the title rather than a box around it, which read as a stray
   frame (UB-544). A pointer navigation shows neither. */
h1[tabindex="-1"]:focus-visible, h2[tabindex="-1"]:focus-visible {
  outline: none;
  text-decoration: underline 2px var(--accent); text-underline-offset: 6px;
}
h1[tabindex="-1"]:focus:not(:focus-visible), h2[tabindex="-1"]:focus:not(:focus-visible) { outline: none; }
/* A page that opens on its rail (the board, /cost, /time, /benchmarks) lands focus on the rail
   instead, named by its hidden h1 (UB-588): after a keyboard navigation the global ring
   above draws round it; a pointer navigation, or a press on the rail's bare background,
   draws nothing. */
[data-route-focus]:focus:not(:focus-visible) { outline: none; }

/* ---------- scrollbars (app scheme: black track / green thumb) ---------- */

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--accent-dim); border-radius: 999px;
                            border: 2px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
::-webkit-scrollbar-corner { background: var(--bg); }
html { scrollbar-color: #1d3a34 #0b0d11; } /* Firefox two-value form takes no var() in FF<112 — literals match the tokens */

/* ---------- header ---------- */

/* One row at every width (owner, 2026-09-26): the wordmark, the five links, the index pill
   and the credit never wrap. Where the row is wider than the screen (phones and portrait
   tablets, or text enlarged by a reader's text-only zoom or minimum font size) the header
   scrolls sideways under the wordmark, which stays pinned at its left edge, and a fade at the
   right edge shows there is more; the page itself never scrolls sideways. The current link
   is scrolled clear of the wordmark and the fade on every navigation, and so is each link
   that takes keyboard focus (core/boot/chrome.js markNav, revealFocusInHeader). */
.app-header {
  display: flex; align-items: center; gap: var(--gap);
  padding: 10px var(--pad-r) 10px var(--pad-l);
  border-bottom: 1px solid var(--line);
  background: var(--bg-raised);
  position: sticky; top: 0; z-index: 20;
  flex-wrap: nowrap;
  overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: none;
}
.app-header::-webkit-scrollbar { display: none; }
/* The wordmark is the home link: it keeps the text colour and takes the global focus ring.
   Pinned while the row scrolls, it sits on a patch of the header's own surface that runs
   from the header's left edge through its full height and fades out past the wordmark, so
   the links pass under it cleanly. */
.brand {
  display: flex; align-items: center; gap: 8px; flex: none;
  color: var(--text); text-decoration: none; border-radius: var(--radius-s);
  position: sticky; left: 0; z-index: 1;
}
.brand::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  top: -12px; bottom: -12px; left: calc(-1 * var(--pad-l)); right: -16px;
  background: linear-gradient(to right, var(--bg-raised) calc(100% - 16px), rgb(var(--bg-raised-rgb) / 0));
}
.brand:hover { text-decoration: none; }
.brand-mark {
  width: 12px; height: 12px; border-radius: 3px;
  background: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim);
}
.brand-name { font-weight: 700; letter-spacing: .1em; font-size: var(--fs-m); }

.app-nav { display: flex; gap: 2px; flex: none; flex-wrap: nowrap; }
.app-nav a {
  color: var(--text-dim); padding: 6px 10px; border-radius: var(--radius-s);
  font-size: var(--fs-m); text-decoration: none; white-space: nowrap;
}
.app-nav a:hover { color: var(--text); background: var(--bg-inset); text-decoration: none; }
.app-nav a[aria-current="page"] { color: var(--text); background: var(--accent-dim); }

/* The index count (owner, 2026-09-27): a green pill right of Documentation, green text on a
   green border. The worker writes the served snapshot's current_count into it at render
   (src/web/shell/chrome.ts) and the boot health read keeps it current (core/models-indexed.js);
   hidden until either has a number. Its two parts sit a glyph apart (`gap`: a flex item drops
   the label's leading space from sight, never from its text). The label shortens with the
   width (the bands below): what it drops stays in the DOM, hidden from sight only, so the
   pill's spoken name is whole, as the chart links' hidden word is. The pill is its hidden
   label's containing block (`position: relative`): on a phone the header is static, and a
   label positioned against the page would sit past the row's end and scroll the page
   sideways. */
/* A status, not a control (UB-669): green text on a green border over no fill, so it never
   reads as one of the filter chips under it, which share the accent wash; set apart from the
   links by its margin. */
.models-indexed {
  position: relative;
  display: inline-flex; align-items: center; align-self: center; flex: none; gap: 1ch;
  margin-left: 8px; padding: 3px 7px; border-radius: 999px;
  border: 1px solid var(--accent); background: transparent; color: var(--accent);
  font-family: var(--mono); font-size: var(--fs-s); line-height: 1; white-space: nowrap;
}
.models-indexed[hidden] { display: none; }
/* The wordmark's copy of the count shows on narrow screens only (the 860px block below). */
.models-indexed--brand { display: none; }
.models-indexed-n { font-weight: 700; font-variant-numeric: tabular-nums; }

.header-credit {
  margin-left: auto; flex: none;
  color: var(--text-dim); font-size: var(--fs-s); text-decoration: none;
  padding: 6px 0; white-space: nowrap;
}
.header-credit:hover { color: var(--text); text-decoration: underline; }

/* Above 860px the row fits at normal text sizes (tests/ui/shell/header-one-row.test.ts): the
   credit gives way, wrapping onto a second line inside the row (two 16px lines are the nav's
   own 32px), never narrower than 14ch, so under enlarged text the row scrolls instead of the
   credit collapsing into a column. Up to 1099px the two chart links drop their shared
   "Intelligence" to read "vs Cost" and "vs Time"; the word is hidden from sight only, so each
   link's name stays whole. (The switch sat at 1059px until the pill's "N Models" had to fit
   beside the full labels, UB-619.) */
@media (min-width: 861px) {
  .header-credit {
    flex: 0 1 auto; min-width: 14ch;
    padding: 0; line-height: 16px; white-space: normal; text-align: right; text-wrap: balance;
  }
  /* The right-edge fade for a row enlarged text makes scroll (review ui-1, round 2). It lies
     over the header's end padding and takes no room: its margins cancel the gap before it and
     its own width, so a row that fits keeps every pixel and the fade covers no text. While
     more of the row lies beyond the edge it holds there (sticky), over the cut-off text. A
     sticky offset counts from inside the header's padding, so `right: 0` would pull the fade
     back over the credit's last letters; minus the end padding it holds at the header's own
     edge. */
  .app-header::after {
    content: ""; flex: none; align-self: stretch; width: var(--pad-r);
    margin: 0 calc(-1 * var(--pad-r)) 0 calc(-1 * var(--gap));
    position: sticky; right: calc(-1 * var(--pad-r)); pointer-events: none;
    background: linear-gradient(to right, rgb(var(--bg-raised-rgb) / 0), rgb(var(--bg-raised-rgb)));
  }
}
@media (max-width: 1099px) {
  .app-nav .nav-long {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
}
/* The pill's label steps down with the width, never back up (UB-619): the full "N Models
   Indexed" from 1160px, "N Models" from 898px (a tighter pill: 2px of margin, 2px less
   padding a side, 11px type), and the number alone at 897px and below, phones included, where even
   "N Models" would push the credit onto a third line (tests/ui/shell/header-pill.test.ts).
   A hidden part is hidden from sight only, so the pill's spoken name stays whole. */
@media (max-width: 1159px) {
  .models-indexed { margin-left: 2px; padding: 3px 5px; font-size: var(--fs-xs); }
  .models-indexed-label-tail {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
}
@media (max-width: 897px) {
  .models-indexed-label {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
}

/* ---------- main + footer ---------- */

/* Every view runs full-bleed, edge to edge with the shared shell gutter only (owner directive
   2026-09-26): the board's width is every page's width. Running text keeps its measure
   (`.view p`). */
.view {
  flex: 1 1 auto;
  padding: 18px var(--pad-r) 40px var(--pad-l);
  width: 100%;
  outline: none;
}
.view p { max-width: var(--measure); }
/* The board's unranked notice is one line over the table, as it was before the measure: the
   board keeps its layout (FW-09, the §6 before/after captures). Other notice lines keep the
   measure. */
.view p.lb-unranked-hint { max-width: none; }
/* A dialog drawn inside a view (the docs search palette) sizes its own lines: its hint's rule
   divides the whole panel, not the first 68 characters of it. */
.view [role="dialog"] p { max-width: none; }
/* While a route loads, the view holds a full viewport. Without it the footer sits at the
   viewport bottom under "Loading…" and every long route shoves it off on first paint
   (CLS ~0.04 desktop, ~0.08 phone). Keyed to the `loading-state` marker, never to
   `.empty-state`, so the 404 and error states keep their visible footer; the height sits
   on .view, not on the dashed box. A browser without :has() keeps the old behaviour. The
   pinned footer's room is the body's; with the header above it the loading page runs a
   header's height past the screen, and the stable scrollbar gutter (`html` above, UB-650)
   keeps the scrollbar that shows up for it from moving the layout when a short route lands. */
.view:has(> .loading-state) { min-height: calc(100vh - var(--footer-h)); }
/* No rule keys on `.view[aria-busy]` (set by main.js while an in-place update is out):
   an inherited property there restyles every node of the ~14.5k-node board in the frame
   that should only show the pressed chip, which doubled that frame at 4x CPU (UB-192). */

/* Pinned to the bottom of the screen on desktop and tablet (owner, 2026-09-27: "put the footer
   as sticky"; design 18 D11), on the page's own surface so nothing reads through it, under the
   dialogs (z-index 200) and over the page. It scrolls with the page on phones and on short,
   narrow windows (the unpin rules at the end of this sheet). The page note's expander panels anchor to the footer, not to
   their summaries: a fixed box is its descendants' containing block as a relative one was. */
.app-footer {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
  background: var(--bg);
  /* The bottom inset keeps the footer text off the home indicator. */
  padding: 8px var(--pad-r) calc(8px + env(safe-area-inset-bottom, 0px)) var(--pad-l);
  border-top: 1px solid var(--line);
  color: var(--text-faint); font-size: var(--fs-s);
  font-family: var(--mono);
}
/* The stamp and the page note inside the footer are the footnote component's:
   components.css ("footer note"). */

.view-head {
  display: flex; align-items: baseline; gap: 12px;
  flex-wrap: wrap; margin-bottom: 14px;
}
.view-head .count { color: var(--text-faint); font-family: var(--mono); font-size: var(--fs-s); }

@media (max-width: 860px) {
  /* The row scrolls here (see the header's rule): a tighter gap and a smaller wordmark and
     credit keep more of it on screen; the credit sits at the row's end. */
  .app-header { gap: 10px; padding: 8px var(--pad-r) 8px var(--pad-l); }
  .brand-name { font-size: var(--fs-s); }
  /* The count beside the pinned wordmark (UB-651): the nav's pill lies past the screen's edge
     until the row is swiped, so the wordmark carries a copy of the number, hidden from the
     accessibility tree (the nav's pill is the one that is named). */
  .models-indexed--brand { display: inline-flex; margin-left: 0; padding: 2px 4px; font-size: var(--fs-2xs); }
  /* With the copy on screen the nav's pill leaves sight, so the count never shows twice
     where the row's end is in view (a tablet, a phone's /docs; UB-665); it stays the named
     instance, hidden from sight only. The nav is its containing block, so it stays inside
     the scrolling row and never widens a phone's page. */
  .app-nav { position: relative; }
  .app-nav > .models-indexed {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap; margin: 0; padding: 0; border: 0;
  }
  /* The wordmark's patch stays opaque 10px past the copy before it fades, so a link scrolled
     under it never reads as one string with the count ("(306)derboard", UB-668). The patch
     keeps its size: the phone credit's budget is unchanged. */
  .brand::before { background: linear-gradient(to right, var(--bg-raised) calc(100% - 6px), rgb(var(--bg-raised-rgb) / 0)); }
  /* Two balanced lines (no taller than a link), so the whole credit fits beside the pinned
     wordmark and its count once the row is scrolled to its end. The lines hug the row's end,
     so the box's slack, not its text, lies under the wordmark's patch. */
  .header-credit {
    font-size: var(--fs-xs); margin-left: 0; padding: 0 20px 0 0;
    white-space: normal; width: max-content; max-width: calc(30ch + 20px); line-height: 14px; text-wrap: balance;
    text-align: right;
  }
  /* The fade at the row's right edge: the last flex item, held at the right edge while more
     of the row lies beyond it (sticky); at the row's end it covers the credit's padding. */
  .app-header::after {
    content: ""; flex: none; align-self: stretch; width: 20px; margin-left: -20px;
    position: sticky; right: 0; pointer-events: none;
    background: linear-gradient(to right, rgb(var(--bg-raised-rgb) / 0), rgb(var(--bg-raised-rgb)));
  }
  .view { padding-top: 12px; }
}

/* Touch pointers (UB-415): a footer link is ~18px tall, so it gets a finger-sized hit area
   without moving the line. ::after, because the separator dots use ::before; 4px a side
   keeps two neighbours' areas apart across the 10px gap. */
@media (pointer: coarse) {
  .app-footer a { position: relative; }
  .app-footer a::after { content: ""; position: absolute; inset: -9px -4px; }
  /* A summary's ::after is its caret, so its hit area grows by padding instead. */
  .foot-x > summary { padding: 9px 0; margin: -9px 0; }
}

/* Every page title steps down together on a phone. The docs title out-ranks this rule and
   keeps its own scale. */
@media (max-width: 560px) {
  h1 { font-size: var(--fs-title-sm); }
}

/* On a phone, and on a landscape screen ~390px tall, the header scrolls away with the page
   instead of pinning, so the view keeps the whole screen. Desktop and tablet keep it sticky
   (see the note above `html`); the credit stays in the header (CHANGELOG #40). The footer
   follows the header (design 18 D11): a pinned two-line footer would take a fifth of a
   phone's screen, so here it sits in flow at the page's end and reserves nothing. Keep the
   query equal to HEADER_UNPINNED_MQ in js/views/docs/interactions-nav.js, which zeroes the
   docs anchor offset here; FOOTER_UNPINNED_MQ in js/core/footer-pin.js is this query and the
   next block's, joined. */
@media (max-width: 640px), (max-height: 500px) {
  :root { --footer-h: 0px; }
  .app-header { position: static; }
  .app-footer { position: relative; }
}
/* A short, narrow window (a tablet in a split view, a small desktop window) wraps the note
   under the stamp at 900px and below, and pinned there the footer took 12-17% of a screen up
   to 700px tall, the proportion D11 unpins phones for (UB-642). The footer sits in flow there;
   the header keeps its pin, and a tall tablet (768 x 1024) keeps both. */
@media (max-width: 900px) and (max-height: 700px) {
  :root { --footer-h: 0px; }
  .app-footer { position: relative; }
}

/* Print: the header and footer print once, in flow, never over the page's text. */
@media print {
  :root { --footer-h: 0px; }
  .app-header, .app-footer { position: static; }
}
