/* **No row on this page is ever given a fill.** Every row sits on the page's own
   dark, on every client, and the colour a client published becomes the colour of
   that row's TEXT -- `lobby.js` writes it onto the name, through `legible`.
   GG's banner rows were the one exception until 2026-08-26 and are not any more:
   a fill is the loudest thing on a row, louder than the countdown this table
   exists for, and it made one reader's rows read as a different KIND of thing
   from the other eight's.

   The palette moved off the old blue/GitHub-grey set on 2026-08-26. Two things
   about it are not taste:

   - **The countdown ramp is jade -> amber -> red**, interpolated in HSL. The old
     one ran blue -> orange, which is grey at its midpoint -- i.e. at exactly the
     twenty minutes the column exists for. The stops live in `lobby.js`
     (`URGENCY`); these variables are the same three colours for everything else
     on the page, and the two have to be edited together.
   - **Nothing here may give a row a fill**, which is now the whole rule rather
     than the contrast argument it used to be: every name is checked at 5:1
     against one known background, so a rule that paints a row breaks the one
     assumption that check rests on. The hover tint and the alarm frame are the
     two exceptions, and both are drawn where there is no text to make
     illegible. */

:root {
  --bg: #0b0d0c;
  --panel: #141817;
  --raised: #1c2120;
  --line: #272e2c;
  --line-soft: #1c2321;
  --ink: #e9edea;
  --ink-2: #b6bfbb;
  --dim: #7f8a86;
  --faint: #4a5350;
  --accent: #4fd39b;
  --good: #4fd39b;
  --warn: #e8b34a;
  --bad: #ef6a5a;

  --ui: "Space Grotesk", "Segoe UI", system-ui, sans-serif;
  --text: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
  --num: "IBM Plex Mono", Consolas, ui-monospace, monospace;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 400 13.5px/1.45 var(--text);
  -webkit-font-smoothing: antialiased;
}

/* Every number on this page is tabular. The Late reg countdown redraws once a
   second, and proportional digits make the whole column twitch as it does. */
.mono, .num, td.late, .cd, .at, .age, .bar-meta {
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
}

/* A label, not a value: section heads, column heads, the wordmark. */
.lbl {
  font-family: var(--ui);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dim);
}

/* ── the top bar ───────────────────────────────────────────────────────
   Two tiers: who and how fresh on top, the filters under it. They used to
   share one baseline-aligned flex row with the alarm control, which is what
   left the sticky header measuring itself against a bar that wrapped. */

.bar {
  display: flex;
  align-items: center;
  gap: 16px 20px;
  padding: 13px 24px 12px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  position: sticky;
  top: 0;
  z-index: 3;
  /* NOWRAP, and it is what makes the rule on `.bar-meta` below work at all.
     A wrapping flex container breaks the LINE before it shrinks anything on it,
     so `flex-shrink` on the pill group never got a chance -- the browser folded
     the alarm control away instead, every time. With nowrap the pills are the
     only item that can give, which is exactly the one that should. */
  flex-wrap: nowrap;
}

.brand { display: flex; align-items: center; gap: 10px; }
.bar h1 {
  font-family: var(--ui);
  font-size: 17px;
  font-weight: 700;
  margin: 0;
  letter-spacing: .18em;
  text-transform: uppercase;
}
/* The live dot. It says the hub answered, nothing more -- how old the READING
   is, is the job of the age pills beside it. */
.pulse-dot {
  width: 6px; height: 6px; border-radius: 999px;
  background: var(--good);
  box-shadow: 0 0 10px rgba(79, 211, 155, .7);
}
.pulse-dot.stale { background: var(--warn); box-shadow: 0 0 10px rgba(232, 179, 74, .6); }
.pulse-dot.dead { background: var(--bad); box-shadow: none; }

/* The admin marker. Drawn only when the hub said this caller may sweep, so a
   viewer who cannot start a process on this machine never sees a hint of one. */
.adminflag {
  border: 1px solid rgba(232, 179, 74, .5);
  color: var(--warn);
  border-radius: 5px;
  padding: 2px 8px;
  font-family: var(--ui);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
}

