/* VIP Zone
   --------
   The house palette. Near-black ground, cards a shade above it, amber for
   anything active, cream for "this one is yours", and gold / silver / bronze
   reserved strictly for the paid places. Those meanings are fixed - a colour
   here is a statement, not decoration, and the same colour must never mean two
   things on one screen.

   Dark only, deliberately. The chart is a heat ramp read against a near-black
   ground; on a light ground the same ramp carries no meaning at all. */

:root {
  /* EVERY SCROLLBAR ON THE PAGE, IN ONE LINE. Without this the browser draws
     its light-mode bars - a white trough down the side of a near-black log -
     because a dark palette in CSS tells it nothing about the chrome it owns.
     Declaring the scheme also fixes the form controls and the overscroll
     colour, which styling each scrollbar by hand would not. */
  color-scheme: dark;

  --bg: #131316;
  /* THE CLIENT HAS NO COLOUR DARKER THAN --bg. Its whole palette is three
     surfaces: BG_COLOR #131316, CARD_BG #1b1b1f, CARD_BORDER #2c2c32. This
     was #0a0a0c - an invented fourth, a full step below anything the client
     draws - and it is what made the chart well and the inset controls read as
     a different, darker application than the page around them. Kept as a name
     because several rules ask for "the recessed surface"; it is now simply
     the page's own background, which is what the client recesses to. */
  --bg-deep: #131316;
  --card: #1b1b1f;
  --border: #2c2c32;
  --button: #242429;
  --button-hover: #2f2f37;
  --track: #3a3a42;

  --fg: #f0f0f2;
  --fg-2: #e6e6ea;
  --muted: #a4a4ad;
  --faint: #8b8b93;

  --accent: #ffb454;          /* the dashboard's bonus-hunt amber */
  --live: #e53935;           /* the ring and label on a board with a hunt */
  --cream: #e8e2c4;           /* "this one is mine" */
  --teal: #3d9a9a;
  --green: #4ade80;
  --danger: #e06c5a;

  --gold: #ffc94a;
  --silver: #aab3c0;
  --bronze: #b0703a;
  --tick: #9a9086;

  --sans: "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
  --mono: "Cascadia Code", "JetBrains Mono", ui-monospace, Consolas, monospace;
}

* { box-sizing: border-box; }

/* A class that sets `display` outranks the browser's own rule for [hidden],
   so anything toggled by that attribute has to be forced down. Without this,
   hiding an element silently does nothing. */
[hidden] { display: none !important; }

/* The whole thing fits the window. Nothing about a hunt is worth scrolling
   for - it is a readout you glance at while something is happening, and a
   figure you have to scroll to find is a figure you will not look at. Anything
   that cannot fit gets its own scroll INSIDE its panel, so the frame never
   moves. */
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* ------------------------------------------------------------- header -- */

.top {
  display: flex; align-items: center; gap: 18px;
  padding: 0 14px; height: 42px;
  background: var(--card); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 20;
}
.viewing {
  font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--faint); font-weight: 700;
}
.viewing b { color: var(--fg); font-size: 13px; letter-spacing: 0; text-transform: none; margin-left: 8px; }

