﻿/* View-level composition. Primitives and tokens live in style.css. */

/* ---------------------------------------------------- shared link colour */

.room-link { transition-property: color; transition-duration: var(--fast); }
.room-link:hover { color: var(--accent); }

/* ------------------------------------------------------------- time spine */

.gantt { display: flex; flex-direction: column; gap: var(--gap-sm); }

.gantt-legend {
  display: flex; flex-wrap: wrap; gap: var(--gap);
  font-size: 11.5px; color: var(--text-mute);
}
.gantt-key { display: inline-flex; align-items: center; gap: 6px; }
.gantt-key::before {
  content: ''; width: 16px; height: 8px; border-radius: var(--radius-sm);
  background: var(--text-mute);
}
.gantt-key-solid::before { background: var(--accent); }
.gantt-key-fade::before {
  background: linear-gradient(90deg, var(--accent), transparent);
}
.gantt-key-muted::before { background: var(--accent); opacity: 0.4; }

/* Wide charts scroll inside this wrapper; the page body never scrolls. */
.gantt-scroll { overflow-x: auto; }
.gantt-svg { width: 100%; min-width: 640px; height: auto; display: block; }

/* ---------------------------------------------------- ONE AXIS TREATMENT
 *
 * gantt, heatmap, rhythm and chart each used to invent their own grid weight,
 * tick colour and corner radius. They are one instrument — the shared time
 * spine — so they get one hairline weight, one tick voice and one cell corner.
 * Anything measured is set in the mono face, including axis labels. */

.gantt-grid-line, .chart-svg .grid, .rhythm-tick {
  stroke: var(--rule-faint); stroke-width: 0.5;
}
.gantt-axis, .rhythm-axis { stroke: var(--rule); stroke-width: 1; }
.gantt-tick, .axis-label, .rhythm-axis-label, .hm-label {
  fill: var(--text-mute);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}

/* The lane is a rule, not a slab. */
.gantt-track { fill: none; }
.gantt-lane-rule { stroke: var(--rule-faint); stroke-width: 0.5; }
.gantt-user-label { fill: var(--text-dim); font-size: 11.5px; }
.gantt-bar { cursor: default; }
.gantt-bar-low { stroke: var(--line); stroke-dasharray: 2 2; stroke-width: 1; }

/* "Now" is the one live element in the whole system, and it is green because
 * --live means observed. Semantics unchanged. */
.gantt-now-line { stroke: var(--live); stroke-width: 1.5; stroke-dasharray: 3 3; }
.gantt-now-label {
  fill: var(--live); font-family: var(--mono); font-size: 9.5px;
  letter-spacing: 0.10em; text-transform: uppercase;
}

.gantt-empty {
  padding: var(--gap-lg); text-align: center;
  color: var(--text-mute); font-size: 13px;
}

/* -------------------------------------------------------------- coverage
 *
 * The headline figure is deliberately large. It is a claim about how much this
 * site can be trusted, and a claim like that should not be set in 12px. */

.coverage-headline {
  display: flex; align-items: center; gap: var(--gap);
  flex-wrap: wrap; margin-bottom: var(--gap-sm);
}
/* Size, face and tracking come from .figure. The grading colour is semantic and
 * stays: rendering 0.7% in the same green as 99% would make a bad number look
 * like a good one, which is the exact failure this page exists to prevent. */
.coverage-figure { color: var(--text-mute); }
.coverage-figure.is-good { color: var(--live); }
.coverage-figure.is-fair { color: var(--text); }
.coverage-figure.is-poor { color: var(--warn); }
.coverage-claim { font-size: 15px; }
.coverage-sub { color: var(--text-mute); font-size: 13px; margin-top: 2px; }

