/* Free4Talk tracker.
 *
 * Light-first, indigo, generous. That is a deliberate reversal: the previous
 * dark-first "instrument" read as an operator console, and the people this is
 * for are not operators — they are looking up a person they know.
 *
 * Colour rules that are load-bearing, not taste:
 *   --live  observed / occupied / healthy
 *   --warn  stale collector, approximate data
 *   --void  truncated — MUST never read as a measured value
 * Nothing else encodes meaning in colour.
 */

:root {
  color-scheme: light dark;

  /* ---- ground: warm paper, not blue-grey */
  --bg:        oklch(97.2% 0.008 85);
  --bg-sunk:   oklch(94.6% 0.010 85);
  --surface:   oklch(99.2% 0.004 85);
  --surface-2: oklch(95.4% 0.009 85);
  --line:      oklch(85%   0.010 85);
  --line-soft: oklch(91%   0.008 85);

  /* ---- ruling: the structural device. A rule, not a rounded box. */
  --rule:       oklch(74% 0.012 85);           /* 1px section rule */
  --rule-faint: oklch(89% 0.009 85);           /* 0.5px hairline   */
  --paper-line: oklch(0% 0 0 / 0.035);         /* ledger ground    */
  --ink-wash:   oklch(24% 0.014 70 / 0.055);   /* hover fill       */
  --rule-pitch: 28px;                          /* ledger period    */
  --rail-w:     148px;                         /* margin column    */

  /* ---- ink: warm near-black, not cool slate */
  --text:      oklch(24% 0.014 70);
  --text-dim:  oklch(44% 0.012 70);
  --text-mute: oklch(54% 0.011 70);   /* was 60% — 60% was 4.1:1, under AA */

  /* ---- load-bearing semantics. Meaning unchanged, contrast fixed. */
  --live:      oklch(48% 0.13  158);  /* was 56% — now 6.5:1 as text */
  --live-dim:  oklch(78% 0.07  158);
  --warn:      oklch(47% 0.12  62);   /* was 62% = 2.8:1. FAILED AA.  */
  --void:      oklch(54% 0.004 70);   /* grey, not faint — see below  */
  --danger:    oklch(48% 0.17  27);

  /* ---- accent: prussian ink. Never a large fill. */
  --accent:      oklch(38% 0.11  258);
  --accent-soft: oklch(93% 0.024 258);
  --accent-dim:  oklch(80% 0.055 258);
  --on-accent:   oklch(99% 0.004 85);  /* text ON an accent fill */

  --radius-sm:   2px;
  --radius:      4px;
  --radius-lg:   6px;
  --radius-pill: 999px; /* only for genuinely pill-shaped things */

  /* Rhythm is intentional, not one padding everywhere — and it is tied to the
   * ledger pitch, so vertical space lands on the ruling. */
  --gap-xs:  4px;
  --gap-sm:  8px;
  --gap:     14px;
  --gap-lg:  28px;   /* one ledger line   */
  --gap-xl:  56px;   /* two               */
  --gap-2xl: 84px;   /* three             */

  --shell-max: 1180px;
  --topbar-h:  56px;

  /* ---- type scale: a real ratio with a display break */
  --t-micro:   10.5px;
  --t-fine:    12px;
  --t-small:   13.5px;
  --t-body:    15px;
  --t-lead:    17px;
  --t-h3:      20px;
  --t-h2:      clamp(22px, 1rem + 1.4vw, 27px);
  --t-h1:      clamp(30px, 1rem + 3.4vw, 46px);
  --t-display: clamp(44px, 2rem + 6vw, 88px);
  --t-figure:  clamp(26px, 1rem + 2.4vw, 40px);

  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", "JetBrains Mono", Menlo, monospace;

  --fast: 120ms;
  --med:  220ms;
  --ease:      cubic-bezier(0.16, 1, 0.3, 1);
  --ease-snap: cubic-bezier(0.2, 0, 0, 1);   /* decisive, for state changes */

  /* "Shadow" is now a rule. An almanac does not float. */
  --shadow:      0 1px 0 var(--rule-faint);
  --shadow-lift: 0 1px 0 var(--rule), 0 8px 20px -14px oklch(30% 0.02 70 / 0.35);
}