.tabs { display: flex; gap: 3px; }
.tab {
  appearance: none; cursor: pointer; font: inherit;
  background: var(--button); color: var(--muted);
  border: 1px solid var(--border); border-radius: 5px; padding: 4px 13px;
}
.tab:hover { background: var(--button-hover); color: var(--fg); }
.tab.is-active { background: var(--accent); border-color: var(--accent); color: #17130a; font-weight: 700; }

.live { margin-left: auto; display: flex; align-items: center; gap: 14px; font-size: 12px; }
.dot { color: var(--green); font-size: 15px; line-height: 1; }
.livelabel { letter-spacing: .1em; font-size: 10.5px; font-weight: 700; text-transform: uppercase; }
.whoname { color: var(--fg); font-weight: 600; }
.linkish {
  appearance: none; border: 0; background: none; cursor: pointer;
  color: var(--muted); font: inherit; text-decoration: underline; padding: 0;
}
.signin { display: flex; gap: 5px; }
.signin-error { color: var(--danger); font-size: 11.5px; max-width: 380px; }
.signin input {
  font: inherit; padding: 3px 8px; border: 1px solid var(--border);
  border-radius: 5px; background: var(--bg-deep); color: var(--fg); width: 150px;
}
.signin button, button.ghost, button.primary, a.ghost {
  font: inherit; cursor: pointer; border-radius: 5px; padding: 4px 12px;
  border: 1px solid var(--border); background: var(--button); color: var(--muted);
  text-decoration: none; display: inline-block;
}
.signin button:hover, button.ghost:hover, a.ghost:hover { background: var(--button-hover); color: var(--fg); }
button.primary { background: var(--accent); border-color: var(--accent); color: #17130a; font-weight: 700; }
button.primary:disabled { opacity: .35; cursor: default; }

/* ------------------------------------------------------------ health -- */

.healthbar {
  padding: 6px 14px; font-size: 12px;
  background: #2a1714; color: var(--danger);
  border-bottom: 1px solid #3d211c;
}

/* ------------------------------------------------------------ panels -- */

/* NO WIDTH CAP. The desktop build this came from runs maximised and sizes its
   chart as `usable_w - 2 * WINDOW_OUTER_MARGIN` - the whole window, less 40
   pixels a side. A 1600px cap here was throwing away 320 pixels of chart on a
   1920 screen and more on anything wider, as two empty gutters, which is the
   one thing the chart can always use more of: the heat is 520 slices, so every
   pixel of width is resolution rather than stretch. The rail is a fixed 430,
   so all of the extra goes to the chart. */
.panel { display: none; padding: 10px 12px 40px; }
/* BOTH COLUMNS END ON THE SAME LINE. They used to be sized by different
   things - the left by what it contains, the rail by a sum against the window
   - so the two bottoms landed wherever those happened to fall and the offset
   between them changed with the window height. Letting the row stretch gives
   the rail the left column's height, which is the one number both can agree
   on, and the log list inside it takes up the difference. */
.split { align-items: stretch; min-height: calc(100vh - 52px - 66px); }
.panel.is-active { display: block; }

h1 { font-size: 18px; margin: 0; font-weight: 700; letter-spacing: -.01em; }
h2 {
  font-size: 10.5px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--faint); font-weight: 700; margin: 0 0 8px;
}

/* ------------------------------------------------------------- tiles -- */
/* Two rows of four, fixed. The grid does not reflow as values arrive - a
   readout that moves under the eye is worse than one showing a dash. */

.tiles {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px;
  grid-auto-rows: 1fr;
  background: var(--border); border: 1px solid var(--border);
  border-radius: 8px; overflow: hidden; margin-bottom: 8px;
}
.tile { background: var(--card); padding: 5px 12px; text-align: center; min-width: 0; }
.tile .k {
  font-size: 10.5px; letter-spacing: .07em; color: var(--muted);
  font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tile .v {
  font-size: 17px; font-weight: 700; margin-top: 1px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tile .v.sm { font-size: 14px; }
.tile .v.amber { color: var(--accent); }

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

.card {
  background: var(--card); border: 1px solid var(--border);
  border-radius: 8px; margin-bottom: 8px;
}
.cardhead {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px; border-bottom: 1px solid var(--border);
}
.seg { display: flex; gap: 3px; }
.seg button {
  appearance: none; cursor: pointer; font: inherit; font-size: 12px;
  background: var(--button); color: var(--muted);
  border: 1px solid var(--border); border-radius: 5px; padding: 3px 14px;
}
.seg button.on { background: var(--accent); border-color: var(--accent); color: #17130a; font-weight: 700; }
.cardhead .grow { flex: 1; }
.cardhead .note { font-size: 11.5px; color: var(--faint); }

/* THE CHART'S OWN HEADER SITS ON THE CHART, and buys back the row it used to
   occupy. The plot reserves 60px above itself for the band bar, and the band
   bar is a centred bracket - so the two top corners are empty on every hunt,
   which is room already paid for. Taking the row out of the flow lifts
   everything below it by its full height.

   Only the buttons take the pointer. The note is a caption lying over a chart
   that answers to hover, and a caption that swallows the cursor is worse than
   no caption. */
#chartcard { position: relative; }
#chartcard > .cardhead {
  position: absolute; z-index: 4;
  top: 10px; left: 12px; right: 12px;
  padding: 0; border-bottom: 0; pointer-events: none;
}
#chartcard > .cardhead .seg { pointer-events: auto; }

/* THE CHART IS THE ONLY THING IN THE LEFT COLUMN THAT CHANGES SIZE.
   Everything else there - the tiles, the seat bar, the slot fold - has a
   height set by what it contains, so the chart is what absorbs whatever is
   left over.

   420 IS THE CLIENT'S OWN DOCKED CHART, MEASURED, NOT PICKED. Its compact
   render is a 420px canvas reserving CHART_MARGIN_TOP_COMPACT (60) +
   CHART_MARGIN_BOTTOM_COMPACT (44) = 104, leaving a 316px plot band, and
   `tools/diff_chart.py` holds this port to that render shape for shape. So
   420 is the floor here, not a target to be squeezed under.

   GROWS BUT NEVER SHRINKS, which is the client's own rule -
   `max(CHART_HEIGHT_ABSOLUTE_FLOOR, usable_h - header_h - margin)`. On a tall
   window the chart takes the slack; on a short one the page gets a scrollbar
   rather than a crushed chart. That direction is deliberate and was learned
   the hard way in the desktop build, which once forced its canvas taller than
   the screen had room for and lost the bottom of the chart off-screen with no
   way to reach it. A scrollbar beats clipping, and both beat a 40px sliver. */
.chart { position: relative; height: 420px; background: var(--bg-deep); }
/* Vector, not a bitmap - see displaysvg.js. The browser rasterises the SVG
   once, at the end, at the real device resolution. */
.chart .vec { position: absolute; inset: 0; }
.chart .vec svg { width: 100%; height: 100%; }
.markers { position: absolute; inset: 0; pointer-events: none; }
/* CHART LABELS AS REAL TEXT, over the canvas rather than painted into it.
   See paintDisplayList's own note for why - the short version is that canvas
   text gets grayscale anti-aliasing while everything else on the page gets
   subpixel, and no amount of correcting the size or the pixel alignment
   closes that gap because it is not a size or alignment problem. */
.textlayer { position: absolute; pointer-events: none; overflow: hidden; }
.textlayer span {
  position: absolute; white-space: pre; line-height: 13px;
  /* The geometry decided where this goes; nothing here may move it. */
  margin: 0; padding: 0;
}
.seatmark {
  position: absolute; transform: translateX(-50%);
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--card); border: 1.5px solid var(--tick);
  color: var(--muted); font-size: 8.5px; font-weight: 700;
  display: grid; place-items: center; font-family: var(--mono);
}
.seatmark.is-mine {
  background: var(--cream); border-color: var(--bg); color: #17130a;
  width: 18px; height: 18px; font-size: 9.5px; z-index: 2;
}

/* THE HOVER READOUT: could a call go where the cursor is.
   The dot sits on the axis, the pill 20px above it, both the same sizes the
   desktop renderer uses. Green carries an amount; red carries the refusal.
   The red is this page's own danger colour rather than a second one imported
   alongside it - one meaning, one colour, and there is already a red here
   that means "this figure is not to be acted on". */
.probe-dot {
  position: absolute; width: 12px; height: 12px; border-radius: 50%;
  transform: translate(-50%, -50%); z-index: 3;
}
.probe-pill {
  position: absolute; transform: translate(-50%, -50%);
  padding: 5px 8px; border-radius: 4px; z-index: 3;
  /* A solid near-black plate, not a tint. The heat behind this runs the whole
     scale, so a translucent label is legible over the cold end and vanishes
     into the warm one - which is the half of the chart worth pointing at. */
  background: #0a0a0b; border: 1px solid currentColor;
  font: 700 10px/1 var(--sans); white-space: nowrap;
}
.probe-dot.ok { background: var(--green); }
.probe-pill.ok { color: var(--green); }
.probe-dot.no { background: var(--danger); }
.probe-pill.no { color: var(--danger); }

/* the strip under the chart: search window, gaps, bands */

/* -------------------------------------------------------------- seat -- */

.seatbar {
  display: flex; align-items: stretch; gap: 1px;
  background: var(--border); border: 1px solid var(--border);
  border-radius: 8px; overflow: hidden; margin-bottom: 8px;
}
.seatbar > * { background: var(--card); }
.seatnum { padding: 9px 16px; display: flex; align-items: center; gap: 11px; }
.seatnum .k {
  font-size: 10.5px; letter-spacing: .07em; color: var(--muted); font-weight: 600;
}
.seatnum .v {
  font-size: 24px; font-weight: 700; color: var(--cream);
  font-variant-numeric: tabular-nums;
}
.seatnum .rank { font-size: 11.5px; color: var(--faint); }
.seatmsg { padding: 9px 16px; display: flex; align-items: center; color: var(--muted); flex: 1; }
.seatacts { padding: 7px 10px; display: flex; align-items: center; gap: 6px; margin-left: auto; }

/* the neighbour strip - the dashboard's own, in SVG */
.strip { padding: 4px 0 10px; }
/* Height is set by the painter so the strip keeps the proportions its
   geometry was designed at, and so a surface carrying only a sentence is not
   given the number line's worth of room. The value here is only a first
   frame, replaced from the drawing's own height on the first paint. */
.strip .vec { display: block; width: 100%; height: 108px; }
.striphint { padding: 0 14px 10px; font-size: 11.5px; color: var(--faint); margin: 0; }

/* The helper's own line. Its own class rather than the strip hint's, which is
   turned off further down the file - a message that only ever appears when
   something needs saying cannot borrow a rule whose job is to hide things. */
.placemsg { padding: 0 14px 11px; font-size: 12px; color: var(--faint); margin: 0; }
.placemsg.bad { color: var(--danger); }
.placemsg.ok { color: var(--green); }

/* ------------------------------------------------------------- seats -- */

.tablewrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: 12.5px; min-width: 560px; }
th, td { text-align: left; padding: 5px 14px; border-bottom: 1px solid var(--border); }
thead th {
  font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--faint); background: var(--bg-deep); font-weight: 700; white-space: nowrap;
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: #202026; }
td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
tr.is-mine td { background: #23211a; }
tr.is-mine td:first-child { box-shadow: inset 2px 0 0 var(--cream); }
td.empty { color: var(--faint); text-align: center; padding: 20px; }
.pill {
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .05em;
  text-transform: uppercase; padding: 1px 6px; border-radius: 3px; font-weight: 700;
}
.pill.placed { background: #16301f; color: var(--green); }
.pill.waiting { background: var(--button); color: var(--faint); }

/* ------------------------------------------------------- collapsible -- */

.fold { padding: 8px 12px; }
.fold summary {
  cursor: pointer; list-style: none; color: var(--muted);
  font-size: 12px; user-select: none;
}
.fold summary::-webkit-details-marker { display: none; }
.fold summary::before { content: "▸ "; color: var(--faint); }
.fold[open] summary::before { content: "▾ "; }
/* Fixed height, rows scrolling inside. A 63-slot hunt and an 8-slot one then
   occupy the same space, so opening this never shoves the rest of the page
   around - about nine rows, which is enough to read a hunt's shape. */
.slotgrid { margin-top: 6px; max-height: 210px; overflow-y: auto; }
.slotrow {
  display: grid; grid-template-columns: 5.5ch 1fr 8ch 9ch;
  gap: 6px; align-items: baseline;
  font-family: var(--mono); font-size: 12px; padding: 2px 4px;
}
.slotrow .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fg); }
.slotrow .bet { color: var(--fg); }
.slotrow .multi, .slotrow .pay { text-align: right; font-weight: 700; }
.slothead {
  position: sticky; top: 0; background: var(--card);
  font-family: var(--sans); font-size: 9.5px; letter-spacing: .08em;
  color: var(--faint); font-weight: 700; border-bottom: 1px solid var(--border);
  padding-bottom: 3px; margin-bottom: 2px;
}
.slothead .multi, .slothead .pay { font-weight: 700; }
.slotempty { color: var(--faint); font-size: 12px; padding: 6px 4px; }

/* ------------------------------------------------------------- later -- */

.soon { max-width: 66ch; padding: 12px 4px; }
.soon h1 { margin-bottom: 6px; }
.soon-lede { font-size: 15px; color: var(--muted); margin: 0 0 14px; }
.soon-list { padding-left: 18px; color: var(--fg-2); }
.soon-list li { margin-bottom: 5px; }
.soon-note { font-size: 12px; color: var(--faint); border-top: 1px solid var(--border);
             padding-top: 12px; margin-top: 18px; }

.foot {
  border-top: 1px solid var(--border); background: var(--card);
  padding: 4px 14px; display: flex; gap: 22px; flex-wrap: wrap;
  font-size: 11.5px; color: var(--faint); flex: none;
}

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
  .streamer .status { animation: none !important; }
}