.coverage-track {
  height: 6px; border-radius: var(--radius-pill);
  background: var(--surface-2); overflow: hidden; margin-bottom: var(--gap);
}
.coverage-fill {
  width: var(--fill, 0%); height: 100%;
  /* A very low figure still needs a visible sliver, or the bar reads as broken
   * rather than as an honest report of almost no coverage. */
  min-width: 3px;
  background: var(--text-mute); border-radius: inherit;
  transition-property: width; transition-duration: 500ms;
  transition-timing-function: var(--ease);
}
.coverage-fill.is-good { background: var(--live); }
.coverage-fill.is-fair { background: var(--accent); }
.coverage-fill.is-poor { background: var(--warn); }

.plain-list {
  margin: 0; padding-left: 1.1em;
  color: var(--text-dim); font-size: 13.5px;
}
.plain-list li { margin-bottom: 5px; text-wrap: pretty; }

/* ---------------------------------------------------------------- charts */

.chart-body { position: relative; }
.chart-svg { width: 100%; height: auto; display: block; }

/* chart.js puts the series class on the ELEMENT being drawn, so .series-* must
 * only ever set `color`. Setting background on it painted the whole legend
 * chip, because the legend item carries the series class too. Everything
 * visible derives from currentColor. */
.series-a { color: var(--accent); }
.series-b { color: var(--live); }

.chart-svg .line { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; }
.chart-svg .area { fill: currentColor; opacity: 0.14; stroke: none; }
.chart-svg .dot, .chart-svg .cursor-dot { fill: currentColor; }
.chart-svg .cursor { stroke: none; }

/* .chart-overlay is an SVG <rect> used as the hover hit-target, not a DOM
 * layer. Without an explicit fill it paints solid black over the whole plot â€”
 * SVG's default fill is black, and `position: absolute` means nothing here. It
 * must stay hit-testable, so `transparent`, never `fill: none`. */
.chart-svg .chart-overlay { fill: transparent; }

.chart-legend {
  display: flex; gap: var(--gap); font-size: 11.5px; color: var(--text-mute);
}
.legend-item { display: inline-flex; align-items: center; gap: 5px; }
.legend-swatch {
  display: inline-block; width: 12px; height: 3px; border-radius: var(--radius-sm);
  background: currentColor; flex: none;
}

/* ------------------------------------------------------------ detail head */

.detail-head {
  display: flex; align-items: flex-start; gap: var(--gap);
  flex-wrap: wrap; margin-bottom: var(--gap);
}
.detail-title { font-size: clamp(19px, 1rem + 1.2vw, 24px); letter-spacing: -0.02em; }
.detail-sub {
  display: flex; align-items: center; gap: var(--gap-sm); flex-wrap: wrap;
  margin-top: 4px; color: var(--text-mute); font-size: 13px;
}
.detail-actions { margin-left: auto; display: flex; gap: var(--gap-sm); }

.split {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}

.id-code {
  font-family: var(--mono); font-size: 12px;
  color: var(--text-mute); word-break: break-all;
}

/* --------------------------------------------------------------- session */