/* The age pills are the group that gives way, and that is deliberate.
   Everything on this bar is wider than a 1280px window can hold once nine
   readers have rows -- measured at 1506px of content -- so something has to
   fold. Left alone, flex-wrap folded the ALARM control onto a second line and
   left the pills whole, which moves the one control on this bar onto a row of
   its own. So the pill group shrinks and wraps INSIDE itself instead: the
   wordmark, the tabs and the alarm keep their places at every width, and the
   readout below them gets as many rows as it needs. */
.bar-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  /* SHRINK, never grow. `1 1 auto` let it eat the bar's free space instead of
     giving any up, so the alarm control still folded -- the opposite of the
     point. `0 1 auto` keeps its content width when there is room and is the
     first thing to give when there is not. */
  flex: 0 1 auto;
  /* A flex item's automatic minimum is its CONTENT, which for a row of pills is
     the whole row -- so without this it cannot shrink even under nowrap. */
  min-width: 0;
}
.dim { color: var(--dim); }
.stale { color: var(--warn); }

/* One pill per client: how old the newest row of THAT reader is. Nothing
   sweeps on a timer and there are nine readers, so "last sweep 4m ago" is
   routinely true of one client while another's rows on screen are hours old. */
.age {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  color: var(--ink-2);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 3px 8px;
  white-space: nowrap;
}
.age .dot { width: 5px; height: 5px; border-radius: 999px; background: var(--good); }
.age.warn { color: var(--warn); background: rgba(232, 179, 74, .08); border-color: rgba(232, 179, 74, .32); }
.age.warn .dot { background: var(--warn); }
.age.bad { color: var(--bad); background: rgba(239, 106, 90, .08); border-color: rgba(239, 106, 90, .32); }
.age.bad .dot { background: var(--bad); }

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  padding: 11px 24px 13px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}
/* A hairline between filter groups. Four controls in one row with only gap
   between them read as twenty loose things. */
.filters .rule { width: 1px; height: 22px; background: var(--line); }

#search {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 7px;
  color: var(--ink);
  padding: 6px 11px;
  min-width: 232px;
  font: inherit;
}
#search::placeholder { color: var(--dim); }
#search:focus { outline: 1px solid var(--accent); }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 1px solid var(--line);
  background: var(--raised);
  color: var(--ink-2);
  border-radius: 999px;
  padding: 5px 12px;
  font: inherit;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
}
.chip:hover { color: var(--ink); }
.chip.on { color: var(--ink); border-color: var(--accent); background: rgba(79, 211, 155, .10); }
.chip .n { font-family: var(--num); font-size: 11px; color: var(--dim); }
.chip.on .n { color: var(--accent); }
/* A category chip with nothing behind it is still drawn -- the four are fixed --
   but it says so, so an empty table after a click is never a surprise. */
.chip.none { opacity: .5; }
.chip.none.on { opacity: 1; }

/* The site picker. One button in the bar, the clients themselves in the popup
   it opens -- see `renderSites` for why the nine word chips that used to sit
   here went away. */
.sites { display: flex; }
.sites-btn { padding: 4px 9px 4px 11px; gap: 6px; }
.sites-btn-lbl { color: inherit; }
.sites-btn .caret { opacity: .55; flex: none; }
.sites-btn:hover .caret, .sites-btn.on .caret { opacity: 1; }
/* The selected clients, in the bar, as the same logos the popup draws. They sit
   on the client's own near-black, which is what every one of those files is
   painted on -- so the tile is black rather than the panel's grey. */
.sites-btn-logo {
  height: 15px;
  width: 30px;
  object-fit: contain;
  border-radius: 3px;
  background: #000;
  flex: none;
}
.sites-btn-word {
  font-size: 10.5px;
  letter-spacing: .02em;
  padding: 1px 5px;
  border-radius: 3px;
  background: #000;
  color: var(--ink-2);
}

.sitepop {
  position: fixed;
  z-index: 24;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px;
  box-shadow: 0 14px 34px rgba(0, 0, 0, .6);
}
.sitepop-title {
  margin: 2px 4px 8px;
  font-family: var(--ui);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--dim);
}
/* Four across. Nine clients is two rows and a bit at 96px a tile, which fits
   under the bar without the popup becoming a panel of its own. */
