/* base.css — fallback faces, reset, element defaults, the type classes, the
 * one focus ring, and the global reduced-motion rule.
 *
 * Nothing here knows about a component. If a rule needs a component's name, it
 * lives in one of the sheets after this one.
 */

/* ------------------------------------------------------ fallback faces
 *
 * Metric-matched stand-ins, so the swap to the webfont (display=swap) moves
 * line breaks as little as possible. The size-adjust values are MEASURED, not
 * guessed: the width of one 70-character pangram at 100px, webfont over local
 * face — Fraunces 400 / Georgia 400 = 29.10 / 33.48, Bricolage 400 / Arial 400
 * = 31.65 / 33.74 (Chrome, Windows, 2026-09-25). Re-measure if a face changes. */
@font-face {
  font-family: "Fraunces Fallback";
  src: local("Georgia"), local("Noto Serif");
  size-adjust: 87%;
}
@font-face {
  font-family: "Bricolage Fallback";
  src: local("Arial"), local("Helvetica"), local("Roboto");
  size-adjust: 94%;
}

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

html {
  -webkit-text-size-adjust: 100%;
  /* In-page jumps (the hero's "see the groups") land clear of the masthead. */
  scroll-padding-top: calc(var(--topbar-h) + var(--s-4));
}
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

body {
  margin: 0;
  background-color: var(--bg);
  background-image: var(--grain);
  color: var(--ink);
  font-family: var(--font-text);
  font-size: var(--t-base);
  line-height: var(--lh-normal);
  letter-spacing: var(--tr-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* The body NEVER scrolls sideways. Wide content scrolls inside its own
   * container — .table-wrap, .spine-scroll, .week-scroll, .year-wrap. `clip`
   * rather than `hidden`, so position: sticky on the masthead keeps working. */
  overflow-x: clip;
}

[hidden] { display: none !important; }

h1, h2, h3, h4, p, figure, dl, dd, ol, ul, blockquote { margin: 0; }
ol, ul { padding: 0; list-style: none; }

a { color: inherit; text-decoration: none; }
a:hover { color: var(--accent); }

button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: none; padding: 0; cursor: pointer; }

svg { display: block; }

kbd { font-family: var(--font-mono); }

/* An avatar from a remote host is a rectangle of unknown colour. A hairline
 * keeps a white photo from dissolving into the page and a black one from
 * looking like a hole, in both themes, without a border that shifts layout. */
img {
  max-width: 100%;
  outline: 1px solid oklch(0% 0 0 / 0.08);
  outline-offset: -1px;
}
:root[data-theme='dark'] img { outline-color: oklch(100% 0 0 / 0.1); }

/* ---------------------------------------------------------------- type
 *
 * The class says which VOICE, never which size: .t-display and .t-h1 differ in
 * scale, but both are the publication speaking. Fraunces sizes its own optical
 * master from the rendered size (font-optical-sizing: auto, the default), so a
 * 90px headline gets the hairline display cut and a 17px topic the text cut. */

.t-headline, .t-display, .t-h1, .t-h2, .t-h3 {
  font-family: var(--font-display);
  font-weight: 560;
  text-wrap: balance;
}

.t-headline {
  font-size: var(--t-headline);
  font-weight: 420;
  line-height: 0.95;
  letter-spacing: var(--tr-display);
}
.t-display {
  font-size: var(--t-display);
  font-weight: 480;
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-display);
}
.t-h1 {
  font-size: var(--t-3xl);
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-display);
}
.t-h2 {
  font-size: var(--t-2xl);
  line-height: 1.1;
  letter-spacing: var(--tr-title);
}
.t-h3 {
  font-size: var(--t-xl);
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-title);
}

/* The italic turn a broadsheet headline takes on its last word. */
.t-italic, .t-headline em, .t-display em, .t-h1 em, .t-h2 em {
  font-style: italic;
  font-weight: inherit;
}

.t-lg  { font-size: var(--t-lg); line-height: var(--lh-normal); }
.t-sm  { font-size: var(--t-sm); }
.t-xs  { font-size: var(--t-xs); }
.t-2xs { font-size: var(--t-2xs); }
.t-3xs { font-size: var(--t-3xs); }

/* The standfirst under a headline: larger than body, lighter than a heading. */
.dek {
  max-width: var(--w-text);
  color: var(--ink-2);
  font-size: var(--t-lg);
  line-height: 1.45;
  text-wrap: pretty;
}