.dur { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.dur-live { color: var(--live); font-weight: 550; }
.dur-sub { color: var(--text-mute); font-size: 12px; }
/* "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 "not a value" while staying legible.
 * The old near-invisible grey rendered these words at 2.0:1. */
.dur-void {
  color: var(--void);
  font-style: italic;
  text-decoration: underline dotted var(--void) 1px;
  text-underline-offset: 3px;
}
.dur-soft { color: var(--text-dim); }

/* -------------------------------------------------------------- leaderboard */

.rank {
  display: inline-grid; place-items: center;
  min-width: 24px; height: 24px; padding: 0 6px;
  border-radius: var(--radius-sm); background: var(--surface-2);
  color: var(--text-mute); font-size: 12px; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.rank-1 { background: color-mix(in oklab, var(--warn) 22%, transparent); color: var(--warn); }
.rank-2, .rank-3 { color: var(--text-dim); }


/* ------------------------------------------------------------------ misc */

.note {
  margin-top: var(--gap); padding-top: var(--gap);
  border-top: 1px solid var(--line-soft);
  color: var(--text-mute); font-size: 12px;
}

.hint {
  color: var(--text-mute); font-size: 12.5px;
}
.hint:empty { display: none; }

/* --------------------------------------------------- detail-page surfaces
 *
 * Room and person pages compose from these. They are the same surface as
 * .panel, kept under a separate name because the detail views stack many of
 * them and the stacking rhythm belongs with the card, not the page. */

.card {
  display: block;
  padding: var(--gap);
  background: var(--surface);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
}
.card + .card { margin-top: var(--gap); }

/* ------------------------------------------------------------------- leaf
 *
 * The replacement for the nine identical rounded boxes. A leaf is a band on a
 * ruled page: no background, no border, no radius, no shadow — a 1px rule above
 * it and a margin rail carrying its label. Structure comes from the rule and
 * from the shared vertical grid, never from a container. */

.leaf {
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr);
  gap: 0 var(--gap-lg);
  padding-block: var(--gap-lg);
  border-top: 1px solid var(--rule);
}
.leaf:first-of-type { border-top: none; padding-top: var(--gap); }
.leaf-rail { grid-column: 1; }
.leaf-body { grid-column: 2; min-width: 0; }

.leaf-label { margin-bottom: 6px; text-wrap: balance; }
.leaf-note { font-size: var(--t-fine); color: var(--text-mute); text-wrap: pretty; }

@media (max-width: 900px) {
  /* One column, and BOTH children must return to auto placement. Leaving
   * .leaf-body pinned to `grid-column: 2` here creates an implicit second track
   * and squeezes the rail to 0px, which silently crushes the section label. */
  .leaf { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .leaf-rail, .leaf-body { grid-column: auto; }
  .leaf-rail { margin-bottom: var(--gap); }
}

.card-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--gap); flex-wrap: wrap;
}

.detail-head-main { min-width: 0; }
.detail-head-side { margin-left: auto; }

.chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
/* Inside .chips these are labels, not controls â€” no pointer, no hover lift. */
.chips .chip { cursor: default; }
.chips .chip:active { transform: none; }
.chip-lang { color: var(--text); }
.chip-lock { color: var(--warn); }

.badge-live {
  color: var(--live);
  border-color: color-mix(in oklab, var(--live) 45%, transparent);
  background: color-mix(in oklab, var(--live) 12%, transparent);
}

.meta-grid { margin: 0; }
.mono, .infinity { font-family: var(--mono); font-size: 12.5px; }
.infinity { font-size: 15px; }

.ext-link { color: var(--accent); }
.ext-link:hover { text-decoration: underline; text-underline-offset: 2px; }
.ext-arrow { margin-left: 3px; font-size: 11px; opacity: 0.7; }

.cell-user { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.cell-clip { max-width: 26ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; }
.dot-sep { color: var(--text-mute); }

.occ { display: inline-flex; align-items: baseline; gap: 1px; font-size: 15px; font-weight: 600; }
.occ-sep, .occ-cap { color: var(--text-mute); font-weight: 450; }
.occ-empty { color: var(--text-mute); }
.occ-full { color: var(--warn); }

/* --------------------------------------------------------------- profile */

.profile-head { display: flex; align-items: center; gap: var(--gap); flex-wrap: wrap; }
/* .profile-id is the WRAPPER around the display name, not the id itself â€” only
 * .profile-uid holds the id. Styling the wrapper as mono inherited into the
 * <h1> and made real people's names look like placeholder text. */
.profile-id { min-width: 0; }
.profile-uid { color: var(--text-mute); font-family: var(--mono); font-size: 12px; }
.profile-bio {
  margin-top: 6px; color: var(--text-dim); font-size: 13.5px;
  max-width: 68ch; text-wrap: pretty;
}

/* ------------------------------------------------------ presence headline
 *
 * The first thing on a person page and the answer they came for. It gets real
 * scale â€” this is a statement, not a status field. */

/* The loudest thing on the page. Full-width rules above and below it — a band,
 * not a callout box — because this sentence is the answer the reader came for. */
.presence {
  display: flex; align-items: center; gap: var(--gap);
  flex-wrap: wrap;
  padding-block: var(--gap);
  margin-bottom: var(--gap);
  border: none;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  border-radius: 0;
  background: none;
}
/* Colour still carries the state, but as a marker in the rule rather than a
 * wash across a card: --live observed, --warn uncertain, --void unknown. */
.presence.is-online { border-top-color: var(--live); }
.presence.is-unknown { border-top-color: var(--warn); }

.presence-main { flex: 1 1 320px; min-width: 0; }

.presence-state { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; }
.presence-label {
  color: var(--text-mute); font-size: 10.5px; font-weight: 600;
  letter-spacing: 0.07em; text-transform: uppercase;
}

.presence-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--void); }
.presence-dot.is-online {
  background: var(--live);
  animation: pulse 2.4s ease-out infinite;
}
.presence-dot.is-unknown { background: var(--warn); }

