/* ULTRABENCH — components: chips, badges, buttons, inputs, tables, panels, empty states. */

/* ---------- buttons + inputs ---------- */

.btn {
  font: inherit; font-size: var(--fs-s); line-height: 1;
  padding: 7px 11px; border-radius: var(--radius-s);
  border: 1px solid var(--line-strong); background: var(--bg-inset); color: var(--text);
  cursor: pointer;
}
.btn:hover { border-color: var(--text-faint); }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
.btn-primary:hover { filter: brightness(1.08); }
.btn-ghost { background: transparent; }

input[type="text"], input[type="password"], input[type="number"], select {
  font: inherit; font-size: var(--fs-m);
  background: var(--bg-inset); color: var(--text);
  border: 1px solid var(--line-strong); border-radius: var(--radius-s);
  padding: 6px 8px; min-width: 0;
}
input::placeholder { color: var(--text-faint); }

/* ---------- chips ---------- */

.chip {
  display: inline-flex; align-items: center;
  font-family: var(--mono); font-size: var(--fs-2xs); line-height: 1;
  padding: 3px 5px; border-radius: 3px;
  border: 1px solid var(--line-strong); color: var(--text-faint);
  white-space: nowrap; vertical-align: middle;
}
.chip + .chip { margin-left: 4px; }
/* trust ladder: 3 strong -> 0 muted */
.chip-t3 { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-dim); }
.chip-t2 { color: var(--accent-soft); border-color: var(--accent-soft-line); }
.chip-t1 { color: var(--text-dim); }
.chip-t0 { color: var(--text-faint); border-style: dashed; }
/* method */
.chip-m-derived_estimate { color: var(--warn); border-color: var(--warn-line); border-style: dashed; letter-spacing: .04em; }
.chip-m-measured  { color: var(--accent); border-color: var(--accent-dim); }
.chip-m-vendor    { color: var(--accent-soft); border-color: var(--accent-soft-line); }
.chip-m-aggregator{ color: var(--info); border-color: var(--info-line); }
.chip-m-community { color: var(--text-dim); }

.badge {
  display: inline-block; font-family: var(--mono); font-size: var(--fs-2xs); line-height: 1;
  padding: 3px 6px; border-radius: 3px; border: 1px solid var(--line-strong); color: var(--text-dim);
}
.badge-core        { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-dim); }
/* reference = published beside the index, weight 0 on purpose (it aggregates weighted rows) */
.badge-reference   { color: var(--info); border-color: var(--info-line); }
.badge-extended    { color: var(--text-dim); }
/* open weights — the one model attribute worth a badge on a public surface */
.badge-open,
.badge-open-weights { color: var(--info); border-color: var(--info-line); }
.badge-superseded  { color: var(--text-faint); }

.est { color: var(--warn); font-style: normal; }
.muted { color: var(--text-faint); }
.dash  { color: var(--text-faint); }
/* An estimate that says how it was made (VF-213): `≈value` is a disclosure, dotted to read
   as one, and opens its derivation under the value, in the cell. Top-aligned, so the chips
   after it stay on the value's line when it opens. */
.est-how { display: inline-block; vertical-align: top; text-align: inherit; }
.est-how > summary { display: inline; list-style: none; cursor: pointer; }
.est-how > summary::-webkit-details-marker { display: none; }
.est-how > summary .muted { text-decoration: underline dotted; text-underline-offset: 3px; }
.est-how-text {
  display: block; max-width: 36ch; margin-top: 4px; white-space: normal; text-align: left;
  font-family: var(--sans); font-size: var(--fs-s); line-height: 1.4; color: var(--text-dim);
}

/* ---------- panels ---------- */

.panel {
  background: var(--bg-raised); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px; margin-bottom: 14px;
}
/* A panel's title is an h2 under the page's one h1 (UB-354), drawn in the small-caps
   register panel titles have always had. Weight and line-height come from the base
   heading rule; only the bottom margin is set, so `.md-stacked`'s top margin still holds. */
.panel-title {
  font-size: var(--fs-m); color: var(--text-dim);
  text-transform: uppercase; letter-spacing: .06em; margin-bottom: 10px;
}
.panel-grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }

.kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 14px; font-size: var(--fs-m); }
.kv dt { color: var(--text-faint); font-size: var(--fs-s); }
.kv dd { margin: 0; font-family: var(--mono); font-variant-numeric: tabular-nums; }

.stat-row { display: flex; flex-wrap: wrap; gap: 18px; }
.stat { display: flex; flex-direction: column; gap: 2px; min-width: 92px; }
.stat .stat-label { color: var(--text-faint); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em; }
.stat .stat-value { font-family: var(--mono); font-size: var(--fs-stat); }

/* ---------- tables (horizontal scroll + sticky first column) ---------- */