/* Why --void stopped being faint: the old value said "this is not a real number"
 * by being nearly invisible, which meant .dur-void's literal words "not
 * measured" rendered at 2.0:1 and could not be read. The signal moves off
 * lightness and onto hue and texture — --void is the only near-zero-chroma grey
 * on a page where everything else carries a warm cast, and .dur-void keeps its
 * italic and gains a dotted underline. Meaning unchanged; expression changed. */

/* Dark is declared twice on purpose: once for the OS preference (unless the
 * reader has explicitly chosen light) and once for the explicit toggle, which
 * must win in both directions. One combined selector list cannot express that.
 * The two blocks below must stay byte-identical — if you change one and not the
 * other, the toggle and the OS preference disagree and the bug only shows on
 * someone else's machine. The dark ground is WARM (hue 75-80), which is why it
 * does not look like every other dashboard's dark theme. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    color-scheme: dark;

    --bg:        oklch(16.5% 0.008 75);
    --bg-sunk:   oklch(13%   0.008 75);
    --surface:   oklch(20%   0.009 75);
    --surface-2: oklch(24.5% 0.010 75);
    --line:      oklch(33%   0.010 75);
    --line-soft: oklch(27%   0.009 75);

    --rule:       oklch(38% 0.011 75);
    --rule-faint: oklch(26% 0.009 75);
    --paper-line: oklch(100% 0 0 / 0.030);
    --ink-wash:   oklch(100% 0 0 / 0.05);

    --text:      oklch(94% 0.006 80);
    --text-dim:  oklch(74% 0.010 80);
    --text-mute: oklch(60% 0.011 80);

    --live:      oklch(78% 0.15  158);
    --live-dim:  oklch(48% 0.10  158);
    --warn:      oklch(80% 0.13  72);
    --void:      oklch(60% 0.004 80);
    --danger:    oklch(70% 0.17  27);

    --accent:      oklch(76% 0.10 250);
    --accent-soft: oklch(26% 0.035 250);
    --accent-dim:  oklch(42% 0.07  250);
    --on-accent:   oklch(16.5% 0.008 75);

    --shadow:      0 1px 0 oklch(0% 0 0 / 0.45);
    --shadow-lift: 0 1px 0 oklch(0% 0 0 / 0.55), 0 10px 24px -16px oklch(0% 0 0 / 0.7);
  }
}

:root[data-theme='dark'] {
  color-scheme: dark;

  --bg:        oklch(16.5% 0.008 75);
  --bg-sunk:   oklch(13%   0.008 75);
  --surface:   oklch(20%   0.009 75);
  --surface-2: oklch(24.5% 0.010 75);
  --line:      oklch(33%   0.010 75);
  --line-soft: oklch(27%   0.009 75);

  --rule:       oklch(38% 0.011 75);
  --rule-faint: oklch(26% 0.009 75);
  --paper-line: oklch(100% 0 0 / 0.030);
  --ink-wash:   oklch(100% 0 0 / 0.05);

  --text:      oklch(94% 0.006 80);
  --text-dim:  oklch(74% 0.010 80);
  --text-mute: oklch(60% 0.011 80);

  --live:      oklch(78% 0.15  158);
  --live-dim:  oklch(48% 0.10  158);
  --warn:      oklch(80% 0.13  72);
  --void:      oklch(60% 0.004 80);
  --danger:    oklch(70% 0.17  27);

  --accent:      oklch(76% 0.10 250);
  --accent-soft: oklch(26% 0.035 250);
  --accent-dim:  oklch(42% 0.07  250);
  --on-accent:   oklch(16.5% 0.008 75);

  --shadow:      0 1px 0 oklch(0% 0 0 / 0.45);
  --shadow-lift: 0 1px 0 oklch(0% 0 0 / 0.55), 0 10px 24px -16px oklch(0% 0 0 / 0.7);
}

/* ------------------------------------------------------------------ base */

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

/* The `hidden` attribute is only a UA rule of `display: none`, so any explicit
 * display on the element beats it — `.chip { display: inline-flex }` silently
 * kept hidden controls on screen. Anything marked hidden must actually be. */