@media (max-width: 900px) {
  .tiles { grid-template-columns: repeat(2, 1fr); }
      .seatbar { flex-wrap: wrap; }
  .seatacts { margin-left: 0; }
}

/* ------------------------------------------------- hunt picker (header) -- */
/* The whole archive is reachable from here. A hunt that was never captured is
   still listed but marked, rather than hidden - it is real history, and a
   picker that silently omits half of it misrepresents how much there is. */

.huntpick {
  font: inherit; font-size: 12.5px; max-width: 300px;
  background: var(--bg-deep); color: var(--fg);
  border: 1px solid var(--border); border-radius: 5px; padding: 3px 8px;
}
.huntpick option[disabled] { color: var(--faint); }
.navbtn {
  appearance: none; cursor: pointer; font: inherit; line-height: 1;
  background: var(--bg-deep); color: var(--muted);
  border: 1px solid var(--border); border-radius: 5px; padding: 3px 9px;
}
.navbtn:hover { background: var(--button-hover); color: var(--fg); }
.navbtn:disabled { opacity: .35; cursor: default; }

.replaynote {
  background: #241d10; border: 1px solid #4a3a1c; color: var(--accent);
  border-radius: 7px; padding: 8px 12px; margin-bottom: 12px; font-size: 12.5px;
}
.replaynote b { color: var(--fg); }