.sitepop-grid {
  display: grid;
  grid-template-columns: repeat(4, 96px);
  gap: 7px;
}
.sitetile {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--raised);
  font: inherit;
  color: var(--ink-2);
  cursor: pointer;
  text-align: left;
}
.sitetile:hover { border-color: var(--line); background: var(--bg); color: var(--ink); }
.sitetile.on {
  color: var(--ink);
  border-color: var(--accent);
  background: rgba(79, 211, 155, .10);
  box-shadow: inset 0 0 0 1px rgba(79, 211, 155, .28);
}
/* A client the current filters leave nothing of is still offered -- it is in
   the table -- but it says so, the way an empty category chip does. */
.sitetile.none { opacity: .45; }
.sitetile.none.on { opacity: 1; }
/* The logo's own black, at the 2:1 every file in `static/logos/` is cut to. */
.sitetile-art {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 2 / 1;
  border-radius: 5px;
  background: #000;
  overflow: hidden;
}
.sitetile-art img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* A client with no logo file. Its name, at the size the logos are drawn at. */
.sitetile-word {
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--ink-2);
  text-align: center;
  padding: 0 4px;
}
.sitetile-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 6px;
}
.sitetile-foot .nm {
  font-size: 11px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sitetile-foot .n {
  font-family: var(--num);
  font-size: 10.5px;
  color: var(--dim);
  flex: none;
}
.sitetile.on .sitetile-foot .n { color: var(--accent); }

.sitepop-all {
  width: 100%;
  margin-top: 8px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink-2);
  border-radius: 6px;
  padding: 5px 8px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.sitepop-all:hover:not(:disabled) { color: var(--ink); border-color: var(--accent); }
.sitepop-all:disabled { color: var(--faint); cursor: default; }

/* The buy-in range. Both bounds optional; an empty box is no bound. */
.range { color: var(--dim); display: flex; align-items: center; gap: 6px; }
.range input {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--ink);
  padding: 4px 8px;
  width: 62px;
  font-family: var(--num);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.range input:focus { outline: 1px solid var(--accent); }
.range .dash { opacity: .6; }


/* `!important`, and it is load-bearing. Every element this class hides also
   wears a class that SETS a display (`.sweeps` is a flex column, `.chips` a
   flex row, `.empty` a block) -- same specificity, so whichever rule is written
   LATER in the file wins. `.sweeps` sits hundreds of lines below this one, so
   the Sweeps tab rendered underneath the Lobby table while the Lobby tab was
   selected: the class was applied correctly and did nothing. */
.hidden { display: none !important; }

/* ── the alarm ─────────────────────────────────────────────────────── */

.lead {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 5px 10px 5px 9px;
  font-family: var(--ui);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dim);
}
.lead svg { flex: none; }
.lead input {
  width: 46px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 5px;
  color: var(--ink);
  padding: 2px 8px;
  font-family: var(--num);
  font-size: 13px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  text-align: right;
  letter-spacing: 0;
}
.lead input:focus { outline: 1px solid var(--accent); }

#alarm-state.armed {
  color: var(--bad);
  font-weight: 700;
  border: 1px solid currentColor;
  border-radius: 6px;
  padding: 2px 8px;
  animation: pulse 1.6s ease-in-out infinite;
}

.dismiss {
  border: 1px solid var(--bad);
  background: transparent;
  color: var(--bad);
  border-radius: 6px;
  padding: 3px 12px;
  font: inherit;
  font-size: 11.5px;
  font-weight: 600;
  cursor: pointer;
}
.dismiss:hover { background: rgba(239, 106, 90, .12); }

/* ── the watchlist ─────────────────────────────────────────────────── */

.pinned {
  margin: 18px 24px 0;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  overflow: hidden;
}
.pinned h2 {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  font-family: var(--ui);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .14em;
  color: var(--ink);
}
.pinned h2 .n { font-family: var(--num); font-size: 11px; font-weight: 400; letter-spacing: 0; text-transform: none; color: var(--dim); }

/* The play button. Same lane in both tables, so the two line up -- the lane
   itself is the <colgroup>'s, shared by both, so no width lives here. */
th.col-play, tbody td.col-play { padding-right: 0; }
.play {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid currentColor;
  background: transparent;
  color: var(--accent);
  border-radius: 6px;
  width: 26px;
  height: 24px;
  line-height: 1;
  font-size: 10px;
  cursor: pointer;
  opacity: .8;
}
.play:hover { opacity: 1; }
.play.on { opacity: 1; background: rgba(79, 211, 155, .14); }