/* Scroll affordance shell. A wide table that runs off a phone screen must SAY so: the
   fade sits above the right edge of the scroller (it cannot scroll away, unlike a
   pseudo-element inside the scroller) and the scrollbar is forced visible on the
   platforms that hide it until touched. Both are narrow-width only — on a desktop the
   table fits and a permanent fade would be decoration that lies. */
.scroll-x { position: relative; }

@media (max-width: 860px) {
  .scroll-x::after {
    content: ""; position: absolute; top: 1px; right: 1px; bottom: 1px; width: 34px;
    pointer-events: none; border-radius: 0 var(--radius) var(--radius) 0;
    background: linear-gradient(to right, rgb(var(--bg-raised-rgb) / 0), rgb(var(--bg-raised-rgb) / .92));
  }
  .scroll-x .table-wrap, .scroll-x .lb-wrap {
    scrollbar-width: thin; scrollbar-color: var(--accent-dim) transparent;
  }
  .scroll-x .table-wrap::-webkit-scrollbar, .scroll-x .lb-wrap::-webkit-scrollbar {
    height: 9px; -webkit-appearance: none;
  }
  .scroll-x .table-wrap::-webkit-scrollbar-track, .scroll-x .lb-wrap::-webkit-scrollbar-track {
    background: transparent;
  }
  .scroll-x .table-wrap::-webkit-scrollbar-thumb, .scroll-x .lb-wrap::-webkit-scrollbar-thumb {
    background: var(--accent-dim); border-radius: 999px;
  }
}

.table-wrap {
  overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg-raised);
  -webkit-overflow-scrolling: touch;
}
table.data { border-collapse: separate; border-spacing: 0; width: 100%; font-size: var(--fs-m); }
table.data th, table.data td {
  padding: 9px 12px; text-align: left; vertical-align: top;
  border-bottom: 1px solid var(--line); white-space: nowrap;
}
table.data th {
  position: sticky; top: 0; z-index: 2;
  background: var(--bg-inset); color: var(--text-dim);
  font-size: var(--fs-xs); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
}
/* A sortable header's control is the button inside it (UB-403): the button takes the
   cell's padding so the whole header stays the hit area, and inherits the header's type
   so nothing about its look changes. The focus ring sits inside the cell because the
   sticky header would clip an outset one. */
table.data th.sortable { padding: 0; user-select: none; }
table.data th.sortable:hover { color: var(--text); }
table.data th .th-sort {
  display: block; width: 100%; padding: 9px 12px; margin: 0; border: 0; background: none;
  color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit;
  text-align: inherit; line-height: inherit; white-space: inherit; cursor: pointer;
}
table.data th .th-sort:focus-visible { outline-offset: -2px; }
table.data th .sort-arrow { color: var(--accent); margin-left: 4px; }
table.data tbody tr:hover { background: var(--bg-inset); }
table.data tbody tr:last-child td { border-bottom: none; }
table.data td.num, table.data th.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
table.data td.wrap { white-space: normal; min-width: 220px; }

table.data th:first-child, table.data td:first-child {
  position: sticky; left: 0; z-index: 1; background: var(--bg-raised);
}
table.data th:first-child { z-index: 3; background: var(--bg-inset); }
table.data tbody tr:hover td:first-child { background: var(--bg-inset); }

/* Narrow screens: the first column stays pinned (it is the row's identity — a benchmark
   name, a quant, a GPU) and gives up width so the columns behind it are reachable
   instead of being pushed off a 390px screen entirely. */
@media (max-width: 560px) {
  table.data th, table.data td { padding: 8px 10px; }
  table.data th .th-sort { padding: 8px 10px; }
  table.data td.wrap, table.data th.wrap { min-width: 0; max-width: 42vw; }
  table.data th:first-child, table.data td:first-child {
    position: sticky; left: 0; max-width: 42vw;
    border-right: 1px solid var(--line-strong);
  }
}

/* ---------- empty / error states ---------- */

.empty-state {
  border: 1px dashed var(--line-strong); border-radius: var(--radius);
  padding: 34px 20px; text-align: center; color: var(--text-dim);
  background: var(--bg-raised);
}
/* A route-level state heads its page with the h1 (VF-216); it keeps the h2's size. */
.empty-state h1, .empty-state h2 { font-size: var(--fs-l); color: var(--text); margin-bottom: 6px; }
.empty-state p { margin: 0 auto; max-width: 46ch; }
/* The action line (a way back, a search) sits apart from the sentence above it. */
.empty-state p + p { margin-top: 12px; }
.error-state { border-color: var(--danger-line); }
.error-state h1, .error-state h2 { color: var(--danger); }
/* The API code under a failed read: small, for anyone reporting it (UB-245). */
.empty-state .error-code { margin-top: 6px; font-size: var(--fs-xs); color: var(--text-faint); }
.state-actions {
  display: flex; gap: 12px; justify-content: center; align-items: center;
  flex-wrap: wrap; margin-top: 14px;
}