/* ------------------------------------------------------------- the rail -- */

/* THE RAIL IS AS WIDE AS THE STREAMER BAR NEEDS, and that width is arithmetic
   rather than taste: five avatars at the client's DIAM (70) plus four GAP_H
   gaps (14) plus its PAD_L either side (16) is 350 + 56 + 32 = 438, and the
   card's own padding is 10 a side. 460 clears it. At 430 the fifth streamer
   wrapped onto a second row, which is not a smaller bar - it is a different
   layout. */
.split { display: grid; grid-template-columns: minmax(0, 1fr) 460px; gap: 12px; }

/* The left column claims the same budget the rail already does, and hands the
   slack to the chart. `min-height`, not `height` - when even the floors do not
   fit, the page has to SCROLL rather than clip. That direction matters: the
   desktop build this came from once forced its canvas taller than the screen
   actually had room for and lost the bottom of the chart off-screen with no
   way to reach it, which is strictly worse than a scrollbar. */
/* THE CHART IS A FIXED SIZE, LIKE THE CLIENT'S. Neither growing nor shrinking.
   Both were tried and both were wrong in opposite directions: shrink-to-fit
   crushed it to a sliver to avoid a scrollbar, and grow-into-slack blew the
   plot band out to 448px against the client's 320 the moment the column had
   room going spare. The client does not stretch its docked chart either - it
   is 420 and the column simply ends where it ends. */
.left { display: flex; flex-direction: column; }
.left > * { flex: none; }
#chartcard { display: flex; flex-direction: column; }
.left { min-width: 0; }

/* The rail holds two stacked cards - the streamer row, then the logs - not one
   panel with a row glued on top. */
/* The rail fills the column height, and the log card takes whatever the
   avatar row leaves - otherwise the log list stops partway down and the space
   below it sits empty, which reads as the log having run out rather than the
   pane being short. */
/* THE RAIL HAS TO LEAVE ROOM FOR WHAT COMES AFTER IT, and it did not. It
   claimed the window less its own offset, but the settings bar and the footer
   live BELOW the split - 94px the rail had already spent. On a window tall
   enough for everything else, the page still scrolled, and it scrolled by
   exactly that much: the rail was the only thing overflowing.

   Measured off the page rather than guessed: header 42, split starts at 52,
   settings bar 27 + footer 26 and the gaps around them come to 94. */