/* Deliberately larger than body text. The sentence IS the page. */
.presence-line {
  font-size: 19px; line-height: 1.45; text-wrap: pretty;
}
.presence-line strong { font-weight: 650; font-variant-numeric: tabular-nums; }
/* The measured part of the sentence, set inline in the measurement voice. */
.presence-line strong.figure { font-weight: 500; }
.presence-line .room-link { color: var(--accent); }
.presence-line .room-link:hover { text-decoration: underline; text-underline-offset: 2px; }

/* Companion links sit inside running text, so they lose the avatar's block
 * rhythm and align to the baseline instead. */
.entity-inline { display: inline-flex; vertical-align: baseline; gap: 5px; }
.entity-inline .entity-name { font-weight: 550; }
.entity-inline:hover .entity-name { color: var(--accent); }

.presence-caveat {
  margin-top: 6px; color: var(--warn); font-size: 12.5px;
}

.btn-go {
  background: var(--accent); color: var(--bg);
  border-color: transparent; font-weight: 600;
}
.btn-go:hover { filter: brightness(1.08); border-color: transparent; }

/* -------------------------------------------------------------- partners */

.partner-list { display: flex; flex-direction: column; gap: 6px; }
/* The detail column carries "4h 12m together Â· 12 times", so it needs real
 * width â€” the old 90px bar with a bare count no longer fits the content. */
.partner {
  display: grid; grid-template-columns: minmax(0, 1fr) 120px minmax(140px, auto);
  align-items: center; gap: var(--gap);
}
@media (max-width: 620px) {
  .partner { grid-template-columns: minmax(0, 1fr) auto; }
  .partner-bar { grid-column: 1 / -1; order: 3; }
}
/* Both are <span>s carrying an inline width â€” they must be block-level or the
 * width is ignored and the bar renders as an empty rail. */
.partner-bar {
  display: block; height: 4px; border-radius: var(--radius-pill);
  background: var(--surface-2); overflow: hidden;
}
.partner-fill {
  display: block; height: 100%; min-width: 3px;
  background: var(--accent); border-radius: inherit;
}
.partner-count {
  color: var(--text-dim); font-size: 12.5px;
  font-variant-numeric: tabular-nums; text-align: right; cursor: help;
}
/* An approximate total must not look as solid as a measured one. */
.partner-fill.is-uncertain {
  background: repeating-linear-gradient(
    90deg, var(--accent) 0 5px, transparent 5px 8px
  );
}

/* -------------------------------------------------------- legacy layout */