.notice {
  border: 1px solid var(--line-strong); border-left: 3px solid var(--warn);
  background: var(--bg-inset); border-radius: var(--radius-s);
  padding: 8px 12px; margin-bottom: 12px; color: var(--text-dim); font-size: var(--fs-s);
}

/* ---------- footer note (components/footnote.js) and the stamp ---------- */

/* The stamp ("snapshot … · updated … · API docs · Privacy") is one group that pushes itself
   to the right edge, so it holds the same place on a page with a note and on one without
   (UB-590). Its middle dots come from CSS so an unstamped footer never shows a dangling
   separator: `#footer-updated` collapses while it is empty and takes its dot with it. */
.footer-meta {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px;
  margin-left: auto;
}
.footer-meta > * + *::before { content: "· "; content: "· " / ""; color: var(--line-strong); }
.app-footer #footer-updated:empty { display: none; }

/* The page's own line (components/footnote.js, owner directive 2026-09-26): prose in the
   reading face, first in the footer, with the stamp beside it at the right edge; on a
   narrow screen the stamp wraps under it. Its expanders open upward over the page, so
   opening one never moves the line. */
.footer-note {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px;
  padding-right: 16px;
  font-family: var(--sans);
}
.footer-note[hidden] { display: none; }
.footer-note strong { color: var(--text-dim); font-weight: 600; }
.foot-dot { color: var(--line-strong); }
.foot-x > summary {
  list-style: none; cursor: pointer; white-space: nowrap; color: var(--accent);
}
.foot-x > summary::-webkit-details-marker { display: none; }
/* The caret is decoration: the `/ ""` form keeps it out of the summary's spoken name, and
   a browser that does not know that form keeps the plain declaration before it. */
.foot-x > summary::after { content: " ▸"; content: " ▸" / ""; }
.foot-x[open] > summary::after { content: " ▾"; content: " ▾" / ""; }
.foot-x > summary:hover, .foot-x[open] > summary { color: var(--text); }
.foot-x-panel {
  position: absolute; left: var(--pad-l); bottom: calc(100% + 6px); z-index: 20;
  width: min(560px, calc(100% - var(--pad-l) - var(--pad-r)));
  padding: 10px 12px;
  background: var(--bg-raised); border: 1px solid var(--line-strong); border-radius: var(--radius);
  /* Lifted upward as well as down: it opens over the page, where a shadow cast only
     downward fell behind the footer and the panel read flat on the table (UB-667). */
  box-shadow: var(--shadow-lift), var(--shadow-pop);
  color: var(--text-dim); line-height: 1.55;
}
.foot-x-panel p { margin: 0; }
.foot-weights {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 2px 20px;
  margin: 0; padding: 0; list-style: none; font-variant-numeric: tabular-nums;
}
.foot-weights li { display: flex; justify-content: space-between; gap: 12px; }
.foot-weights li > span + span { color: var(--text); }
/* Wide enough for every panel to fit beside its summary: each opens right above the item
   that opened it instead of at the footer's left edge. */
@media (min-width: 1200px) {
  .foot-x { position: relative; }
  .foot-x-panel { left: -12px; bottom: calc(100% + 14px); width: 560px; }
}
/* Once the line cannot hold the whole note, a dot would dangle at a line's end or open the
   next: the statement takes its own line, and the expanders and links follow it spaced,
   undotted. The board's note is ~800px wide. The stamp wraps under it at the left, spaced
   and undotted the same way, so a phone's second stamp line never opens on a dot (UB-590). */
@media (max-width: 900px) {
  .footer-note { gap: 4px 16px; padding-right: 0; }
  .footer-note > :first-child { flex-basis: 100%; }
  .foot-dot { display: none; }
  .footer-meta { margin-left: 0; gap: 4px 16px; }
  .footer-meta > * + *::before { content: none; }
}

/* One line wherever every page's expanders and the whole stamp fit beside a shortened
   statement (UB-666): the statement gives way first, cut with an ellipsis ("How it's
   computed" says it in full), so the pinned footer never wraps onto a second line of the
   screen. The expanders, the link and the stamp never shrink. /time's note is the longest
   (≈500px of expanders and link beside the ≈485px stamp), so from 1200px the statement keeps
   ≈160px at least. Nothing on the line clips: the panels overflow the note, never cut by it. */
@media (min-width: 1200px) {
  .app-footer { flex-wrap: nowrap; }
  .footer-note { flex: 0 1 auto; min-width: 0; flex-wrap: nowrap; }
  .footer-note > span:first-child {
    flex: 0 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .footer-note > :not(:first-child) { flex: none; }
  .footer-meta { flex: none; flex-wrap: nowrap; }
}