/* A pinned tournament the current snapshot has no row for -- the gap between
   the edition that just closed and the one the next sweep reads. Drawn dim
   rather than dropped: a line that vanishes reads as an unpin. */
tbody tr.placeholder { color: var(--dim); }
tbody tr.placeholder .play { color: var(--faint); }
tbody tr.placeholder .name { font-weight: 500; }
tbody tr.placeholder .cd { font-size: 15px; font-weight: 500; color: var(--faint); }

/* Alarm ringing on this row.
   Everything here is drawn OUTSIDE the text: a hard frame, and a play cell that
   blinks solid red. The row's background is left alone, and that was measured
   before it was a preference: back when GG's banners were painted, tinting one
   even a fifth of the way dropped its name to 4.76:1, under the ratio this page
   exists to hold. No row is given a fill any more, and the conclusion survives
   for a simpler reason -- every name is checked against ONE known background.
   The frame carries no text, so it can be as loud as it likes.
   Inset shadows rather than borders: nothing shifts by a pixel when a row starts
   or stops ringing. */
tbody tr.alarming td {
  box-shadow: inset 0 2px 0 var(--bad), inset 0 -2px 0 var(--bad);
}
tbody tr.alarming td.col-play {
  box-shadow: inset 5px 0 0 var(--bad), inset 0 2px 0 var(--bad), inset 0 -2px 0 var(--bad);
  /* `steps(1)`, not a fade: a crossfade reads as a colour, and this has to read
     as an alarm. */
  animation: alarm-cell .9s steps(1) infinite;
}
@keyframes alarm-cell {
  0%, 49% { background: var(--bad); }
  50%, 100% { background: transparent; }
}
tbody tr.alarming td:last-child {
  box-shadow: inset -5px 0 0 var(--bad), inset 0 2px 0 var(--bad), inset 0 -2px 0 var(--bad);
}
/* Dark glyph on the blinking red block: white on that red is 3.1:1, black 6.7:1.
   The red border keeps the button visible through the transparent half. */
tbody tr.alarming .play {
  color: var(--bg);
  border-color: var(--bad);
  background: var(--bad);
  opacity: 1;
}
/* The countdown is the number the alarm is about, so it carries the weight. */
tbody tr.alarming td.late .cd { font-weight: 700; }
tbody tr.alarming .name { font-weight: 700; }

/* The whole watchlist answers for a ringing row -- it is above the fold, and a
   frame on one line of it can still be scrolled past. */
.pinned.ringing {
  border-color: rgba(239, 106, 90, .55);
  box-shadow: 0 0 0 1px rgba(239, 106, 90, .12), 0 10px 30px rgba(0, 0, 0, .45);
}
.pinned.ringing h2 {
  background: linear-gradient(90deg, rgba(239, 106, 90, .10), rgba(239, 106, 90, 0) 340px);
}

table { width: 100%; border-collapse: collapse; }

main { padding: 22px 24px 0; }

/* ── Resizable, hideable columns ──────────────────────────────────────────
   `table-layout: fixed` is what makes a dragged width MEAN something: under
   auto layout the browser re-measures the content and quietly overrides it.
   Only the two lobby tables take it -- the Sweeps run list has no <colgroup>
   and fixed layout would hand its columns equal widths.

   Both tables get the same <colgroup>, so the watchlist above stays lined up
   with the table below; `min-width` is the sum of the visible columns, which is
   what lets the page scroll sideways once they no longer fit. */
#table, .pinned table { table-layout: fixed; }

/* The grip: a lane at the right edge of every header cell. Slightly wider than
   the line it draws, because a 1px target is a target nobody hits. */
thead th { position: relative; }
.grip {
  position: absolute;
  top: 0;
  right: -5px;
  width: 10px;
  height: 100%;
  cursor: col-resize;
  z-index: 3;
  touch-action: none;
}
/* The separator IS the affordance: a control nobody can see is a control
   nobody drags. `--line` is all but invisible against the header's own panel,
   which is fine for the rule under a row and useless for a thing meant to be
   grabbed -- so the divider gets a colour of its own, runs the full height of
   the header cell, and brightens to the accent under the pointer. */