/* Height comes from the row now, so the rail ends where the left column does.
   The cap is still needed for the other direction: when the left column is
   taller than the window - a short screen, where the page has to scroll - the
   rail must not grow past what can be seen, or its list scrolls off the
   bottom instead of inside itself.

   MEASURED OFF THE PAGE, NOT ASSUMED, and re-measured whenever anything
   joins or leaves that chrome - a stale figure here does not look like a bug,
   it looks like a page that scrolls for no reason, or one with a band of dead
   space along the bottom. Today: the split starts at 52, and below it sit the
   panel's own 40px of padding and the 26px footer, so 66. It was 94 while the
   settings strip lived down there; that strip is in the header now. */
.rail { display: flex; flex-direction: column; gap: 8px;
        position: sticky; top: 50px;
        max-height: calc(100vh - 52px - 66px); }
.streamercard {
  background: var(--card); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 10px; flex: none;
}
/* A flex child of the rail, and nothing more. It used to carry its own sticky
   position and its own copy of the window-height sum, which the rail was
   already doing one level up - two rules for one height, and the flex line
   won anyway. It takes what the avatar row leaves. */
.logcard {
  background: var(--card); border: 1px solid var(--border); border-radius: 8px;
  display: flex; flex-direction: column; flex: 1 1 auto;
  min-height: 0;
}
.railhead {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 12px; border-bottom: 1px solid var(--border);
}
.railtitle {
  font-size: 10.5px; letter-spacing: .13em; text-transform: uppercase;
  color: var(--fg); font-weight: 700;
}
.railcount { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--faint); }

/* THE HEADER IS THE TOGGLE. Two titles where there was one, rather than a
   control bolted beside a title - the card has two contents and the heading
   says which is showing, which is the same job it was already doing.

   Centred, and the only thing in the row. Pinned left it read as a label with
   dead space after it rather than as a pair to choose between, and the total
   that used to sit at the far right was the same figure the All tab already
   carries a few pixels below - one number, in the place that can act on it. */
.railhead:has(> .railswitch:only-child) { justify-content: center; }
.railswitch { display: flex; gap: 22px; }
.railswitch button {
  appearance: none; background: none; border: 0; cursor: pointer;
  padding: 3px 2px; font: inherit;
  font-size: 13px; letter-spacing: .1em; text-transform: uppercase;
  font-weight: 700; color: var(--faint);
  border-bottom: 2px solid transparent;
}
.railswitch button:hover { color: var(--muted); }
.railswitch button.on { color: var(--fg); border-bottom-color: var(--accent); }
.railswitch .n {
  font-family: var(--mono); font-size: 13px; letter-spacing: 0;
  color: var(--muted); margin-left: 7px; font-weight: 600;
}

/* Each view takes what the header leaves. Only the list inside scrolls, so
   switching between them never moves the card or the page. */
.railview { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.railview[hidden] { display: none; }
/* In the rail the slot list fills the card instead of being capped - the cap
   existed to stop it dominating a column it no longer sits in. */
.railview .slotgrid {
  margin-top: 0; max-height: none; flex: 1 1 auto; padding: 7px 10px;
}

/* The two lists that actually scroll. `color-scheme` already makes the bar
   dark; this narrows it and pulls the thumb in off the card edge, because a
   full-width bar hard against a rounded border is the one place the default
   still looks bolted on. */
.loglines, #slotgrid { scrollbar-width: thin; scrollbar-color: var(--track) transparent; }
.loglines::-webkit-scrollbar,
#slotgrid::-webkit-scrollbar { width: 10px; }
.loglines::-webkit-scrollbar-track,
#slotgrid::-webkit-scrollbar-track { background: transparent; }
.loglines::-webkit-scrollbar-thumb,
#slotgrid::-webkit-scrollbar-thumb {
  background: var(--track); border-radius: 6px;
  border: 3px solid var(--card); background-clip: padding-box;
}
.loglines::-webkit-scrollbar-thumb:hover,
#slotgrid::-webkit-scrollbar-thumb:hover { background: var(--button-hover); background-clip: padding-box; }