[hidden] { display: none !important; }

html {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  margin: 0;
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.55;
  /* Wide content scrolls inside its own container; the body never does. */
  overflow-x: hidden;

  /* The ledger. One gradient, no request, and it scrolls with the page —
   * deliberately NOT background-attachment: fixed, which costs a compositing
   * layer on mobile and makes the ruling belong to the window rather than to
   * the page. The ruling IS the identity: it costs nothing and it is the
   * cheapest thing that makes a screenshot recognisably ours. */
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0,
    transparent calc(var(--rule-pitch) - 1px),
    var(--paper-line) calc(var(--rule-pitch) - 1px),
    var(--paper-line) var(--rule-pitch)
  );
  background-position: 0 -1px;
}

/* Every updating number is tabular so counters do not jitter as digits change. */
.num, .stat-value, .occ, .dur, td.ta-right, .strip-item strong, .meter-text {
  font-variant-numeric: tabular-nums;
}

h1, h2, h3 { text-wrap: balance; margin: 0; font-weight: 600; letter-spacing: -0.018em; }
p { text-wrap: pretty; margin: 0; }

/* ---------------------------------------------------- the measurement voice
 *
 * Humanist sans for prose, tabular mono for anything measured. The two faces
 * carry different kinds of claim: --font asserts, --mono reports. Every number
 * on this site is an observation, which is what earns mono the display job. */

.figure {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-figure);
  font-weight: 500;
  letter-spacing: -0.03em;
  line-height: 1;
}
/* Same voice, row scale — for figures that sit inside a line of a table rather
 * than heading a section. The face and the tabular lining do not change. */
.figure-sm { font-size: var(--t-lead); }
.figure-md { font-size: var(--t-h3); }

.figure-unit {
  font-family: var(--font);
  font-size: var(--t-micro);
  font-weight: 650;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-mute);
}

/* Small-caps labels — the almanac's section voice. */
.label {
  font-size: var(--t-micro);
  font-weight: 650;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--text-mute);
}

a { color: inherit; text-decoration: none; }

/* One focus indicator for the whole site. It is 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,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius);
}

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

img { outline: 1px solid oklch(0% 0 0 / 0.08); outline-offset: -1px; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) img { outline-color: oklch(100% 0 0 / 0.1); }
}
:root[data-theme='dark'] img { outline-color: oklch(100% 0 0 / 0.1); }

.shell { width: 100%; max-width: var(--shell-max); margin-inline: auto; padding-inline: var(--gap); }

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 60;
  background: var(--accent); color: var(--bg);
  padding: 10px 16px; border-radius: 0 0 var(--radius) 0;
}
.skip-link:focus { left: 0; }

.muted { color: var(--text-mute); }
.stack { display: flex; flex-direction: column; gap: var(--gap); }
.ta-right { text-align: right; }

/* For labels an arrow 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%);
}

/* ---------------------------------------------------------------- topbar */

/* A flush rule bar. No blur, no radius, no shadow, no pointer-events dance —
 * the bar is opaque and the page is ruled off beneath it, which is what an
 * almanac masthead does. Brand hard left, nav and control hard right. */
.topbar {
  position: sticky; top: 0; z-index: 40;
  background: var(--bg);
  border-bottom: 1px solid var(--rule);
}

.topbar-inner {
  height: var(--topbar-h);
  display: flex; align-items: center; gap: var(--gap);
}

.brand {
  display: flex; align-items: center; gap: 9px;
  flex: none;
  font-size: var(--t-small); font-weight: 600; letter-spacing: -0.01em;
}
.brand-dim { color: var(--text-mute); font-weight: 450; }

/* The mark: four ticks on an axis, the last one in accent — the "now" tick.
 * Ink, not a gradient chip; the only chromatic thing in the masthead. */
.mark { flex: none; display: block; }
.mark-axis, .mark-tick { stroke: var(--text); }
.mark-now { stroke: var(--accent); }

.nav { display: flex; gap: 2px; }