/* THE KICKER: small caps in the mono face, over every section head and on
 * every eyebrow. `.label` is the same voice under its older name and stays an
 * alias, so the header search and the method notes keep working unchanged.
 * A unit is never set this way — a letterspaced unit reads as a label and
 * competes with the figure it belongs to. */
.kicker, .label {
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  font-weight: 600;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--ink-3);
  line-height: var(--lh-snug);
}
/* A kicker sitting on its own rule, as over a newspaper column. */
.kicker[data-rule] {
  display: inline-block;
  padding-top: var(--s-2);
  border-top: var(--edge-strong);
}

/* Running prose. One measure token. */
.prose {
  max-width: var(--w-text);
  font-size: var(--t-base);
  line-height: var(--lh-normal);
  text-wrap: pretty;
}
.prose > * + * { margin-top: var(--s-4); }
.prose a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }

.prose-relax { line-height: var(--lh-relax); }

/* Anything we MEASURED. The mono face is the measurement voice; tabular figures
 * keep a column of numbers in one place as it updates. */
.num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  letter-spacing: var(--tr-fig);
}

.muted { color: var(--ink-3); }
.dim   { color: var(--ink-2); }

/* "Not measured" must never read as a value. The signal is HUE AND TEXTURE,
 * not faintness: --void is the one near-zero-chroma grey on a warm page, and
 * the italic plus a dotted underline says "this is not a value" while staying
 * legible. Shared verbatim by .dur-void and .asof[data-state=unknown]. */
.void {
  color: var(--void);
  font-style: italic;
  text-decoration: underline dotted var(--void) 1px;
  text-underline-offset: 3px;
}

.ta-right { text-align: right; }
.ta-center { text-align: center; }
.nowrap { white-space: nowrap; }

.stack > * + * { margin-top: var(--s-4); }

/* User-written text (topics, names): any script, any direction. The element
 * gets dir="auto"; this keeps one long unbroken string from widening a column. */
.ugc { overflow-wrap: anywhere; unicode-bidi: plaintext; }
.clamp-1 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clamp-2 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

/* ------------------------------------------------------------- focus
 *
 * ONE focus indicator for the whole site: 2px of --accent at 2px offset, on
 * every stop, including the ones that removed their own outline. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: var(--bw-2) solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-2);
}

/* main carries tabindex="-1" so app.js can focus it after a route change. That
 * focus is programmatic, never a :focus-visible match, so no ring is drawn —
 * but Safari still paints a default outline for it, hence the explicit none.
 * Section heads that the hero jumps to are focused the same way. */
main:focus, [tabindex='-1']:focus:not(:focus-visible) { outline: none; }

/* For labels that a glyph carries visually but a screen reader cannot. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}

.skip-link {
  position: absolute; z-index: 100;
  top: var(--s-2); left: var(--s-2);
  padding: var(--s-2) var(--s-4);
  background: var(--ink);
  color: var(--bg);
  border-radius: var(--r-2);
  box-shadow: var(--shadow-lift);
  font-size: var(--t-sm);
  font-weight: 600;
  transform: translateY(-200%);
}
.skip-link:focus-visible { transform: none; color: var(--bg); }

/* ------------------------------------------------------------- press
 *
 * Buttons press; records do not. A row that shrinks under the cursor reads as
 * a control, and these rows are records. */
button:active, .btn:active { transform: scale(0.97); }
.ranked-row:active, .table tbody tr:active, .hdr-opt:active, .gcard:active { transform: none; }

::selection { background: var(--accent-soft); color: var(--ink); }

/* ------------------------------------------------------- reduced motion
 *
 * Blanket on purpose. Position never animates but opacity still may — a
 * cross-fade has no vestibular cost — and the spinner is REPLACED rather than
 * frozen into a static broken ring. Components that need a still design (the
 * helix, the headline) also get one explicitly in their own sheet; this rule
 * is the net under them, not the design. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .hdr-panel, .readout { animation: fade var(--dur-2) both !important; }
  @keyframes fade { from { opacity: 0; } to { opacity: 1; } }

  /* Charts arrive drawn, not drawing. */
  .line-path { stroke-dashoffset: 0 !important; }
  .spine-bar, .dist-bar { transform: none !important; }
  .meter-fill { transition: none !important; }

  /* Liveness stops moving and says so in ink instead. */
  .live-dot, .presence-dot[data-state='online'] { animation: none !important; }

  /* Nothing lifts. */
  .gcard:hover, .btn:hover { transform: none !important; }

  .spinner { display: none; }
  .state-loading::after { content: '…'; }
}
