/* states.css — loading, empty, waiting, failed, skeleton.
 *
 * THREE TREATMENTS THAT MUST STAY VISUALLY DISTINCT, because conflating any two
 * of them is a lie in exactly the way the honesty rules name:
 *
 *   .asof[data-state=stale]  we measured, a while ago    --warn, upright
 *   .void / fresh-unknown    we never measured this      --void, italic, dotted
 *   .state-empty             we measured, and the        --ink-3, upright,
 *                            answer is nothing           a full sentence
 *
 * And a fourth, which is none of those: .state-waiting — "we have not watched
 * long enough yet". That is not "there is nothing"; it is a promise with a
 * known threshold, so it gets a progress bar and its own component.
 */

.state {
  display: grid;
  gap: var(--s-2);
  padding-block: var(--s-5);
}

/* The title is the publication speaking, so it is set in Fraunces. */
.state-title {
  font-family: var(--font-display);
  font-size: var(--t-xl);
  font-weight: 500;
  line-height: var(--lh-snug);
  letter-spacing: var(--tr-title);
  color: var(--ink);
  text-wrap: balance;
}
.state-detail {
  max-width: var(--w-text);
  color: var(--ink-3);
  font-size: var(--t-sm);
  line-height: var(--lh-normal);
  text-wrap: pretty;
}
.state-action { margin-top: var(--s-2); justify-self: start; }

/* Measured zero. Upright, full sentence, achromatic — never --void, because we
 * DID look. The detail must say why it is empty in our terms ("Nobody has been
 * recorded in this group yet"), never "No data". */
.state-empty .state-title { color: var(--ink-2); font-style: italic; font-weight: 420; }
.state-empty { padding-left: var(--s-4); border-left: var(--edge-strong); }

.state-loading {
  display: flex; align-items: center; gap: var(--s-2);
  color: var(--ink-3);
  font-size: var(--t-sm);
}
.state-loading.is-inline { display: inline-flex; padding: 0; }

.spinner {
  flex: none;
  width: 14px; height: 14px;
  border: 2px solid var(--rule);
  border-top-color: var(--ink-3);
  border-radius: var(--r-full);
  animation: spin 620ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Under reduced motion base.css hides .spinner entirely and appends an ellipsis
 * to .state-loading — the spinner is REPLACED, not frozen into a broken ring. */

.state-error { padding-left: var(--s-4); border-left: var(--bw-2) solid var(--danger); }
.state-error .state-title { color: var(--danger); }
.state-error .state-detail { color: var(--ink-2); }
.state-error .state-http {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
}

/* "Not enough history yet" — a DIFFERENT component from empty, because
 * "we have not watched long enough" is not "there is nothing". */
.state-waiting .state-title { color: var(--ink-2); font-weight: 600; }
.waiting-progress {
  width: min(100%, 16rem);
  height: 4px;
  margin-top: var(--s-1);
  border-radius: var(--r-full);
  background: var(--surface-2);
  overflow: hidden;
}
.waiting-progress-fill {
  height: 100%;
  border-radius: inherit;
  background: var(--ink-3);
  transition: width var(--dur-4) var(--ease-out);
}

/* --------------------------------------------------------- skeletons
 *
 * Ranked lists only. The row rhythm is known, so the page does not reflow when
 * the data lands. NEVER A SKELETON CHART — a fake graph is worse than no graph,
 * because a reader cannot tell a placeholder shape from a measured one. */
.skeleton { display: grid; gap: var(--s-3); }
.sk-line {
  height: 0.85em;
  border-radius: var(--r-1);
  background: linear-gradient(
    90deg,
    var(--surface-2) 0%,
    color-mix(in oklab, var(--surface-2) 55%, var(--bg)) 50%,
    var(--surface-2) 100%
  );
  background-size: 200% 100%;
  animation: sk 1.4s ease-in-out infinite;
}
@keyframes sk { from { background-position: 200% 0; } to { background-position: -200% 0; } }

.sk-row { display: grid; gap: var(--s-2); padding-block: var(--s-3); border-bottom: var(--edge-soft); }
/* A card skeleton for .gboard: the numeral block and two title lines, at the
 * card's real height, so the board does not jump when the groups land. */
.sk-card {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr);
  column-gap: var(--s-3);
  row-gap: var(--s-2);
  padding: var(--s-4) var(--s-3);
  border-top: var(--edge-soft);
  min-height: 6.5rem;
}
.sk-card > .sk-line:first-child { grid-row: span 3; height: 1.9rem; }
.sk-w40 { width: 40%; }
.sk-w60 { width: 60%; }
.sk-w80 { width: 80%; }

/* A refetch keeps its content and dims — no spinner, no layout change, because
 * switching a board tab must not blank the page. */
[aria-busy='true'] .ranked,
[aria-busy='true'] .table-wrap { opacity: 0.55; transition: opacity var(--dur-2); }