thead th { --grip-line: #3d4a46; }
.grip::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 4px;
  width: 2px;
  border-radius: 2px;
  background: var(--grip-line);
}
.grip:hover::after, .grip.on::after {
  background: var(--accent);
  box-shadow: 0 0 0 1px rgba(79, 211, 155, .35);
}
/* While a drag is on, the cursor must not flicker back to a text caret over the
   rows, and a drag across the table must not select every name it crosses. */
body.resizing, body.resizing * { cursor: col-resize !important; }
body.resizing { user-select: none; }

/* The filler column. It has no width of its own -- under fixed layout the
   columns with no width divide whatever is left, which is exactly the job. */
th.col-filler, tbody td.col-filler { padding: 0; }

/* The column menu. Right-click anywhere in either table. */
.colmenu {
  position: fixed;
  z-index: 20;
  min-width: 190px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px;
  box-shadow: 0 12px 28px rgba(0, 0, 0, .55);
  font-size: 13px;
}
.colmenu-title {
  margin: 2px 8px 6px;
  font-family: var(--ui);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--dim);
}
.colmenu-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 8px;
  border-radius: 6px;
  cursor: pointer;
  color: var(--ink);
}
.colmenu-row:hover { background: var(--bg); }
/* A column that cannot be turned off says so, rather than offering a checkbox
   that does nothing: Late reg, Buy-in, Site and Tournament are the four the
   table exists to be read on -- what closes when, for how much, on which
   client, and which event. */
.colmenu-row.locked { cursor: default; color: var(--dim); }
.colmenu-row.locked:hover { background: none; }
.colmenu-lock {
  margin-left: auto;
  font-family: var(--ui);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .08em;
  opacity: .7;
}
.colmenu-reset {
  width: 100%;
  margin-top: 6px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--dim);
  border-radius: 6px;
  padding: 5px 8px;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.colmenu-reset:hover { color: var(--ink); border-color: var(--accent); }


thead th {
  position: sticky;
  /* Set from JS off the bar's real height. It used to be a hardcoded 49px, which
     the alarm control in the bar quietly invalidated: a header that no longer
     matches the bar it sits under leaves a strip of scrolling rows above it. */
  top: var(--head-top, 49px);
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  text-align: left;
  font-family: var(--ui);
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--dim);
  padding: 0 12px 8px;
  white-space: nowrap;
  z-index: 2;
}
thead th.num { text-align: right; }

tbody td {
  padding: 0 12px;
  height: 46px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
  white-space: nowrap;
  /* Under fixed layout a cell no longer widens its column, so a long value has
     to end somewhere: an ellipsis says "there is more", spilling says nothing
     and lands on top of the next column. */
  overflow: hidden;
  text-overflow: ellipsis;
}
tbody tr:hover td { background: rgba(255, 255, 255, .022); }
tbody td.num { text-align: right; font-size: 12.5px; color: var(--ink-2); }
/* The buy-in is the figure rows are compared on, so it carries more weight than
   the counts beside it. */
tbody td.c-buyin { font-size: 14px; font-weight: 600; color: var(--ink); }
/* The name is the one cell that wraps instead of clipping: it is the value
   someone reads in full, and a tournament cut off mid-word is a row that has to
   be widened before it can be identified. */
tbody td.col-name { white-space: normal; }

/* Late reg: ONE column, two lines. It is one stored instant -- how long is left,
   and the clock time it shuts at -- and the two used to be separate columns of
   the same weight, which spent 120px saying the same thing twice. */
.cd { font-size: 19px; font-weight: 600; line-height: 1.05; }
.at { font-size: 10.5px; color: var(--dim); line-height: 1.3; }
/* "not today". Nothing is drawn more than 12 h out, so this is the whole of
   what a spelled-out date said, in a sixth of the room -- see `deadlineShort`.
   Warm, because a time that is not today is the one thing about that line
   somebody can misread as tonight. */
.at .plus, td.c-end .plus { color: var(--warn); font-weight: 600; }
/* Registration closed: a label, not a number, so the countdown drops a size and
   a weight rather than shouting a figure nobody can act on. */
td.late.closed .cd { font-size: 16px; font-weight: 500; }

