/* data.css — figures, ranked rows, tables, durations, provenance, freshness.
 *
 * This is the sheet that carries the product's argument: every number says how
 * it was measured and when, and the three ways a number can be absent stay
 * visually distinct.
 */

/* ------------------------------------------------------------- figures
 *
 * The measurement primitive. The figure ladder is deliberately NOT the text
 * ladder: a measurement at 30px and a heading at 30px are not the same object.
 * Sharing one token is how a secondary figure ended up louder than the number
 * its list was actually ordered by. */
.fig {
  display: inline-flex; flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.fig-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  font-size: var(--t-fig);
  font-weight: 600;
  line-height: var(--lh-flat);
  letter-spacing: var(--tr-fig);
  white-space: nowrap;
}
.fig[data-size='giant'] .fig-value {
  font-size: var(--t-fig-giant);
  font-weight: 480;
  letter-spacing: -0.055em;
}
.fig[data-size='xl'] .fig-value { font-size: var(--t-fig-xl); font-weight: 520; }
/* The night edition lights its big figures; paper leaves them as ink. */
.fig[data-size='giant'] .fig-value,
.fig[data-size='xl'] .fig-value { text-shadow: var(--glow); }
.fig[data-size='lg'] .fig-value { font-size: var(--t-fig-lg); }
.fig[data-size='sm'] .fig-value { font-size: var(--t-fig-sm); font-weight: 550; }

/* The unit is prose, not a label: sentence case, text face, never letterspaced
 * caps. A letterspaced unit reads as a heading and competes with its figure. */
.fig-unit {
  font-family: var(--font-text);
  font-size: var(--t-xs);
  font-weight: 400;
  line-height: var(--lh-snug);
  color: var(--ink-3);
  letter-spacing: var(--tr-body);
  white-space: normal;
}

.fig[data-tone='live'] .fig-value { color: var(--live); }
.fig[data-tone='warn'] .fig-value { color: var(--warn); }
.fig[data-tone='void'] .fig-value {
  color: var(--void);
  font-style: italic;
  font-weight: 400;
  text-decoration: underline dotted var(--void) 1px;
  text-underline-offset: 3px;
}

/* --------------------------------------------------------------- stats */

.stat { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; }
.stat-label { color: var(--ink-3); }
.stat-note { font-size: var(--t-2xs); color: var(--ink-3); text-wrap: pretty; }
.stat-trend { margin-top: var(--s-1); }

/* A horizontal run divided by vertical hairlines, sized to sit in the aside or
 * to span the page. It wraps rather than scrolls: a statistic that has fallen
 * off the right edge is not a statistic. */
.stat-run {
  display: flex; flex-wrap: wrap;
  gap: var(--s-5);
  align-items: start;
}
.stat-run[data-rule='1'] > .stat + .stat {
  padding-left: var(--s-5);
  border-left: var(--edge-soft);
  margin-left: calc(-1 * var(--s-5) + var(--s-5));
}
.stat-run > .stat { flex: 1 1 8rem; }

/* ---------------------------------------------------------- ranked lists
 *
 * THE LOUD FIGURE ON A RANKED ROW MUST BE THE FIGURE THE LIST IS ORDERED BY.
 * This has broken twice, both times found only by looking at the deployed page.
 * `orderedBy` is a required argument of rankedList() and is rendered as the
 * figure column's caption, so the rule is mechanical rather than remembered. */

.ranked { display: block; }

.ranked-caption {
  display: grid;
  grid-template-columns:
    var(--w-rank) auto minmax(0, 1fr) minmax(0, var(--w-fig));
  column-gap: var(--s-4);
  padding-bottom: var(--s-2);
  border-bottom: var(--edge);
  color: var(--ink-3);
}
.ranked-caption-fig { grid-column: 4; text-align: right; }