.nav-link {
  /* 40px minimum hit area even though the label is short. Emphasis comes from
   * a rule, never from a fill — colour is reserved for the semantics. */
  position: relative;
  display: inline-flex; align-items: center; min-height: 40px;
  padding: 0 12px;
  color: var(--text-mute); font-size: var(--t-small); font-weight: 500;
  transition-property: color;
  transition-duration: var(--fast);
  transition-timing-function: ease-out;
}
.nav-link:hover { color: var(--text); }
.nav-link.is-active { color: var(--text); font-weight: 600; }
.nav-link.is-active::after {
  content: '';
  position: absolute; left: 12px; right: 12px; bottom: 8px;
  height: 2px; background: var(--text);
}

.theme-toggle {
  min-height: 32px; min-width: 40px; padding: 0 10px;
  background: transparent; color: var(--text-mute);
  border: 1px solid var(--rule); border-radius: var(--radius);
  font-family: var(--mono); font-size: var(--t-micro); font-weight: 600;
  letter-spacing: 0.10em; text-transform: uppercase; cursor: pointer;
  transition-property: color, border-color;
  transition-duration: var(--fast);
}
.theme-toggle:hover { color: var(--text); border-color: var(--text); }
.theme-toggle:active { transform: scale(0.96); }

/* ------------------------------------------------------- liveness dots
 *
 * The old header status strip is gone (see app.js). These remain because
 * individual rooms and people still need to say "as of the latest poll". */

.live-dot, .stale-dot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
}
.live-dot {
  background: var(--live);
  box-shadow: 0 0 0 0 color-mix(in oklab, var(--live) 60%, transparent);
  animation: pulse 2.4s ease-out infinite;
}
.stale-dot { background: var(--warn); }

@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in oklab, var(--live) 55%, transparent); }
  70%  { box-shadow: 0 0 0 6px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
/* The guard is blanket on purpose. It used to name .live-dot and kill
 * transitions only, which left every ANIMATION running — including the
 * presence dot's pulse on a person page and the route-entrance below. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
  .live-dot, .presence-dot.is-online { animation: none !important; }
}

.is-stale { color: var(--warn); }
.is-disconnected { opacity: 0.72; }

/* ---------------------------------------------------------------- banner */

#banner:empty { display: none; }

.alert {
  max-width: var(--shell-max); margin: var(--gap) auto 0;
  padding: 11px var(--gap);
  border: 1px solid var(--line); border-left: 3px solid var(--warn);
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--warn) 8%, var(--surface));
  font-size: 13.5px; line-height: 1.45;
}
.alert-warn strong { color: var(--warn); }

/* ------------------------------------------------------------------ main */

main { display: block; padding-block: var(--gap-lg) var(--gap-xl); min-height: 60vh; }
main:focus { outline: none; }

/* Route entrance. Content settles rather than snapping — transform and opacity
 * only, so it stays on the compositor and never reflows. It is a one-shot on
 * the top-level blocks the router mounts, and the reduced-motion guard above
 * collapses it to 1ms. */
main > * {
  animation: settle var(--med) var(--ease-snap) both;
}
@keyframes settle {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

/* The almanac's margin line: one continuous vertical rule down the left of the
 * content column, paired with the body's horizontal ruling. Below 1100px there
 * is no margin to have, so it goes away rather than crowding the text. */
@media (min-width: 1100px) {
  main { position: relative; }
  main::before {
    content: '';
    position: absolute;
    top: 0; bottom: 0;
    left: max(var(--gap), calc(50% - var(--shell-max) / 2 + var(--gap)));
    width: 1px;
    background: var(--rule-faint);
    pointer-events: none;
  }
}

/* The last box. A panel is a band on the page like everything else. */
.panel {
  background: none;
  border: none;
  border-radius: 0;
}
.panel-body { padding: var(--gap) 0; }

.section-head { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--gap); }
.section-title { font-size: 17px; }
.section-note { color: var(--text-mute); font-size: 13px; }

.page-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--gap); flex-wrap: wrap; margin-bottom: var(--gap);
}
.page-title { font-size: clamp(20px, 1rem + 1.4vw, 26px); letter-spacing: -0.02em; }
.page-sub { color: var(--text-mute); font-size: 13.5px; }