/* Status: a dot and a word. It used to be a bordered badge, which put a box in
   the row for a value that is one of four. */
.state {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--text);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .09em;
  text-transform: uppercase;
}
.state .dot { width: 6px; height: 6px; border-radius: 999px; flex: none; }
/* An announced row's dot is hollow: it has not started. */
.state .dot.hollow { background: none; border: 1px solid currentColor; }

/* The name carries the colour the client published, and it is the only place
   that colour is drawn -- no bar beside it, and no fill behind the row. See the
   note in `rowElement`. */
.name { font-weight: 600; }

.sub { font-size: 11px; }

.badge {
  display: inline-block;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .07em;
  border-radius: 4px;
  padding: 1px 6px;
  border: 1px solid currentColor;
}

.closing { animation: pulse 1.6s ease-in-out infinite; }
@keyframes pulse { 50% { opacity: .45; } }

/* The rules the table is read under, said once under it instead of guessed at. */
.legend {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
  padding: 14px 12px 24px;
  color: var(--dim);
  font-size: 11.5px;
}
.legend .sep { opacity: .5; }
.legend .last { margin-left: auto; font-family: var(--num); }

.empty { color: var(--dim); padding: 30px 12px; }
.empty.hidden { display: none; }
code { background: var(--raised); padding: 1px 5px; border-radius: 4px; font-family: var(--num); font-size: 12px; }

/* The Site column, sitting just before the name it belongs to. A filled tag
   rather than bare text: it is the one value scanned rather than read, and
   nine clients are told apart faster by hue than by spelling. Wide enough for
   the longest label spelled in full -- a client is named the way its window is
   titled, so nobody has to learn which abbreviation means which. */
.sitetag {
  display: inline-block;
  /* An inline-block with `overflow: hidden` takes its BASELINE from its bottom
     margin edge, so the line's own descender space is added underneath it and
     the tag sits high in a cell the browser has centred correctly. Aligning the
     span to the line's middle is what actually centres the tag -- the td's
     `vertical-align: middle` only centres the line box around it. */
  vertical-align: middle;
  border-radius: 4px;
  padding: 2px 7px;
  font-family: var(--text);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  background: rgba(255, 255, 255, .06);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* ── The Sweeps tab ──────────────────────────────────────────────────────
   A console, not a table of tournaments: no row wears a client's colour here,
   so this half of the page is the only one that sets its own.

   It is drawn only for a caller the hub said may start a sweep -- see
   `setAdmin` in `sweeps.js`. Someone reading the lobby from anywhere else
   never sees the tab, and the routes behind it were already refused to them. */

.tabs { display: flex; gap: 6px; }

.tab {
  border: 1px solid var(--line);
  background: var(--raised);
  color: var(--ink-2);
  border-radius: 7px;
  padding: 5px 14px;
  font-family: var(--ui);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  cursor: pointer;
}
.tab:hover { color: var(--ink); }
.tab.on { color: var(--bg); background: var(--accent); border-color: var(--accent); }

.sweeps { padding: 18px 24px 24px; display: flex; flex-direction: column; gap: 16px; }

.sweep-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 11px; }
.sweep-head .lbl { color: var(--ink); letter-spacing: .14em; }
.sweep-head .hint { font-family: var(--num); font-size: 11px; color: var(--dim); }

/* One card per client, instead of nine identical buttons in a row. The question
   this tab is opened with is "which client is stale" -- so a card leads with the
   age of its rows and what its last run did, and the button is the answer to it
   rather than the whole control. */
.sweep-actions {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
  gap: 12px;
}