.ranked-row {
  display: grid;
  grid-template-columns:
    var(--w-rank)                /* rank — px, never ch */
    auto                         /* faces               */
    minmax(0, 1fr)               /* names + claim       */
    minmax(0, var(--w-fig));     /* THE ORDERING FIGURE */
  column-gap: var(--s-4);
  /* Not center: the rank and the figure belong to the row's FIRST line, so a
   * two-line title does not drag them down past their own row. */
  align-items: start;
  padding-block: var(--s-3);
  border-bottom: var(--edge-soft);
  transition: background-color var(--dur-1) var(--ease-out);
}
a.ranked-row { color: inherit; display: grid; }
a.ranked-row:hover { background: var(--ink-wash); color: inherit; }
.ranked-row.is-flagged { opacity: 0.78; }
.ranked-row:focus-visible { outline-offset: -2px; }

/* A rank is a position, not a measurement, so it is NOT mono: it is set in
 * the publication's own voice, as a broadsheet numbers its items. */
.ranked-rank {
  font-family: var(--font-display);
  font-style: italic;
  font-variant-numeric: lining-nums tabular-nums;
  font-size: var(--t-lg);
  font-weight: 420;
  color: var(--ink-3);
  line-height: 1.2;
  text-align: right;
}
.ranked-row.is-podium .ranked-rank { color: var(--ink); font-weight: 650; }

.ranked-body { min-width: 0; }
.ranked-title {
  font-size: var(--t-base);
  font-weight: 600;
  line-height: var(--lh-snug);
  overflow-wrap: anywhere;
}
a:hover > .ranked-body > .ranked-title { color: var(--accent); }
.ranked-marks { margin-top: var(--s-1); }

/* At 720px with a 144px figure track the figure sits at most ~430px from the
 * longest name, so the ~680px gap a dotted leader was once invented to cross
 * cannot occur — and there is no leader. */
.ranked-fig {
  display: flex; flex-direction: column; align-items: flex-end;
  gap: 2px;
  text-align: right;
}
.ranked-fig .fig { align-items: flex-end; }
.ranked-fig .fig-unit { text-align: right; }

/* The quiet meta line under a row title. Parts are joined by a real separator
 * NODE, never by a literal ' · ' inside a string: a joined string cannot be
 * restyled, cannot wrap sensibly, and puts punctuation inside a translated
 * sentence. */
.claim {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--s-2);
  margin-top: var(--s-1);
  color: var(--ink-3);
  font-size: var(--t-xs);
  line-height: var(--lh-snug);
}
.claim-sep {
  flex: none;
  width: 2px; height: 2px;
  border-radius: var(--r-full);
  background: var(--rule);
  align-self: center;
}

.ranked-empty { padding-block: var(--s-5); }

@media (max-width: 29.9375rem) {
  .ranked-row {
    grid-template-columns: var(--w-rank) auto minmax(0, 1fr);
    row-gap: var(--s-2);
  }
  .ranked-row > .ranked-fig {
    grid-column: 2 / -1;
    align-items: flex-start;
    text-align: left;
  }
  .ranked-caption { grid-template-columns: var(--w-rank) auto minmax(0, 1fr); }
  .ranked-caption-fig { grid-column: 2 / -1; text-align: left; }
}

/* --------------------------------------------------------------- meter */

.meter { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; }
.meter-label {
  display: flex; justify-content: space-between; gap: var(--s-2);
  font-size: var(--t-2xs); color: var(--ink-3);
}
.meter-track {
  position: relative;
  height: var(--viz-bar-h);
  border-radius: var(--r-full);
  background: var(--surface-2);
  overflow: hidden;
}
.meter-fill {
  height: 100%;
  border-radius: inherit;
  background: var(--ink-2);
  /* The one exception to compositor-only motion: a meter's WIDTH is the datum,
   * and there is exactly one on screen at a time. */
  transition: width var(--dur-4) var(--ease-out);
}
.meter[data-tone='live'] .meter-fill { background: var(--live); }
.meter[data-tone='warn'] .meter-fill { background: var(--warn); }
/* "Approximate" is drawn as texture, not as a lighter fill — a lighter fill
 * reads as a smaller number. */