/* ------------------------------------------------------------- controls */

.filters { display: flex; flex-wrap: wrap; gap: var(--gap-sm); align-items: center; }

.search-field { position: relative; flex: 1 1 260px; min-width: 0; }

.input, .select {
  width: 100%; min-height: 40px;
  padding: 0 12px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--radius);
  font: inherit; font-size: 14px;
  transition-property: border-color, background-color;
  transition-duration: var(--fast);
}
.input::placeholder { color: var(--text-mute); }
.input:hover, .select:hover { border-color: var(--text-mute); }
.select { width: auto; padding-right: 30px; cursor: pointer; }

.chip {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 32px; padding: 0 11px;
  background: var(--surface-2); color: var(--text-dim);
  border: 1px solid transparent; border-radius: var(--radius-pill);
  font: inherit; font-size: 12.5px; cursor: pointer; white-space: nowrap;
  transition-property: color, background-color, border-color;
  transition-duration: var(--fast);
}
.chip:hover { color: var(--text); border-color: var(--line); }
.chip-active, .chip[aria-pressed='true'] {
  background: var(--accent-dim); color: var(--text); border-color: var(--accent);
}
.chip-count { color: var(--text-mute); font-variant-numeric: tabular-nums; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 40px; padding: 0 16px;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--radius);
  font: inherit; font-size: 14px; font-weight: 500; cursor: pointer;
  transition-property: transform, background-color, border-color;
  transition-duration: var(--fast);
  transition-timing-function: ease-out;
}
.btn:hover { border-color: var(--text); background: var(--ink-wash); }

.back-link {
  display: inline-flex; align-items: center; gap: 5px;
  min-height: 40px; color: var(--text-mute); font-size: 13.5px;
  transition-property: color; transition-duration: var(--fast);
}
.back-link:hover { color: var(--text); }
.back-icon { width: 14px; height: 14px; }

/* --------------------------------------------------------------- badges */

.badge {
  display: inline-flex; align-items: center;
  padding: 1px 7px; border-radius: var(--radius-sm);
  font-size: 11px; font-weight: 500; letter-spacing: 0.01em;
  border: 1px solid var(--line); color: var(--text-dim);
  white-space: nowrap;
}
.badge-muted { color: var(--void); border-color: var(--line-soft); }
.badge-gold {
  color: var(--warn);
  border-color: color-mix(in oklab, var(--warn) 40%, transparent);
  background: color-mix(in oklab, var(--warn) 10%, transparent);
}

.tag {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 7px; border-radius: var(--radius-sm);
  background: var(--surface-2); color: var(--text-dim);
  font-size: 11.5px; font-weight: 500; white-space: nowrap;
}

/* -------------------------------------------------------------- avatars */

.avatar {
  --avatar-size: 32px;
  position: relative; flex: none;
  display: inline-grid; place-items: center;
  width: var(--avatar-size); height: var(--avatar-size);
  border-radius: 50%; overflow: hidden;
  background: var(--surface-2); color: var(--text-mute);
  font-size: calc(var(--avatar-size) * 0.4); font-weight: 600;
  user-select: none;
}
.avatar-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  opacity: 0; outline: none;
  transition-property: opacity; transition-duration: 200ms;
}
.avatar-img.is-loaded { opacity: 1; }

.entity { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.entity-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  transition-property: color; transition-duration: var(--fast);
}
a.entity:hover .entity-name { color: var(--accent); }

.face-pile { display: flex; align-items: center; }
.face-pile .avatar { margin-left: -6px; box-shadow: 0 0 0 2px var(--surface); }
.face-pile .avatar:first-child { margin-left: 0; }
.face-more {
  margin-left: -6px; display: inline-grid; place-items: center;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--surface-2); box-shadow: 0 0 0 2px var(--surface);
  font-size: 10px; color: var(--text-mute); font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------- tables */

.table-wrap { overflow-x: auto; }