.railtabs { display: flex; flex-wrap: wrap; gap: 3px; padding: 7px 10px; border-bottom: 1px solid var(--border); }
.railtabs button {
  appearance: none; cursor: pointer; font: inherit; font-size: 11px;
  background: var(--button); color: var(--muted);
  border: 1px solid var(--border); border-radius: 4px; padding: 2px 9px;
  text-transform: capitalize;
}
.railtabs button:hover { background: var(--button-hover); color: var(--fg); }
.railtabs button.on { background: var(--accent); border-color: var(--accent); color: #17130a; font-weight: 700; }

/* The log body. Monospace and dense on purpose - these are scanned for a
   shape, not read as prose, and a proportional font makes columns of
   timestamps impossible to run an eye down. */
.loglines {
  flex: 1; overflow-y: auto; padding: 7px 10px;
  font-family: var(--mono); font-size: 11.5px; line-height: 1.5;
}
.logline { display: flex; gap: 8px; margin-bottom: 2px; }
.logline .ts { color: #4f7e86; flex: none; }
.logline .tag { flex: none; font-weight: 700; min-width: 34px; }
.logline .msg { color: var(--fg-2); word-break: break-word; }
.logline.bad .msg { color: var(--danger); }
.logline.good .msg { color: var(--green); }
.tag-PRED { color: var(--accent); }
.tag-HUNT { color: #ffd88a; }
.tag-SEAT { color: var(--teal); }
.tag-SYS  { color: #6fae9a; }
.tag-ERR  { color: var(--danger); }
.logline .rep { color: var(--faint); }
.logempty { color: var(--faint); padding: 8px 2px; }

@media (max-width: 1240px) {
  .split { grid-template-columns: 1fr; }
  .rail { position: static; height: 380px; }
}

/* The band strip lives in the same slot as the gaps readout, below the plot,
   and the two are switched between rather than stacked. Stacking them would
   push the chart up and make the panel taller than the thing it describes. */
.switch {
  appearance: none; border: 0; background: none; cursor: pointer;
  color: var(--faint); font-size: 11px; padding: 0 2px; line-height: 1;
}
.switch:hover { color: var(--accent); }
.switch.on { color: var(--accent); }
/* Three cells with a rule between them, the middle one taking the rest -
   the client's layout, and the reason the end cells are fixed-width is the
   same reason it packs them first: an axis that can expand will take
   everything if it is allowed to go first. */
.bandstrip { border-top: 1px solid var(--border); padding: 0; }
.bandrow { display: flex; align-items: center; gap: 0; padding: 8px 4px; }
.bandcell { flex: none; padding: 0 12px; }
/* The middle cell of the gap mode takes the room the axis takes in band mode,
   so the two read as one component in two states rather than two components. */
.bandcell-mid { flex: 1 1 auto; min-width: 0; text-align: center;
                border-left: 1px solid var(--border);
                border-right: 1px solid var(--border); }
.bandcell-mid .k, .bandcell-mid .v { text-align: center; }
.bandcell-mid .v { font-size: 12px; font-weight: 400; color: var(--muted);
                   font-family: inherit; }
.bandcell .v.muted { font-size: 12px; font-weight: 400; color: var(--muted);
                     font-family: inherit; }
.bandrow .switch { flex: none; margin-left: 8px; }
.bandcell .k {
  font-size: 9.5px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--faint); text-align: center; white-space: nowrap;
}
.bandcell .v {
  font-size: 17px; font-weight: 700; color: var(--fg); text-align: center;
  font-family: var(--mono); white-space: nowrap;
}
/* Green above break-even, red below. The colour IS the reading - a median of
   0.94x and one of 1.04x are a hair apart as numbers and opposite as answers. */
.bandcell .v.up { color: var(--green); }
.bandcell .v.down { color: var(--danger); }
/* Provisional: the bar is real but still falling while slots are acquired, so
   it wears the bonus-hunt amber rather than reading as settled. */
.bandcell .v.provisional { color: var(--accent); }
.bandcell .v .n {
  font-size: 11px; font-weight: 700; color: var(--muted);
  font-family: inherit; margin-left: 6px;
}
.bandcell-right { text-align: right; }
.bandcell-right .k, .bandcell-right .v { text-align: right; }
.bandrow #bandaxis {
  flex: 1 1 auto; min-width: 0;
  border-left: 1px solid var(--border); border-right: 1px solid var(--border);
}
#bandaxis { display: block; width: 100%; height: 72px; }
#bandaxis svg, #bandbars svg, #stripcanvas svg { display: block; }
#bandbars { display: block; width: 100%; height: 96px; margin-top: 8px; }

/* No expanded view. The client's docked chart is the compact one and this
   page shows only that, so the .is-big rules that used to live here went with
   the button. The expanded geometry is still in the renderer and still held
   to the client's own render by tools/diff_chart.py - it simply has no way in
   from the page. */

/* The streamer bar. The active one is ringed in amber, which is the only
   thing on the row that needs to be readable at a glance. */
/* NO WRAPPING AND NO SCROLLBAR. One row is the layout; five avatars over two
   rows is a different thing that happens to contain the same pictures.
   Measured: each cell is 74 (the client's DIAM of 70 plus its 2px ring either
   side) and the gap is its GAP_H of 14, so five need 5*74 + 4*14 = 426. The
   rail's card gives 438 of content. It fits with room, and the horizontal
   scrollbar that appeared was purely this rule's own 16px of side padding
   pushing 426 over the edge. */
.streamers { display: flex; gap: 14px; flex-wrap: nowrap;
             justify-content: center; padding: 14px 16px; }
.streamer {
  appearance: none; cursor: pointer; border: 0; background: none; padding: 2px;
  display: flex; flex-direction: column; align-items: center; gap: 0;
  /* The reference sets these in POINTS - 9 for the name, 8 for the status
     line, 6 for the long one. At 96dpi that is about 12, 10.5 and 8 pixels,
     which is what these are. The name was bumped there for readability and
     shrinking it again here would undo that. */
  font: inherit; font-size: 12px; color: var(--muted);
}
/* THE GAP UNDER THE AVATAR IS 14, AND IT IS NOT ARBITRARY.
   It was 3, which is smaller than the rings are wide. On an avatar that is
   both live and selected the two stack out to about 12px past the picture, so
   the outer ring's bottom arc was drawn straight through the name below it -
   which reads as broken text rather than as a ring that is too big.
   The row below it is 1, which is the reference's own spacing. */
.streamer .who { margin-top: 14px; }
.streamer .status { margin-top: 1px; }
/* 70, which is the client's own StreamerSelectorBar.DIAM - it went 44 -> 64
   -> 70 there and this had stopped at 54. The rest of that bar's spacing is
   its PAD_L (16), GAP_H (14) and LABEL_GAP/LABEL_H (16) below. */
.streamer img, .streamer .init {
  width: 70px; height: 70px; border-radius: 50%; display: block;
  border: 2px solid transparent; box-sizing: border-box;
  /* Dimmed until this is the streamer being tracked - the row is a state
     readout, not a set of equal buttons, and only one of them is live. */
  filter: brightness(0.42) saturate(0.7);
  transition: filter .15s ease;
}
.streamer .init {
  display: grid; place-items: center; color: #fff;
  font-weight: 700; font-size: 15px; letter-spacing: .02em;
}
.streamer:hover img, .streamer:hover .init { filter: brightness(0.75); }
/* Not being run by this service. Left visible, because the row is a readout of
   the whole field rather than a set of buttons, but plainly not a thing to
   press - a click that resolves back to the default board is worse than no
   click at all. */
.streamer.off { cursor: default; opacity: .45; }
.streamer.off:hover img, .streamer.off:hover .init { filter: brightness(0.42) saturate(0.7); }

/* TWO CONCENTRIC RINGS, and they stack. Ported from the bar this came from
   rather than approximated: the red ring hugs the avatar and means that board
   has a hunt running; the amber ring sits FURTHER OUT and means this is the
   board being looked at. When both apply the red is the inner one.

   The spread values are that bar's own spacing - 4px clear of the avatar, a
   3px ring, a 2px gap, then the 3px ring - drawn as shadows so they cost the
   avatar no layout and cannot shift the row as boards come in and out of a
   hunt. A border would resize the picture instead. */
/* UNDIMMED BY BEING LIVE, OR BY BEING THE ONE LOOKED AT. Not by having a hunt.
   
   A hunt on its own used to lift the dim, and with one board being watched that
   was invisible - only one could have a hunt at a time. Feeding five at once
   made every avatar bright and the readout meaningless.
   
   It is also the wrong rule. An offline streamer with a hunt running is treated
   as offline throughout: no ring, no status label, sorted to the right - on the
   grounds that the hunt is quite possibly a community one that is not his. The
   dimming had been left out of that and is now part of it, so every surface
   says the same thing about him. */
.streamer.live img, .streamer.live .init {
  filter: none;
  box-shadow: 0 0 0 4px var(--card), 0 0 0 7px var(--live);
}
.streamer.on img, .streamer.on .init {
  filter: none;
  box-shadow: 0 0 0 4px var(--card), 0 0 0 7px var(--accent);
}
.streamer.on.live img, .streamer.on.live .init {
  box-shadow: 0 0 0 4px var(--card), 0 0 0 7px var(--live),
              0 0 0 9px var(--card), 0 0 0 12px var(--accent);
}
.streamer.on .who { color: var(--accent); font-weight: 700; }
/* A chip behind the name, for the same reason the reference has one: these sit
   under real photographs, and a name has to stay readable whatever brightness
   happens to be behind it. */
.streamer .who {
  line-height: 1; background: var(--card); border-radius: 4px;
  padding: 2px 5px;
}

/* The status line under the name, only present while that board has a hunt.
   It pulses, which is the reference's own behaviour and the reason it reads as
   "right now" rather than as another label: 1.8s, fading most of the way to
   the background and back. */
.streamer .status {
  font-size: 10.5px; font-weight: 700; line-height: 1; color: var(--live);
  letter-spacing: .01em; white-space: nowrap;
  animation: huntpulse 1.8s ease-in-out infinite;
}
/* "Predictions Closed" is far wider than "Predictions Open" and wider than one
   avatar's own column, so at the same size it runs into the neighbour's label.
   The reference drops it to a smaller size for that one case rather than
   letting it collide, and so does this. */
.streamer .status.long { font-size: 8px; letter-spacing: 0; }
@keyframes huntpulse { 0%, 100% { opacity: 1; } 50% { opacity: .12; } }

/* The second chip row sits directly above the slots table, where the figures
   about how the hunt is GOING belong - the top row is about how it was set up. */


/* Nothing else may grow. */
.left > .card { flex: none; }

/* EXCEPT THE LAST ONE, which takes the slack so the two columns' bottom cards
   end on the same line rather than the left one stopping short and leaving
   bare page under it. The row already stretches both COLUMNS to a common
   height; without this the left column's contents simply sat at the top of
   theirs, so the alignment existed in the layout and not on the screen.

   The chart deliberately does not absorb it - it is a fixed 420 - so the slack
   has to land somewhere, and a card is a better place for it than a gap. */
/* Cards carry a bottom margin to space them from the next one. The last card
   has no next one, and that margin was the whole of why the two columns still
   ended eight pixels apart after the row was made to stretch. */
.left > .card:last-child { margin-bottom: 0; }

/* THE SEAT CARD AND THE BAR ROW SIT ON THE PAGE COLOUR, NOT THE CARD COLOUR.
   The strip is painted by the renderer, which fills its own surface with the
   page background - so on a card one step lighter it came out as a black bar
   inside a grey frame, a border the drawing did not ask for and does not line
   up with. Matching them makes the strip read as sitting on the page, which is
   what it does.
   The 1px rules between the seat cells still show: those are the container's
   border colour behind a gap, not a fill, so they survive the cells going
   dark. Card borders stay too - the edge is what defines a card here now. */
#seat-card, #seat-card .seatbar > *, .bandstrip { background: var(--bg); }
/* THE SLACK GOES TO THE RAIL, NOT INTO A CARD. The left column's cards are
   each the size of what they hold, and the chart is deliberately fixed, so on
   a short hunt the column genuinely ends before the window does. Growing the
   last card to close that gap only moves it: a card with two rows of content
   and two hundred pixels of nothing under them is a hole, where the same
   space below the column is just page.
   The rail is a list, and a list filling its pane is what a list should do -
   so the row is held to the window's height and the rail takes it up. */
.left > #seat-card { display: flex; flex-direction: column; }
#seat-card .strip > .vec { width: 100%; }


/* The slot list fills the rail card it now sits in, and the rows scroll inside
   it. The cap this rule used to carry was there to stop a sixty-slot hunt
   running away with the LEFT column; in a card that is already the height of
   the window there is nothing to run away with, and a cap would just leave
   dead space under the last row. */
#slotgrid { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.striphint { display: none; }

/* The settings row, along the bottom. Only the entries that belong to a
   read-only board: the rest of the row in the source configures things this
   service deliberately does not do, and the notification settings arrive with
   that half of the VIP Zone. */
/* IN THE HEADER NOW, beside the tabs. Sized to sit in that row rather than to
   be tucked under the fold: the same padding and radius the tabs use, so the
   four controls read as one row of controls. Kept quieter than a tab - these
   open a panel, they do not change which page you are on, and a filled state
   would claim otherwise. */
.settingsbar { display: flex; gap: 4px; }
.settingsbtn {
  appearance: none; cursor: pointer; font: inherit; font-size: 12.5px;
  background: none; color: var(--muted);
  border: 1px solid transparent; border-radius: 5px; padding: 4px 12px;
  white-space: nowrap;
}
.settingsbtn:hover { background: var(--button); border-color: var(--border); color: var(--fg); }
.settingsbtn[disabled] { opacity: .4; cursor: default; }
.settingsbtn[disabled]:hover { background: none; border-color: transparent; color: var(--muted); }
.settingsbtn .gear { color: var(--faint); margin-right: 5px; }

/* ---------------------------------------------------------- Hunt Cards -- */
/* A panel, not a page. It covers what it is changing so the row above the
   chart stays visible behind it - the whole point of the settings is what
   they do to that row. */
.modal {
  position: fixed; inset: 0; z-index: 50; display: flex;
  align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.55); padding: 20px;
}
.modalbox {
  background: var(--bg); border: 1px solid var(--border); border-radius: 10px;
  max-width: 620px; width: 100%; max-height: 90vh; display: flex;
  flex-direction: column; box-shadow: 0 18px 48px rgba(0, 0, 0, 0.5);
}
.modalhead {
  padding: 12px 16px; border-bottom: 1px solid var(--border);
  font-weight: 700; font-size: 14px;
}
.modallede {
  margin: 0; padding: 12px 16px 4px; color: var(--muted); font-size: 12px;
  line-height: 1.55;
}
.modallede b { color: var(--fg); }
.cardgrid { overflow-y: auto; padding: 8px 16px 12px; }
.cardrow {
  display: grid; grid-template-columns: 1fr repeat(3, 88px);
  align-items: center; gap: 6px; padding: 3px 0;
}
.cardrow > span { text-align: center; }
.cardhead {
  color: var(--faint); font-size: 11px; font-weight: 700;
  border-bottom: 1px solid var(--border); padding-bottom: 6px; margin-bottom: 4px;
}
.cardname { text-align: left; font-size: 12.5px; color: var(--fg); }
/* ON is a filled state and OFF is not, so the column reads as a shape before
   it reads as words - the client's panel is scanned far more often than it is
   changed. */
.toggle {
  appearance: none; cursor: pointer; font: inherit; font-size: 11px;
  font-weight: 700; border-radius: 4px; padding: 3px 0; width: 100%;
  background: none; border: 1px solid transparent; color: var(--faint);
}
.toggle.on {
  background: #1d4f34; border-color: #2f7d50; color: #9be8b8;
}
.modalfoot {
  display: flex; gap: 8px; padding: 12px 16px;
  border-top: 1px solid var(--border);
}
.modalfoot .ghost:last-child { margin-left: auto; }

/* The operator switch. Styled as one of the settings controls rather than as
   something in the board itself: it changes what the model does for everyone
   looking, which is not a per-viewer preference and should not sit among the
   per-viewer ones. */
.settingsbtn.switch { display: inline-flex; align-items: center; gap: 6px;
                      cursor: pointer; }
.settingsbtn.switch input { accent-color: var(--accent); cursor: pointer; margin: 0; }
.settingsbtn.switch.on { color: var(--accent); border-color: var(--accent); }

/* Which board a log line belongs to. Five hunts run at once, so a line
   reading `opened "Ze Zeus"` with no board named belongs to any of them. Sized
   and coloured to sit between the tag and the message without competing with
   either - it is context, not the event. */
.logline .who {
  flex: 0 0 auto;
  min-width: 3.6em;
  color: var(--muted-fg, #a4a4ad);
  font-size: 11px;
  letter-spacing: .02em;
  opacity: .85;
}