.meter.is-uncertain .meter-fill {
  background: repeating-linear-gradient(
    135deg,
    var(--ink-2) 0 4px,
    color-mix(in oklab, var(--ink-2) 35%, var(--surface-2)) 4px 8px
  );
}

/* -------------------------------------------------------- distribution */

.dist { display: grid; gap: var(--s-2); }
.dist-row {
  display: grid;
  grid-template-columns: minmax(4rem, 8rem) minmax(0, 1fr) auto;
  gap: var(--s-3);
  align-items: center;
}
.dist-label { font-size: var(--t-xs); color: var(--ink-2); }
.dist-track { height: var(--viz-bar-h); background: var(--surface-2); border-radius: var(--r-1); }
.dist-bar {
  height: 100%;
  background: var(--data-4);
  border-radius: var(--r-1);
  transform-origin: left center;
  animation: dist-grow var(--dur-4) var(--ease-out) both;
}
@keyframes dist-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.dist-row.is-highlight .dist-bar { background: var(--data-5); }
.dist-row.is-highlight .dist-label { color: var(--ink); font-weight: 600; }
.dist-count {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-xs);
  color: var(--ink-2);
  text-align: right;
}

/* --------------------------------------------------------------- delta */

.delta {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-xs);
  white-space: nowrap;
}
.delta[data-dir='up'] { color: var(--live); }
.delta[data-dir='down'] { color: var(--danger); }

/* ------------------------------------------------------------ durations
 *
 * `truncated = 1` means THE POLLER STOPPED WATCHING, not "the user left".
 * Truncated sessions were 42% of all recorded time and their durations cluster
 * at identical inflated values, so they must never render as an observed
 * duration. That is what .dur-void is for, and it shares the exact --void
 * treatment defined in base.css. */
.dur { display: inline-flex; align-items: baseline; flex-wrap: wrap; gap: var(--s-1); }
.dur-live { color: var(--live); font-weight: 550; }
.dur-sub { color: var(--ink-3); font-size: var(--t-xs); }
.dur-void {
  color: var(--void);
  font-style: italic;
  text-decoration: underline dotted var(--void) 1px;
  text-underline-offset: 3px;
}
.dur-soft { color: var(--ink-2); }

/* --------------------------------------------------------------- table */

/* Wide content scrolls inside its own container. The body never scrolls
 * sideways — non-negotiable. */
.table-wrap { overflow-x: auto; scrollbar-gutter: stable; }

.table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
.table caption {
  caption-side: top;
  text-align: left;
  padding-bottom: var(--s-2);
  color: var(--ink-3);
  font-size: var(--t-xs);
}
.table th {
  padding: var(--s-2) var(--s-3) var(--s-2) 0;
  border-bottom: var(--edge);
  color: var(--ink-3);
  font-size: var(--t-2xs);
  font-weight: 650;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  text-align: left;
  white-space: nowrap;
}
.table td {
  padding: var(--s-3) var(--s-3) var(--s-3) 0;
  border-bottom: var(--edge-soft);
  vertical-align: top;
}
.table th:last-child, .table td:last-child { padding-right: 0; }
.table tbody tr:hover { background: var(--ink-wash); }
.table.is-dense td { padding-block: var(--s-2); }
.table .ta-right { text-align: right; }

/* A definition row with a dotted leader — for a facts list, not for a ranked
 * row. The leader is only ever drawn across a SHORT distance here, because the
 * label column is capped. */
.meta-row {
  display: grid;
  grid-template-columns: minmax(6rem, 11rem) minmax(0, 1fr);
  gap: var(--s-3);
  padding-block: var(--s-2);
  border-bottom: var(--edge-hair);
  font-size: var(--t-sm);
}
.meta-label { color: var(--ink-3); }
.meta-value { min-width: 0; overflow-wrap: anywhere; }

/* ------------------------------------------------------ notes and method */