.grid, .stats-grid {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.stat-strip {
  display: grid; gap: var(--gap-sm);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  margin-bottom: var(--gap);
}

.room-grid { display: flex; flex-direction: column; gap: 6px; }
.skeleton-card {
  padding: 14px var(--gap);
  background: var(--surface); border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  display: flex; flex-direction: column; gap: 7px;
}

.seg-group { display: inline-flex; padding: 0; gap: 0; background: transparent; }

.change-field { color: var(--text-dim); font-weight: 500; }
.cell-to { color: var(--text-mute); }
.caveat-mark { color: var(--warn); cursor: help; }
.note-caution { border-top-color: color-mix(in oklab, var(--warn) 30%, transparent); }

/* ---------------------------------------------------------- chart extras */

.chart-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--gap); flex-wrap: wrap; }
.chart-caption { color: var(--text-mute); font-size: 12px; margin-top: var(--gap-sm); }
.chart-plot { position: relative; }
.chart-readout {
  position: absolute; top: 0; padding: 6px 9px;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: var(--radius); font-size: 12px; white-space: nowrap;
  box-shadow: var(--shadow);
}
.readout-time { color: var(--text-mute); }
.readout-val { font-variant-numeric: tabular-nums; }
.readout-dim { color: var(--text-mute); }
.cursor-line { stroke: var(--text-mute); stroke-width: 1; stroke-dasharray: 3 3; }




.check-list { display: flex; flex-direction: column; gap: 6px; }
.check-mark { color: var(--live); }
.check-name { font-weight: 500; }
.check-detail { color: var(--text-mute); font-size: 12.5px; }

/* ------------------------------------------------------------- follow-in */

.followin-group + .followin-group { margin-top: var(--gap); }
.followin-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
/* Wraps rather than truncates: the sentence is the point, not the name. */
.followin-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--gap-sm); }
.followin-detail { color: var(--text-dim); font-size: 12.5px; }

/* ------------------------------------------------- rhythm and catch window
 *
 * The sentence is the product and gets presence-block scale; the heatmap is
 * evidence and stays quiet underneath it.
 *
 * The three cell states are load-bearing, not decorative:
 *   .is-seen       accent ink, opacity carries the rate
 *   .is-empty      flat surface â€” a MEASURED zero
 *   .is-unwatched  hatching â€” no measurement exists
 * Drawing the third like the second would report our own downtime as this
 * person's absence, which is the same class of lie as `--void` for truncated
 * durations. They must stay visually distinct in both themes. */

.catch-line {
  font-size: 16px; line-height: 1.45; text-wrap: pretty; margin-top: var(--gap-sm);
}
.catch-line strong { font-weight: 650; font-variant-numeric: tabular-nums; }
.catch-line.is-waiting { color: var(--text-dim); font-weight: 550; }

.catch-detail {
  margin-top: 4px; color: var(--text-mute); font-size: 12.5px;
  font-variant-numeric: tabular-nums; text-wrap: pretty;
}

.rhythm-fact {
  margin-top: 6px; color: var(--text-dim); font-size: 13px; text-wrap: pretty;
}

.rhythm-progress {
  margin-top: var(--gap-sm); height: 4px; border-radius: var(--radius-pill);
  background: var(--surface-2); overflow: hidden;
}
.rhythm-progress-fill { height: 100%; background: var(--accent-dim); border-radius: inherit; }

.rhythm-map { margin-top: var(--gap); }
/* The grid keeps its pixel size and scrolls inside this box, so the page body
 * never scrolls sideways on a phone. */
.rhythm-scroll { overflow-x: auto; }
.rhythm-svg { display: block; width: 100%; min-width: 534px; height: auto; }

.rhythm-cell { stroke: var(--bg); stroke-width: 0.5; }
.rhythm-cell.is-seen { fill: var(--accent); }
.rhythm-cell.is-empty { fill: var(--surface-2); }
.rhythm-cell.is-unwatched { stroke: var(--line-soft); }

.rhythm-hatch-bg { fill: var(--surface); }
.rhythm-hatch-line { stroke: var(--void); stroke-width: 1.4; opacity: 0.45; }

/* Ties the sentence to the chart: the same claim, drawn twice. */
.rhythm-window { fill: none; stroke: var(--live); stroke-width: 1.5; opacity: 0.9; }