.reader {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px 13px;
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.reader.running { border-color: rgba(79, 211, 155, .45); box-shadow: 0 0 0 1px rgba(79, 211, 155, .10); }
.reader.failed { border-color: rgba(239, 106, 90, .45); }
.reader.warned { border-color: rgba(232, 179, 74, .45); }
.reader h3 {
  margin: 0;
  font-family: var(--ui);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--ink);
}
.reader .head { display: flex; align-items: center; gap: 8px; }
.reader .head .dot { width: 6px; height: 6px; border-radius: 999px; margin-left: auto; background: var(--good); }
.reader.failed .head .dot { background: var(--bad); }
.reader.warned .head .dot { background: var(--warn); }
.reader.running .head .dot { box-shadow: 0 0 8px rgba(79, 211, 155, .8); }
.reader .kv { display: flex; justify-content: space-between; align-items: baseline; font-size: 11.5px; gap: 10px; }
.reader .kv .k { color: var(--dim); }
.reader .kv .v { font-family: var(--num); font-variant-numeric: tabular-nums; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reader .kv .v.good { color: var(--good); }
.reader .kv .v.warn { color: var(--warn); }
.reader .kv .v.bad { color: var(--bad); }

.sweep-actions .go {
  border: 1px solid var(--accent);
  background: rgba(79, 211, 155, .10);
  color: var(--accent);
  border-radius: 7px;
  padding: 7px 0;
  text-align: center;
  font-family: var(--ui);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  cursor: pointer;
}
.sweep-actions .go:hover:not(:disabled) { background: rgba(79, 211, 155, .20); }
/* A sweep already in flight, or one being asked for. The button says which in
   its own label -- a control that looks pressable and does nothing is the
   failure this tab exists to make impossible. */
.sweep-actions .go:disabled { border-color: var(--line); background: var(--raised); color: var(--dim); cursor: default; }

/* The clear strip. Drawn apart from the sweep controls and in the bad colour
   because it is the one control here that throws readings away -- a wipe
   sitting in the same row as "Sweep GGPoker", in the same colour, is a misclick
   waiting to happen. Outline rather than a solid fill: this is not the action
   anyone comes to this tab for. */
.sweep-clear {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  border: 1px solid var(--line);
  border-left: 3px solid rgba(239, 106, 90, .6);
  border-radius: 8px;
  background: #101413;
  padding: 11px 14px;
}
.sweep-clear .lbl { color: var(--bad); letter-spacing: .1em; }
.sweep-clear .why { font-size: 11.5px; color: var(--dim); }
.sweep-clear #clear-buttons { display: flex; gap: 6px; flex-wrap: wrap; margin-left: auto; }

.sweep-clear .wipe {
  border: 1px solid rgba(239, 106, 90, .55);
  background: transparent;
  color: var(--bad);
  border-radius: 6px;
  padding: 4px 11px;
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}
.sweep-clear .wipe:hover:not(:disabled) { background: rgba(239, 106, 90, .12); }
.sweep-clear .wipe:disabled { border-color: var(--line); color: var(--dim); cursor: default; }

.sweep-note { color: var(--warn); font-size: 12px; }

/* Side by side while there is room, stacked when there is not. The log is the
   half worth the space: the run list is four short columns. */
.sweep-panes { display: grid; grid-template-columns: minmax(300px, 430px) 1fr; gap: 16px; }
@media (max-width: 900px) { .sweep-panes { grid-template-columns: 1fr; } }

table.runs { border-collapse: collapse; width: 100%; align-self: start; }
table.runs th {
  text-align: left;
  padding: 0 10px 8px;
  border-bottom: 1px solid var(--line);
  font-family: var(--ui);
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--dim);
  white-space: nowrap;
}
table.runs td {
  padding: 0 10px;
  height: 38px;
  border-bottom: 1px solid var(--line-soft);
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

tr.run { cursor: pointer; }
tr.run:hover td { background: var(--panel); }
tr.run.on td { background: rgba(79, 211, 155, .06); }
tr.run.on td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
tr.run-done .result { color: var(--good); }
tr.run-failed .result { color: var(--bad); }
tr.run-interrupted .result { color: var(--warn); }
tr.run-running .result { color: var(--accent); }

.run-log {
  margin: 0;
  padding: 14px 16px;
  background: #080a09;
  border: 1px solid var(--line);
  border-radius: 10px;
  color: var(--ink-2);
  font: 400 12px/1.62 var(--num);
  /* Tall enough to read a pass without scrolling, and it scrolls itself rather
     than growing the page: the run list beside it must stay in view. */
  height: 60vh;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
}

/* ── the Tests tab ─────────────────────────────────────────────────────────
   One chip per site, in the run's own order, upgraded as the log's TEST lines
   arrive: pending -> running -> pass/fail. The moods reuse the reader cards'
   colours -- a verdict and a card state are the same vocabulary. */

/* The two "run the whole chain" buttons -- the Sweeps tab's Sweep all and the
   Tests tab's Run all tests. Same shape on purpose: they are the same kind of
   thing (a server-side chain that owns the machine until it is done) and both
   sit at the end of their panel's heading. */
#tests-play,
#sweep-all {
  border: 1px solid var(--accent);
  background: rgba(79, 211, 155, .10);
  color: var(--accent);
  border-radius: 7px;
  padding: 6px 16px;
  margin-left: auto;
  font-family: var(--ui);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  cursor: pointer;
}
#tests-play:hover:not(:disabled),
#sweep-all:hover:not(:disabled) { background: rgba(79, 211, 155, .20); }
#tests-play:disabled,
#sweep-all:disabled { border-color: var(--line); background: var(--raised); color: var(--dim); cursor: default; }