.note {
  color: var(--ink-2);
  font-size: var(--t-xs);
  line-height: var(--lh-relax);
  text-wrap: pretty;
}
.note[data-kind='caution'] { color: var(--warn); }
.note[data-kind='caveat'] { color: var(--ink-3); font-style: italic; }

/* THE SIGNATURE COMPONENT. Every board, chart and ranked list passes one: when
 * it was computed, how, over what window, what is excluded, how many qualified.
 * It lives in the aside, BESIDE what it describes, rather than in a paragraph
 * under the board where the old page buried it. */
.provenance {
  display: grid;
  gap: var(--s-2);
  font-size: var(--t-xs);
  line-height: var(--lh-relax);
  color: var(--ink-2);
}
.provenance-term {
  font-size: var(--t-3xs);
  font-weight: 650;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--ink-3);
}
.provenance-def { margin: 0 0 var(--s-3); }
.provenance-def:last-child { margin-bottom: 0; }
.provenance .age-bar { margin-top: var(--s-2); }

/* ==========================================================================
 * FRESHNESS — the "as of" ladder
 *
 * A figure computed on a timer can be hours old, and the page must ALWAYS be
 * able to say so without that looking like an error. So the age is always on
 * the page, and in its normal state it is quiet and achromatic: a component
 * that is always present cannot be mistaken for an alarm when its wording
 * changes.
 *
 * Colour escalates exactly twice — once to --live for "observed right now",
 * once to --warn for genuine staleness. `recent` and `aging` are achromatic on
 * purpose. Nothing here ever takes role="alert": old data is not wrong data.
 * ======================================================================== */

.asof {
  display: inline-flex; align-items: center; flex-wrap: wrap;
  gap: var(--s-1);
  font-size: var(--t-2xs);
  line-height: var(--lh-snug);
  color: var(--fresh-quiet);
}
.asof-time {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
}

.asof[data-state='live'] { color: var(--fresh-live); }
.asof[data-state='recent'],
.asof[data-state='aging'] { color: var(--fresh-quiet); }
.asof[data-state='stale'] { color: var(--fresh-stale); }

/* "Never computed" is the same class of claim as "not measured", so it gets the
 * same treatment, verbatim: --void, italic, dotted underline. Keeping this
 * distinct from `stale` is load-bearing — conflating "we measured a while ago"
 * with "we never measured" is a lie. */
.asof[data-state='unknown'] {
  color: var(--fresh-unknown);
  font-style: italic;
  text-decoration: underline dotted var(--fresh-unknown) 1px;
  text-underline-offset: 3px;
}

/* THE AGE IS DRAWN, not merely written. A hairline whose filled portion is the
 * ratio of the figure's age to its own recompute interval: readable at a glance
 * without a word being read. */
.age-bar {
  display: inline-block;
  width: var(--fresh-bar-w);
  height: var(--fresh-bar-h);
  flex: none;
  background: var(--fresh-track);
  border-radius: var(--r-full);
  overflow: hidden;
  vertical-align: middle;
}
.age-bar-fill {
  display: block;
  height: 100%;
  background: var(--fresh-fill);
  border-radius: inherit;
}
/* Past its own interval the bar is full, so fullness alone stops carrying
 * information. An overrun end-cap in --warn says "and then some". */
.age-bar[data-over='true'] { box-shadow: inset -2px 0 0 var(--fresh-stale); }
.age-bar[data-over='true'] .age-bar-fill { background: var(--fresh-stale); }

.fresh-note {
  font-size: var(--t-xs);
  line-height: var(--lh-relax);
  color: var(--ink-2);
  text-wrap: pretty;
}
.fresh-note[data-state='stale'] .fresh-note-consequence { color: var(--fresh-stale); }
.fresh-note[data-state='unknown'] .fresh-note-consequence {
  color: var(--fresh-unknown);
  font-style: italic;
}
.fresh-note .age-bar { margin-top: var(--s-2); }