.rhythm-day-label { fill: var(--text-dim); font-size: 11px; }

.rhythm-legend {
  display: flex; flex-wrap: wrap; gap: var(--gap); margin-top: var(--gap-sm);
  color: var(--text-mute); font-size: 11.5px;
}
.rhythm-key { display: inline-flex; align-items: center; gap: 6px; }
.rhythm-key::before {
  content: ''; width: 11px; height: 11px; border-radius: var(--radius-sm);
  border: 1px solid var(--line-soft);
}
.rhythm-key.is-seen::before { background: var(--accent); border-color: transparent; }
.rhythm-key.is-empty::before { background: var(--surface-2); }
.rhythm-key.is-unwatched::before {
  background: repeating-linear-gradient(45deg, var(--void) 0 1.5px, var(--surface) 1.5px 5px);
}

.rhythm-tznote { margin-top: 6px; color: var(--text-mute); font-size: 11.5px; }

/* ============================================================================
 * HOME — the standing table
 *
 * There is no hero, no wide input and no Users/Rooms toggle here any more.
 * Search is a header capability (style.css, .hdr-*), and the landing page is
 * the one thing our data can say that a scrape of the live site cannot: which
 * pairs are together more than coincidence explains.
 * ========================================================================== */

/* The board heading carries the page. It is an <h1> on #/ and an <h2> when a
 * results page sits above it, so the scale comes from the class, not the tag. */
.board-title { font-size: var(--t-h2); letter-spacing: -0.025em; }

/* The masthead line. Mono small-caps, and every field in it is a measurement. */
.colophon {
  margin-top: var(--gap);
  display: flex; flex-wrap: wrap; align-items: baseline;
  font-family: var(--mono);
  font-size: var(--t-micro);
  font-weight: 500;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--text-mute);
}
.colophon:empty { display: none; }
.colophon-part { display: inline-flex; align-items: baseline; }
.colophon-sep { padding-inline: 8px; color: var(--rule); }



/* Ledger tabs, not a filled pill. Emphasis is a 2px rule and weight — never a
 * fill, which is what carried the old dark-mode contrast failure. */
.seg {
  position: relative;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 34px; padding: 0 2px;
  margin-right: var(--gap-lg);
  background: transparent; border: none; border-radius: 0;
  color: var(--text-mute); font: inherit; font-size: var(--t-small); font-weight: 500;
  cursor: pointer;
  transition-property: color;
  transition-duration: var(--fast);
}
.seg:last-child { margin-right: 0; }
.seg:hover { color: var(--text); }
.seg.is-active { background: transparent; color: var(--text); font-weight: 650; }
/* The underline is the active state, and it is not colour alone — the weight
 * and the ink both change with it. */
.seg.is-active::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 2px;
  height: 2px; background: var(--text);
}


/* --------------------------------------------------------- results page
 *
 * `#/?q=` and `#/search?q=` render this above the board. Both kinds at once,
 * every match, each group labelled — the mode toggle that used to decide which
 * half you were allowed to see is gone. */

.results-section { margin-bottom: var(--gap-xl); }
.search-results:empty { display: none; }
.search-results { margin-top: var(--gap); max-width: 760px; }

.result-group + .result-group { margin-top: var(--gap-lg); }
.result-group-label { display: block; margin-bottom: 6px; }

/* Ruled rows, not a floating list — the same object as the duo ledger and the
 * header popover, so the same thing looks the same wherever it appears. */
.result-list { border-top: 1px solid var(--rule); }