.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th {
  position: sticky; top: 0; z-index: 1;
  padding: 8px var(--gap-sm);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  color: var(--text-mute); font-size: 11px; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase; text-align: left;
  white-space: nowrap;
}
.table td { padding: 9px var(--gap-sm); border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
.table tbody tr {
  transition-property: background-color; transition-duration: var(--fast);
}
.table tbody tr:hover { background: var(--ink-wash); }
.table tbody tr:last-child td { border-bottom: none; }

/* ------------------------------------------------------------ stat tiles */

.stats {
  display: grid; gap: var(--gap-sm);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin-bottom: var(--gap);
}
.stat {
  padding: 11px 13px;
  background: var(--surface); border: 1px solid var(--line-soft);
  border-radius: var(--radius);
}
.stat-label {
  color: var(--text-mute); font-size: 10.5px; font-weight: 600;
  letter-spacing: 0.07em; text-transform: uppercase;
}
/* Size, face and tracking come from .figure — a stat value is a measurement,
 * so it speaks in the measurement voice. Only the spacing lives here. */
.stat-value { margin-top: 5px; }
.stat-sub { color: var(--text-mute); font-size: 12px; margin-top: 1px; }

/* --------------------------------------------------------------- states */

.state { padding: var(--gap-xl) var(--gap); text-align: center; color: var(--text-mute); }
.state-title { color: var(--text-dim); font-weight: 500; }
.state-detail { font-size: 13px; margin-top: 5px; max-width: 52ch; margin-inline: auto; }
.state-loading { display: flex; align-items: center; justify-content: center; gap: var(--gap-sm); }
.state-error .state-title { color: var(--danger); }
.state-error .btn { margin-top: var(--gap); }

.spinner {
  width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--accent);
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.sk { background: var(--surface-2); border-radius: var(--radius-sm); animation: sk 1.4s ease-in-out infinite; }
.sk-line { height: 11px; }
.w40 { width: 40%; } .w60 { width: 60%; } .w80 { width: 80%; }
@keyframes sk { 0%, 100% { opacity: 0.5; } 50% { opacity: 0.9; } }

/* --------------------------------------------------------------- footer */

.footer {
  margin-top: var(--gap-xl);
  padding-top: var(--gap-lg);
  background: none;
  border-top: 1px solid var(--rule);
  color: var(--text-mute); font-size: var(--t-fine);
}
.footer-inner { padding-bottom: var(--gap-xl); }
.footer p + p { margin-top: 6px; }
.footer-note { max-width: 68ch; }
.footer-mark { display: block; margin-bottom: var(--gap); opacity: 0.75; }

/* The tracking line is a measurement, so it is set like one. */
#footer-meta {
  font-family: var(--mono); font-size: var(--t-micro);
  letter-spacing: 0.10em; text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}

.footer-link {
  color: var(--accent); margin-left: 6px;
  transition-property: color; transition-duration: var(--fast);
}
.footer-link:hover { text-decoration: underline; text-underline-offset: 2px; }
.footer-dim { color: var(--text-mute); opacity: 0.8; }

/* ------------------------------------------------------------- meta rows */

/* A facts table with dotted leaders — the detail that makes this read as an
 * index page rather than a settings panel. The leader is a pseudo-element on
 * the ROW (not on the label: the label is `position: relative` itself, so a
 * pseudo anchored there would only span the label). Label and value carry the
 * page ground and sit above it, so dots show only in the gap between them, and
 * a long wrapping value simply leaves no gap to fill. */
.meta-row {
  position: relative;
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--gap); padding: 8px 0;
  border-bottom: 1px solid var(--rule-faint);
}
.meta-row::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0;
  height: calc(100% - 8px - 0.32em);
  border-bottom: 1px dotted var(--rule-faint);
  z-index: 0; pointer-events: none;
}
.meta-row:last-child { border-bottom: none; }
.meta-label, .meta-value {
  position: relative; z-index: 1; background: var(--bg);
}
.meta-label { color: var(--text-mute); font-size: var(--t-small); margin: 0; padding-inline: 0 6px; }
.meta-value {
  margin: 0; text-align: right; padding-inline: 6px 0;
  font-family: var(--mono); font-size: var(--t-small);
}

@media (max-width: 560px) {
  :root { --gap-xl: 28px; }
  .nav-link { padding: 0 10px; font-size: 13px; }
  .brand-text { display: none; }
}