/* One small button per client, for the single-client site test. Outlined and
   quiet: the full run is the headline action on that tab, these are the
   follow-up to a sweep of one client. */
.site-buttons { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 11px; }
.site-buttons button {
  border: 1px solid var(--line);
  background: transparent;
  color: var(--ink);
  border-radius: 6px;
  padding: 5px 10px;
  font-family: var(--ui);
  font-size: 11px;
  letter-spacing: .06em;
  cursor: pointer;
}
.site-buttons button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.site-buttons button:disabled { color: var(--dim); cursor: default; }

.test-chips { display: flex; flex-wrap: wrap; gap: 10px; }

.test-chip {
  display: inline-flex;
  flex-direction: column;
  gap: 2px;
  min-width: 108px;
  padding: 8px 12px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
}
.test-chip .name {
  font-family: var(--ui);
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
}
.test-chip .detail {
  font-family: var(--num);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--dim);
}
.test-chip.pending { opacity: .6; }
.test-chip.running { border-color: rgba(79, 211, 155, .45); box-shadow: 0 0 0 1px rgba(79, 211, 155, .10); }
.test-chip.running .detail { color: var(--accent); }
.test-chip.pass { border-color: rgba(79, 211, 155, .45); }
.test-chip.pass .detail { color: var(--good); }
.test-chip.fail { border-color: rgba(239, 106, 90, .45); }
.test-chip.fail .detail { color: var(--bad); }

/* ── the login gate ──────────────────────────────────────────────────────
 *
 * Drawn only off the capture machine (`lobby.js` unhides it), so on the hub's
 * own page none of this is ever painted.
 *
 * It COVERS rather than replaces: the table behind it is not rendered until a
 * session exists, and a full-screen layer means no partially-populated page can
 * flash through underneath while the first poll is in flight.
 */

.gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--bg);
}

/* `hidden` has to beat the flex above, or the attribute does nothing at all --
 * the classic trap of styling a element that also carries [hidden]. */
.gate[hidden] { display: none; }

.gate-card {
  width: 100%;
  max-width: 340px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 28px 26px 24px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
}

.gate-card h2 {
  margin: 0;
  font: 600 20px/1.2 var(--ui);
  color: var(--ink);
}

.gate-sub {
  margin: 0 0 10px;
  font: 400 13px/1.4 var(--text);
  color: var(--dim);
}

.gate-card label {
  font: 500 11px/1 var(--ui);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--dim);
}

.gate-card input {
  padding: 9px 10px;
  font: 400 14px/1.2 var(--text);
  color: var(--ink);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: 7px;
}

.gate-card input:focus {
  outline: none;
  border-color: var(--accent);
}

.gate-card button {
  margin-top: 12px;
  padding: 10px;
  font: 600 14px/1 var(--ui);
  color: #06231a;
  background: var(--accent);
  border: 0;
  border-radius: 7px;
  cursor: pointer;
}

/* A button that is working has to LOOK unavailable, or the second click sends
 * the pair twice and the second failure is the one the person reads. */
.gate-card button[disabled] {
  opacity: 0.55;
  cursor: default;
}

.gate-error {
  margin: 10px 0 0;
  min-height: 1em;
  font: 400 12.5px/1.4 var(--text);
  color: var(--bad);
}

/* Says which source the rows came from, and is drawn ONLY for the mirror. The
 * hub is the normal case and does not need announcing; a mirrored list is
 * minutes-to-hours behind and does. */
.src-mirror {
  color: var(--warn);
  border-color: var(--warn);
}