.result {
  display: flex; align-items: center; gap: var(--gap);
  padding: 12px 6px;
  border-bottom: 1px solid var(--rule-faint);
  transition-property: background-color; transition-duration: var(--fast);
}
.result:last-child { border-bottom: none; }
.result:hover { background: var(--ink-wash); }
.result-body { min-width: 0; flex: 1; }
.result-name {
  font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.result-id { color: var(--text-mute); font-family: var(--mono); font-size: 11.5px; }
.result-side { text-align: right; font-size: 12.5px; white-space: nowrap; }

.dim { color: var(--text-mute); }

/* ------------------------------------------------------------ duo ranking */

.ranked-section { margin-top: var(--gap-xl); }
/* On `#/` the board is the page, so it starts at the top of the column. The
 * margin above only exists to separate it from a results page. */
.ranked-section:first-child { margin-top: 0; }

.dot-badge {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: var(--warn);
}
.count-pair strong { font-weight: 650; font-variant-numeric: tabular-nums; }

.duo-controls {
  margin-top: var(--gap);
  display: flex; flex-wrap: wrap; gap: var(--gap-sm) var(--gap);
  align-items: center;
}
/* Compact variant: these sit under a heading, not in the hero, so they must not
   compete with the search toggle above them. Same ledger language, tighter. */
.seg-sm .seg {
  min-height: 26px; padding: 0 1px;
  margin-right: var(--gap);
  font-size: var(--t-fine); font-weight: 500;
}
.seg-sm .seg.is-active { font-weight: 650; }
.seg-sm .seg.is-active::after { bottom: 0; height: 1.5px; }

/* One ruled column, not a card grid. The rank sits in a margin column so the
 * board shares its left edge with the leaf rails on every other page. */
.duo-ledger {
  margin-top: var(--gap-lg);
  border-top: 1px solid var(--rule);
}

.duo-row {
  display: grid;
  grid-template-columns: 40px auto minmax(0, 1fr) minmax(180px, auto);
  align-items: center;
  gap: var(--gap);
  padding: 12px 6px;
  border-bottom: 1px solid var(--rule-faint);
  /* Hover is a wash, not a lift. Nothing on this page floats. */
  transition-property: background-color;
  transition-duration: var(--fast);
}
.duo-row:hover { background: var(--ink-wash); }

/* Two faces, overlapped — the pair reads as one unit before any text is read. */
.duo-faces { display: flex; flex: none; }
.duo-faces > :nth-child(2) {
  /* Enough overlap to read as one unit, little enough that an initials-only
     fallback on the first face is still legible under it. */
  margin-left: -10px;
  outline: 2px solid var(--surface);
  border-radius: 50%;
}

.duo-body { min-width: 0; flex: 1; }
.duo-names {
  display: flex; align-items: baseline; gap: 6px;
  min-width: 0;
}
.duo-name {
  font-size: 15px; font-weight: 650; letter-spacing: -0.01em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: 45%;
}
.duo-name:hover { color: var(--accent); }
.duo-amp { color: var(--text-mute); font-size: 13px; flex: none; }
.duo-meta { margin-top: 3px; font-size: 12.5px; }

.duo-live {
  margin-top: 6px;
  display: flex; align-items: center; gap: 6px;
  min-width: 0; max-width: 100%;
  font-size: 12.5px; color: var(--live);
}
.duo-live:hover { text-decoration: underline; }
/* Same fact, no present tense. `--live` means observed AND current; a collector
 * that stopped two days ago has observed nothing since, so the row drops to
 * --warn and the copy drops "right now". This is UI_SPEC.md §6's last bullet,
 * which this board did not enforce until now. */
.duo-live.is-stale { color: var(--warn); }
/* A room topic is free text up to a couple of hundred characters — one line,
   cut off, with the whole thing in the title. */
.duo-live-room { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The figure column. The loud number here is the one the list is SORTED by. */
.duo-figure {
  display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
  text-align: right; font-size: var(--t-fine); white-space: nowrap;
}
.duo-time strong { font-variant-numeric: tabular-nums; }
/* The index number, in the margin column, set like a table of contents. */
.duo-rank {
  color: var(--text-mute);
  font-family: var(--mono); font-size: var(--t-fine);
  font-variant-numeric: tabular-nums;
  text-align: left;
}
