/* Shared styles for the fantasy football site. */

:root {
  /* Surfaces, stepped. --panel-3 is the first step that is unmistakably
   * lighter than --panel at a glance; --panel-2 is a whisper, so it is used
   * for fills that only need to read as "not flat", never as state. */
  --bg: #0f1115;
  --panel: #171a21;
  --panel-2: #1c2029;
  --panel-3: #232833;
  --row-alt: #1a1d25;   /* even-row zebra band: visible, still quieter than hover */

  --line: #272c36;
  --line-2: #39414f;    /* louder rule, for bracketing column groups */

  --text: #e6e8ec;
  --dim: #8b93a1;

  --accent: #3ba55d;
  --accent-hi: #45b869; /* accent hover/active */
  --on-accent: #06210f; /* text that sits on an --accent fill */
  --err: #e0525f;
  --warn: #d9a441;
  /* AN ASSUMED NUMBER: one the site supplied because ESPN's was below what the
   * waiver wire would give you at that position (Tim, 2026-09-18 — "color code
   * them in orange or something to show it's assumed"). See js/floor.js.
   *
   * Deliberately a clear ORANGE and not the amber --warn above, because the two
   * appear on the same cells and mean opposite things: amber is "this number is
   * bad", orange is "this number is not ESPN's". Half a shade apart would make
   * them one cue with two meanings. Never used alone — every place it appears
   * carries a dotted underline and words as well. */
  --assumed: #e8833a;
  /* Neutral informational blue, for a quantity that is neither good nor bad --
   * a roster-strength bar, a "this is the row you picked" edge. Distinct from
   * --accent on purpose: green here would read as "winning".
   * It was briefly deleted as unused, which left the home page's strength bars
   * with no fill at all, so: index.html and analysis.html both use it. */
  --good: #4fb0e0;
  /* The Trade page's "you GET a man on bye the week you play his manager"
   * pill (2026-09-29). A clear YELLOW, apart from the amber --warn and the
   * orange --assumed, which mean other things on nearby cells. Its partner,
   * "you SEND him", is --accent. */
  --bye-get: #e8d44d;
  --on-bye-get: #262003;

  /* Focus deliberately is NOT --accent: green already means "positive" in the
   * tables, and a focus ring that reads as a value would be ambiguous. */
  --focus: #6ea8fe;

  /* Categorical series palette. Owned here so a team's colour can be matched
   * in CSS (swatches, table accents) as well as in charts.js. See the palette
   * note at the top of js/charts.js before changing any of these - they were
   * validated for colour-vision deficiency as a set. */
  --series-1: #3987e5;
  --series-2: #d95926;
  --series-3: #199e70;
  --series-4: #a540bb;
  --series-5: #c98500;
  --series-6: #9085e9;
  --series-7: #008300;
  --series-8: #d55181;
  --series-9: #105fd9;
  --series-10: #e66767;

  /* Radius scale. Everything rounds to one of these five. */
  --r-1: 4px;
  --r-2: 6px;
  --r-3: 8px;
  --r-4: 10px;
  --r-pill: 999px;

  /* Panel padding. The table bleed (.table-scroll) has to be the exact
   * negative of this or the table stops lining up with the panel edge, so
   * both read the same token instead of repeating the number. */
  --pad-panel: 18px;

  /* The site is dark. Without this, form controls, scrollbars and the caret
   * are drawn with the browser's light chrome. */
  color-scheme: dark;
}

* { box-sizing: border-box; }

/* `hidden` MEANS hidden, whatever else the element's class says. The browser's
 * own `[hidden] { display: none }` is the weakest rule there is, so any class
 * that sets `display` quietly overrides it — which is exactly how the Trade
 * page's pop-up (`.modal { display: flex }`) sat over the page from load with a
 * Close button that set `hidden` and changed nothing on screen. Every page's
 * JS toggles `.hidden`, so this is the one place to make it stick. */
[hidden] { display: none !important; }

/* iOS Safari inflates the text of any block it decides is too narrow to read,
 * which on a rotate silently re-sizes half the page and pulls every table out
 * of alignment with the one above it. The phone layout below sets its own
 * sizes; this stops the browser second-guessing them. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  padding: 0 0 80px;
  -webkit-font-smoothing: antialiased;
}

.wrap { max-width: 1180px; margin: 0 auto; padding: 0 20px; }
/* Data pages: a 17-column standings table and ten teams at once need the room.
 * Apply to the header's .wrap too, or the nav stops lining up with the page. */
.wrap.wide { max-width: 1600px; }

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

header.site {
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  padding: 14px 0;
  margin-bottom: 26px;
}
header.site .wrap { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
header.site .brand { font-weight: 650; font-size: 15px; letter-spacing: -0.01em; }
header.site nav { display: flex; gap: 4px; margin-left: auto; }
header.site nav a {
  color: var(--dim);
  text-decoration: none;
  font-size: 14px;
  padding: 6px 11px;
  border-radius: var(--r-2);
}
header.site nav a:hover { color: var(--text); background: var(--panel-2); }
/* "Where am I" has to survive a glance from across the desk, so the active
 * page gets accent ink, a real surface step and an underline - not the ~2%
 * luminance nudge that hover uses. */
header.site nav a.active {
  color: var(--accent-hi);
  background: var(--panel-3);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--accent);
}

h1 { font-size: 22px; margin: 0 0 4px; letter-spacing: -0.01em; }
.sub { color: var(--dim); font-size: 14px; margin: 0 0 26px; }

/* -------------------------------------------------------------------- focus
 * One ring for everything focusable. :focus-visible so a mouse click never
 * paints it, which is why the old `outline: none` was there in the first
 * place - this keeps the mouse clean without blinding the keyboard.
 */

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

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

section.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-4);
  padding: var(--pad-panel);
  margin-bottom: 16px;
}
section.panel > h2 {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
  margin: 0 0 14px;
  font-weight: 600;
}
.panel-note { color: var(--dim); font-size: 13px; margin: 12px 0 0; }

/* ------------------------------------------------ reading aids (2026-09-16)
 *
 * Tim: "the site is really messy and wordy". Two pieces, used on every page:
 *
 *   .lede         ONE short sentence under a panel's title, always visible —
 *                 what the panel shows, in plain words. Aim for under 25.
 *   details.explain
 *                 everything else a panel used to print under its table: the
 *                 method, the caveats, the edge cases. Closed by default, one
 *                 click away, and still a real part of the document (so search,
 *                 screen readers and the test suites all still reach it).
 *
 *   <p class="lede">Every trade where both lineups get better.</p>
 *   …table…
 *   <details class="explain">
 *     <summary>How this works</summary>
 *     <p class="panel-note" id="tradeNote"></p>
 *   </details>
 *
 * The notes keep their ids and their class, so page modules fill them exactly
 * as before; only where they sit changed. */
.lede { color: var(--dim); font-size: 13.5px; margin: -6px 0 14px; max-width: 72ch; }

details.explain {
  margin-top: 14px;
  border-top: 1px solid var(--line);
  padding-top: 10px;
}
details.explain > summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--dim);
  font-size: 12.5px;
  font-weight: 600;
  user-select: none;
}
details.explain > summary::-webkit-details-marker { display: none; }
details.explain > summary::before {
  content: '';
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.15s ease;
}
details.explain[open] > summary::before { transform: rotate(45deg); }
details.explain > summary:hover { color: var(--text); }
/* "How this works" is 18.8px tall, and it is on 27 panels — the worst of the
 * four sub-44px tap targets the density audit measured. It gets the 44px floor
 * only where there are FINGERS: keyed off `hover: none` (no pointer), never off
 * the width, per the two-media-features rule. On a mouse it stays tight, which
 * is the whole point of a density pass — 27 panels x 25px is 675px of height
 * that a cursor does not need. */
@media (hover: none) {
  details.explain > summary { min-height: 44px; }
}
details.explain > .panel-note,
details.explain > p { margin-top: 8px; max-width: 90ch; line-height: 1.6; }

/* A note that changes what a number MEANS, so it stays visible rather than
 * going behind the "How this works" fold. It was page-local to trade.html
 * until the schedule page needed the same thing for a divisional league.
 * Defined after `.panel-note` deliberately: an element carries both classes
 * and they have equal specificity, so source order is the whole of why this
 * one wins the colour. */
.warn-line { color: var(--warn); }
.warn-line strong { color: var(--warn); }

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

.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 5px; }

input, select {
  background: #0d0f13;
  border: 1px solid var(--line);
  color: var(--text);
  border-radius: var(--r-2);
  padding: 9px 11px;
  font: inherit;
  font-size: 14px;
  width: 100%;
}
/* Border tint on top of the shared ring, so the field itself reads as active. */
input:focus-visible, select:focus-visible { border-color: var(--focus); }

button {
  background: var(--panel-3);
  border: 1px solid var(--line);
  color: var(--text);
  border-radius: var(--r-2);
  padding: 9px 15px;
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
button:hover:not(:disabled) { background: #2b3140; }
button:disabled { opacity: 0.45; cursor: default; }
button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}
button.primary:hover:not(:disabled) { background: var(--accent-hi); }

/* A labelled control. A segmented toggle says what its OPTIONS are and never
 * what the CHOICE is, which is fine alone and ambiguous the moment there are
 * two of them side by side. It matters more on a phone than it reads here:
 * js/touch-titles.js deliberately leaves buttons alone — a tap on a control has
 * to work the control — so a control whose only explanation is a `title` has no
 * explanation at all there. Shared by the Players page's four filters and the
 * schedule page's run count. */
.ctl { display: flex; flex-direction: column; gap: 6px; }
.ctl-label {
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dim);
  font-weight: 600;
}
/* One option that needs a sentence, said under the control rather than in a
 * `title` on the button — which is a sentence a phone can never read. */
.ctl-hint { font-size: 12px; color: var(--dim); max-width: 62ch; }

/* Segmented toggle */
.segmented { display: inline-flex; border: 1px solid var(--line); border-radius: var(--r-2); overflow: hidden; }
.segmented button { border: none; border-radius: 0; background: transparent; padding: 8px 16px; }
.segmented button + button { border-left: 1px solid var(--line); }
.segmented button.on { background: var(--accent); color: var(--on-accent); font-weight: 600; }
/* overflow:hidden clips an outside ring, so pull it inside the track. */
.segmented button:focus-visible { outline-offset: -2px; }

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

.badge {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: var(--r-pill);
  vertical-align: middle;
}
.badge.demo { background: #3a2f10; color: var(--warn); border: 1px solid #5a4718; }
.badge.live { background: #10331d; color: var(--accent); border: 1px solid #1d5330; }

/* -------------------------------------------------------------------- table
 * `position: sticky` on <th> only sticks against a real scrollport. Because
 * this element scrolls (overflow), it - not the page - is what the header
 * measures itself against, so without a max-height there is nothing to scroll
 * inside and the header just leaves with the page. The max-height IS the fix.
 */

.table-scroll {
  overflow: auto;
  max-height: 70vh;
  /* Bleed to the panel edge. Must stay the exact negative of --pad-panel. */
  margin: 0 calc(-1 * var(--pad-panel));
  padding: 0 var(--pad-panel);
}

table {
  border-collapse: collapse;
  width: 100%;
  font-size: 13.5px;
  /* 13.5px x 1.35 + 10px padding = ~28px a row, so ten teams land in ~280px
   * and fit on screen together. The body's 1.5 was ~37px and pushed the last
   * two teams below the fold. */
  line-height: 1.35;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
th, td { padding: 5px 9px; text-align: right; border-bottom: 1px solid var(--line); }
th {
  color: var(--dim);
  font-weight: 600;
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  position: sticky;
  top: 0;
  background: var(--panel);
  -webkit-user-select: none;
  user-select: none;
  /* Above the body cells, including the sticky-left first column, which would
   * otherwise paint over the header as it scrolls past. */
  z-index: 2;
}
/* A grouping band above the real header row (Record | Scoring | Ranks ...).
 * Both rows are <th>, so both would stick to top:0 and sit on top of each
 * other; the band gives up stickiness and lets the labels win. sortable.js
 * reads the LAST header row, so the band is invisible to sorting. */
tr.colgroup th {
  position: static;
  text-align: center;
  color: var(--dim);
  font-size: 10.5px;
  letter-spacing: 0.09em;
  padding-top: 2px;
  padding-bottom: 2px;
  border-bottom: 0;
}
tr.colgroup th + th { border-left: 1px solid var(--line-2); }

/* Only headers wired up by sortable.js advertise themselves as clickable. */
th.sortable { cursor: pointer; }
th.sortable:hover { color: var(--text); }
/* Header cells have no room outside themselves for a ring - inset it. */
th.sortable:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
th.sorted { color: var(--text); }
th.sorted::after { content: " \25BE"; color: var(--accent); }
th.sorted.asc::after { content: " \25B4"; }

td.name, th.name {
  text-align: left;
  font-weight: 550;
  position: sticky;
  left: 0;
  background-color: var(--panel);
  /* The frozen column casts an edge and a shadow over whatever it is covering,
   * which is also the standing cue that more columns run off to the right.
   * (A gradient on .table-scroll - the usual scroll-shadow trick - cannot work
   * here: it paints behind the cells, and zebra plus this sticky column make
   * those opaque.) The border is the part guaranteed to render; the shadow is
   * the part that makes the overlap legible where a browser paints it. */
  border-right: 1px solid var(--line-2);
  box-shadow: 6px 0 7px -7px rgba(0, 0, 0, 0.85);
}
td.name { z-index: 1; }
/* The corner is sticky in both axes, so it has to outrank both of them. */
th.name { z-index: 3; }

/* Zebra. td.name carries its own opaque background (it has to - it is sticky
 * and scrolls over other cells), so it needs banding stated explicitly or the
 * frozen column stripes out of phase with the rest of its row.
 * This must stay ABOVE the hover and .me rules: they tie on specificity and
 * win only by coming later.
 * All three set background-COLOR, never the `background` shorthand: the
 * shorthand resets background-image to none and erases the red/green tint on
 * every cell it matches (AUDIT §1.1; tests/heat-draw-check.mjs). */
tbody tr:nth-child(even) td,
tbody tr:nth-child(even) td.name { background-color: var(--row-alt); }

/* Written as `tbody tr.me td` (not `tr.me td`) purely to out-specify the
 * zebra rule above; hover still comes later and still wins. */
tbody tr.me td { background-color: #14261a; }
tbody tr.me td.name { background-color: #14261a; color: #7fd99b; }

/* Row hover is behind `hover: hover` because a touch screen has no hover to
 * leave: iOS resolves :hover on tap and then keeps it painted until something
 * else is tapped, so on a phone the row you last touched stays lit as though it
 * were selected. These tables already say "you" (tr.me) and "the one you
 * picked" (tr.picked) with their own colours, and a third, meaningless
 * highlight competing with them is worse than no hover at all. */
@media (hover: hover) {
  tbody tr:hover td { background-color: var(--panel-3); }
  tbody tr:hover td.name { background-color: var(--panel-3); }
}

/* `td.win` / `td.lose` lived here for the Schedule page's Results table, the
 * only thing on the site that put a bare `win`/`lose` class on a <td>. That
 * panel was deleted on 2026-09-23 (it was ESPN's scoreboard with a sort), and
 * the rules went with it. The matchup cards style their own winners through
 * `.side.win` / `.side.lose` inside schedule.html and index.html. */

.pos { color: var(--accent); }
.neg { color: var(--err); }
.muted { color: var(--dim); }

/* ------------------------------------------- THE SHARED RED/GREEN SCALE
 *
 * Tim, 2026-09-19: "colorizing everything red/green based on a comparison with
 * other positions ... I want the range to be a lot tighter so it's easier to be
 * in green/red, not just the extremes ... on a spectrum or something so that
 * the more red or more green shows better information."
 *
 * The arithmetic, the step boundaries and the defence of both live in
 * js/heat.js; this is only how a step is painted. Classes rather than inline
 * styles so the scale is themeable in ONE place and so a test can assert a
 * cell's step — the thing this replaced on the Stats page was a per-render
 * `rgba()` ramp that could be neither.
 *
 * IT OWNS THE BACKGROUND AND THE WEIGHT. NOTHING ELSE. That is what lets it sit
 * on top of everything already built: this site spends the FOREGROUND of a cell
 * on meanings that must not be overwritten — orange `--assumed` ("this number
 * is not ESPN's"), the injury reds, "Bye" and "OUT", `.pos`/`.neg` — so a cell
 * can be green here AND orange-assumed at the same time and both claims
 * survive. Any page style that needs to own a cell's background back (the
 * season sheet's cross-week `.lit`, for instance) simply comes later in its own
 * stylesheet.
 *
 * THE TINT IS A `background-image`, NOT A `background-color`, and that is the
 * load-bearing trick in this block rather than a stylistic choice. Three rules
 * in this file already own a cell's background COLOUR and every one of them is
 * more specific than a bare class: the zebra band
 * (`tbody tr:nth-child(even) td`), row hover (`tbody tr:hover td`) and the
 * season sheet's cross-week highlight. A background-color here would have been
 * wiped off every even row on the site and would have needed a specificity
 * arms race to get back. A background-IMAGE is a different property: the flat
 * gradient composites ON TOP of whatever colour is underneath, so zebra and
 * hover both show through the alpha and both keep working untouched, with no
 * hover variants to maintain.
 *
 * It also gives the one override that IS wanted for free: a page rule using the
 * `background` SHORTHAND resets background-image to none, so the season sheet's
 * `#seasonTable td.lit` still takes a cell's background completely when a
 * reader points at a man — the same precedence the ▼▼ low mark had before this
 * replaced it, and the ▲/▼ and the weight still survive on that cell.
 *
 * FOUR ALPHAS, spaced so two ADJACENT steps are distinguishable without a
 * legend on a dark ground. Step 4 stops at 0.34 because past about 0.4 the
 * green starts competing with `.lit`, the red starts reading as an error state,
 * and --text stops being comfortable on either.
 *
 * AND NEVER COLOUR ALONE. Two of the four cues are in here: the weight climbs
 * with every step (a hue-free reading of magnitude), and `.heatmark` draws the
 * ▲/▼ that js/heat.js puts on the end of the scale (a hue-free reading of
 * direction). The other two are words — on the cell's title and in the key
 * under the table — and are written by js/heat.js.
 *
 * THE WEIGHT IS DECLARED SEPARATELY, BELOW THE TINTS, at a higher specificity.
 * That is a bug fix rather than a layout preference — see the long note on it —
 * and the two halves are kept apart because they need different specificities
 * for opposite reasons: the tint is a background-IMAGE precisely so it does NOT
 * have to out-rank anything, and the weight has to. */
.heat { font-variant-numeric: tabular-nums; }
/* A measured-but-normal cell. It draws nothing, but it carries the class so a
 * stylesheet and a test can tell "inside the middle band" from "this scale
 * refused to draw at all", which look identical otherwise. */
.heat-0 {}

.heat-up-1 { background-image: linear-gradient(rgba(59, 165, 93, 0.09), rgba(59, 165, 93, 0.09)); }
.heat-up-2 { background-image: linear-gradient(rgba(59, 165, 93, 0.17), rgba(59, 165, 93, 0.17)); }
.heat-up-3 { background-image: linear-gradient(rgba(59, 165, 93, 0.25), rgba(59, 165, 93, 0.25)); }
.heat-up-4 { background-image: linear-gradient(rgba(59, 165, 93, 0.34), rgba(59, 165, 93, 0.34)); }

.heat-dn-1 { background-image: linear-gradient(rgba(224, 82, 95, 0.09), rgba(224, 82, 95, 0.09)); }
.heat-dn-2 { background-image: linear-gradient(rgba(224, 82, 95, 0.17), rgba(224, 82, 95, 0.17)); }
.heat-dn-3 { background-image: linear-gradient(rgba(224, 82, 95, 0.25), rgba(224, 82, 95, 0.25)); }
.heat-dn-4 { background-image: linear-gradient(rgba(224, 82, 95, 0.34), rgba(224, 82, 95, 0.34)); }

/* THE WEIGHT CHANNEL, AND WHY IT IS WRITTEN AT A HIGHER SPECIFICITY THAN THE
 * TINT ABOVE IT. Do not flatten this back into those eight rules as tidying;
 * it is a bug fix, and the bug fails silently and looks correct.
 *
 * WHAT WENT WRONG (2026-09-19). Weight is one of the four "never colour alone"
 * channels and it is the hue-free one — the reading of MAGNITUDE available to
 * somebody who cannot separate red from green. It used to be declared on the
 * single-class rules above, at specificity 0,1,0, which ANY page-local
 * descendant rule out-ranks. The live case: `index.html`'s
 * `.rank .vv { font-weight: 600 }` (0,2,0, and a page's own <style> is parsed
 * after css/app.css) beats `.heat-up-3`, so a cell on the scale kept its TINT
 * and silently lost its WEIGHT. Nothing looked broken, and no test could see it.
 *
 * SO: 0,2,1, and that number is chosen rather than maximised.
 *   - `.heat.heat-up-1` is the two classes every cell on the scale carries;
 *     `js/heat.js` always emits `heat` alongside the step, which is one of the
 *     reasons it does.
 *   - `:not(html)` matches every element in the document and contributes one
 *     ELEMENT unit — it is a specificity bump, not a filter, and it is written
 *     element-agnostically because a cell on the scale is a <td> on four pages
 *     and a <span> inside an <li> on two others. Naming the elements instead
 *     would put the same silent failure back the first time the scale landed on
 *     a tag nobody had listed.
 *   - It deliberately stops at 0,2,1 rather than going higher. An
 *     ELEMENT-QUALIFIED page rule — `table.grid td.grid-total` (0,2,2), the
 *     Proj avg grid's bold totals column, which is both a totals band and on
 *     the scale — is a deliberate statement about that column and must still
 *     win. Raising this to three classes would quietly lighten every coloured
 *     totals cell on the site, which is a visible change nobody asked for.
 *
 * The four rules pair the two sides rather than spelling out eight, because the
 * ladder is symmetrical by design: a cell far from the mean is heavy whichever
 * side it is on, and the hue is what says which. */
.heat.heat-up-1:not(html), .heat.heat-dn-1:not(html) { font-weight: 500; }
.heat.heat-up-2:not(html), .heat.heat-dn-2:not(html) { font-weight: 550; }
.heat.heat-up-3:not(html), .heat.heat-dn-3:not(html) { font-weight: 620; }
.heat.heat-up-4:not(html), .heat.heat-dn-4:not(html) { font-weight: 700; }

/* The end-of-scale glyph. Sized like the season sheet's old `.lowmark`, which
 * it replaces, so the tables it lands on do not change height. `aria-hidden` on
 * the span is js/heat.js's job: the cell's own title says it in words, and a
 * screen reader announcing "black up-pointing triangle" mid-number is worse
 * than silence. */
.heatmark {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: -0.08em;
  margin-left: 1px;
  opacity: 0.8;
}

/* A rank is an ordinal, not a quantity - shrinking and dimming it stops 1-10
 * from being scanned as though it were a points column. */
.rank {
  color: var(--dim);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Bracket a group of related columns in a wide table. Both names do the same
 * thing so it does not matter which one a page reaches for. */
th.group-start, td.group-start,
th.grouped, td.grouped { border-left: 1px solid var(--line-2); }

/* THE PLAYOFFS START HERE. Tim (2026-09-17): show weeks 15–17 after the
 * regular season, with "a line between 14 and 15 so that it's clear that it's
 * separated from the full season". One class, on the first playoff week's
 * header AND every body cell under it, on every page that lays weeks across —
 * the Players tables, the Analysis week grids and the player card's run.
 * Heavier and lighter than --line-2 so it reads as a boundary, not a group.
 * The header also says "PO" in text (and "playoffs" to a screen reader), so
 * the line is never the only cue. Written to outrank `td.grouped` (later in
 * the file) and the card's `.tc-run .now` edge (page styles, same weight). */
th.po-start, td.po-start,
.tc-run th.po-start, .tc-run td.po-start { border-left: 2px solid var(--dim); }
/* The week you play the other manager in the deal (Tim, 2026-09-21): an
 * arrow stacked under the week number, like PO, so the column keeps its width. */
.tc-run .tc-vs {
  display: block;
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  color: var(--text);
}
.po-tag, .tc-run .tc-po {
  display: block;
  font-size: 8.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  line-height: 1.1;
  color: var(--warn);
}
/* WHAT HAS ALREADY HAPPENED STOPS HERE. Tim (2026-09-19): "put a line after the
 * last week and current week to seperate what's already happened". The same
 * mechanism as `po-start` above, and deliberately the same declaration: a
 * column can be BOTH the first week after the split and the first playoff week,
 * and two rules setting one `border-left` to one value draw one line rather
 * than two. The "PO" tag is what tells the two lines apart when they fall on
 * different columns; `js/player-card.js` names the split week in words under
 * the chart, so neither line is ever the only cue.
 *
 * A LEFT border on the first column AFTER, never a right border on the last
 * column BEFORE: the card's run wraps onto balanced lines, and a right border
 * on the last column of a line is drawn at the edge of a line with nothing
 * after it, where nobody can see it.
 *
 * Scoped to `.tc-run` because the card is the only table that lays a season out
 * in a wrapped run; a page table that wanted the same line would use
 * `po-start`'s unscoped pair above. */
.tc-run th.split-start, .tc-run td.split-start { border-left: 2px solid var(--dim); }

/* THE WEEKS HE STARTS. Bold, as Tim asked — and because NEVER COLOUR OR WEIGHT
 * ALONE is the rule here, also underlined in the accent colour, which is a cue
 * that survives a reader who cannot see weight. `js/player-card.js` adds an
 * `sr-only` word inside the same cell for a reader who sees neither.
 *
 * `text-decoration` rather than a `border-bottom`: a border would change the
 * cell's box and nudge the header row's height, and the thead already carries a
 * hairline under every cell. A decoration is drawn inside the line box and
 * costs no layout at all.
 *
 * IT CANNOT CHANGE A COLUMN'S WIDTH, which matters more here than it looks: the
 * card computes how many weeks fit a line from a CONSTANT (`CHART.col`) rather
 * than by measuring, so a cell that outgrew it would wrap onto a line the
 * arithmetic promised would fit — and the card has had no scroller since Tim
 * asked for it gone. A week number is one or two digits at 10.5px inside a
 * 34px floor, so the Week row never drives a column's width whatever its
 * weight; the Proj row's four-character numbers do, which is why the scale's
 * weight ladder is switched off below.
 *
 * Written at 0,2,2 so it outranks `.tc-run thead th` in the pages' own style
 * blocks, which sets the quieter colour and weight this overrides. */
.tc-run thead th.wk-start {
  color: var(--text);
  font-weight: 800;
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

/* THE SCALE INSIDE THE CARD GIVES UP ITS WEIGHT LADDER, and keeps everything
 * else. Two reasons, both argued at length at the top of js/player-card.js:
 * bold on the week number already means "he starts" one line above, and two
 * meanings on one channel a centimetre apart is what HANDOFF forbids; and a
 * heavier "18.2" is a wider "18.2", which the constant above cannot absorb.
 * The background tint, the ▲/▼ and the words all survive, so the cell is still
 * readable without separating the hues — it loses a hue-free reading of
 * MAGNITUDE in the middle steps and keeps one of DIRECTION at the ends, which
 * is the claim a reader of one man's season actually wants.
 *
 * `font-weight: inherit` rather than a number, so the cell keeps whatever the
 * card's own type is and this rule never has to be kept in step with it.
 *
 * `.tipcard` is in the selector for specificity, not for reach — the run only
 * ever exists inside the card. It puts this at 0,3,2 against the ladder's
 * 0,2,1, so the opt-out wins on specificity rather than on being further down
 * the file, which is the kind of dependency that breaks when somebody moves a
 * block. */
.tipcard .tc-run td.heat { font-weight: inherit; }

/* The end-of-scale glyph, drawn from CSS rather than as a `<span>`.
 *
 * `.heatmark` above is an inline span, and inline is exactly what this column
 * cannot afford — it would widen a 34px cell and it would put a triangle inside
 * the cell's TEXT, where the projection is read back as a number by the suites
 * and by anyone checking a figure against ESPN. Generated content is in neither
 * the text nor the box: `display: block` stacks it under the number the same way
 * `.tc-mk` stacks OUT and `.tc-po` stacks PO, so the column keeps its width.
 *
 * It is decoration, which is why it is allowed to live in a stylesheet at all:
 * the cell's `aria-label` carries the whole sentence, so nothing here is the
 * only way to learn anything. */
.tc-run td.heat-up-4::after,
.tc-run td.heat-dn-4::after {
  display: block;
  font-size: 8.5px;
  font-weight: 700;
  letter-spacing: -0.08em;
  line-height: 1;
  opacity: 0.8;
}
.tc-run td.heat-up-4::after { content: '\25B2'; }
.tc-run td.heat-dn-4::after { content: '\25BC'; }

/* The key's swatch for that line. */
.legend .key .sw.po-key,
.key-line .lg .lg-mark.po-key { border-left: 2px solid var(--dim); }
.legend .key .sw.po-key .po-tag,
.key-line .lg .lg-mark.po-key .po-tag { display: inline; font-size: 10px; }
/* Words for a screen reader and nobody else. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* THE PLAYER CARD'S KEY, WHICH IS HEARD AND NEVER SEEN.
 *
 * Tim, 2026-09-20: "Also in the preview, I want all the words beneath the chart
 * to dissapear-they're not needed." All four blocks that used to sit under the
 * chart — the blank-Act note, the PO note, what bold means / where the heavy
 * line falls / how to read the colour, and the glossary of marks — are still
 * built, still correct and still in the markup, inside this one box. They were
 * 164 of the hover card's 314 pixels at 1500px and 253 of the sheet's 562 at
 * 390px. The argument, and what it costs a reader who cannot separate red from
 * green, is at the top of js/player-card.js.
 *
 * IT REPEATS `.sr-only` RATHER THAN RELYING ON IT, and that is the whole reason
 * this rule exists as well as the class. `.tc-note` and `.tc-legend` are styled
 * in analysis.html's and trade.html's own <style> blocks — a page's inline
 * styles come AFTER this stylesheet, so at equal specificity they win — and one
 * of the things they set is `margin-top: 7px`. A card that had lost its words
 * and kept four 7px margins would be a different defect from the one Tim
 * reported, and would be invisible to every suite. `.tipcard .tc-key` is 0,2,0
 * and beats a page-local `.tc-note` at 0,1,0; `position: absolute` then takes
 * the whole box out of flow, so nothing inside it — margin, padding, border or
 * line-height — can add a pixel to the card whatever a page says about it.
 *
 * Do not give it a `title`: a title anywhere in this card has the browser draw
 * a second tooltip over the card, which is the one thing the card exists to
 * avoid. sr-only and aria-label draw nothing, which is why they are what is
 * used here. */
.tipcard .tc-key {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* THE LINE UNDER THE NAME on the player card: ESPN's Avg, this week's Proj and
 * position rank (Tim, 2026-10-02), as one small line. Styled here rather than
 * in each page's <style> because all three card pages draw it the same way;
 * it reuses the card's own dim and text tokens, no new colour. */
.tipcard .tc-glance {
  font-size: 11.5px;
  color: var(--dim);
  margin-bottom: 4px;
  font-variant-numeric: tabular-nums;
}
.tipcard .tc-glance b { color: var(--text); font-weight: 600; }

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

.chart { width: 100%; margin-top: 4px; }
.chart svg { display: block; width: 100%; height: auto; }
/* charts.js gives its in-SVG legend entries tabindex=0; SVG gets no useful
 * default ring, so state one. */
.ff-legend-item:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* Inline sparkline slot: sized by the container so charts.js can measure it. */
.spark {
  display: inline-block;
  vertical-align: middle;
  width: 90px;
  height: 24px;
  position: relative;
}
.spark svg { display: block; width: 100%; height: 100%; }

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

.empty {
  color: var(--dim);
  font-size: 14px;
  text-align: center;
  padding: 28px 12px;
  border: 1px dashed var(--line);
  border-radius: var(--r-3);
}
.pending {
  border: 1px dashed #4a3c17;
  background: #1a1610;
  color: var(--warn);
  border-radius: var(--r-3);
  padding: 12px 14px;
  font-size: 13.5px;
  margin-top: 12px;
}
.pending strong { color: #f0c463; }

pre {
  background: #0d0f13;
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  padding: 12px;
  overflow: auto;
  max-height: 340px;
  font-size: 12.5px;
  line-height: 1.45;
  margin: 14px 0 0;
  color: #c3cad6;
}
code { background: #0d0f13; padding: 1px 5px; border-radius: var(--r-1); font-size: 12.5px; }

/* ----------------------------------------------------- panels side by side
 *
 * Tim, 2026-09-18: "anywhere we can condense horizontally and fit 2 boxes, we
 * should. This might mean there is a mix between full-width boxes and
 * half-width boxes." So a page is exactly that mix: the wide tables — ten
 * squads by twenty columns, which are the point of this site — stay full
 * width, and the narrow panels around them pair up. Wrap the ones that should
 * share a line in a `.panel-row`.
 *
 * ONLY EVER WRAP ADJACENT PANELS. Panel order is Tim's, page by page, and
 * `tests/stats-order.mjs` and the order assertions in fc/tr/an-test read the
 * rendered document in document order — a wrapper leaves that untouched, but
 * moving a panel to find it a partner would break both the test and his
 * order.
 *
 * `auto-fit` + `minmax(min(<col>, 100%), 1fr)` does two things a fixed
 * `1fr 1fr` cannot:
 *   - a row whose other panel is `[hidden]` gives the whole width to the one
 *     that is left, instead of leaving it in column one with a hole beside it
 *     (`panelEarly` on the Stats page is hidden most of the season);
 *   - `min()` caps the track at the container, so it stacks on a phone with no
 *     media query and can never be wider than the screen.
 *
 * `min-width: 0` on the children is NOT optional and is a bug fix, not tidying.
 * A grid item's automatic minimum is its min-content, so one wide table inside
 * a column pushed the row wider than the page and scrolled the whole DOCUMENT
 * sideways instead of scrolling inside its own `.table-scroll`. Measured on
 * index.html: 571px of content on a 390px phone. */
.panel-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min, 420px), 100%), 1fr));
  gap: 16px;
  margin-bottom: 16px;
}
.panel-row > * { min-width: 0; }
/* The ROW owns the spacing, not the panels in it. A panel keeps its own
 * `margin-bottom: 16px` for when it stands alone; left on inside a row it is
 * added to the grid's own 16px gap the moment the row stacks, which cost 16px
 * of double spacing per pair on a phone — measured at +101px across the site,
 * turning a saving into a regression on the one screen Tim reads it on. */
.panel-row > .panel { margin-bottom: 0; }

/* Small layout utilities, so pages don't need inline styles. */
.hidden { display: none; }
.form-grow { flex: 1 1 200px; }
.form-narrow { flex: 0 0 110px; }
.narrow-page { max-width: 820px; }
.status-line { margin-top: 14px; font-size: 14px; min-height: 20px; }
.kv { display: grid; grid-template-columns: auto 1fr; gap: 6px 18px; margin: 0; font-size: 14px; }
.kv dt { color: var(--dim); }
.kv dd { margin: 0; }
.left { text-align: left; }
.spread { justify-content: space-between; }
.form-select { width: 140px; }
.mb-12 { margin-bottom: 12px; }

.stat-row { display: flex; gap: 26px; flex-wrap: wrap; margin-bottom: 4px; }
.stat .k { font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--dim); }
.stat .v { font-size: 21px; font-weight: 600; font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------- connection bar
 * Answers "is this my real league, demo data, or quietly broken?" at a glance.
 * Not knowing that was the single biggest time-waster on this project.
 *
 * Class names and the three states are fixed by js/connection.js, which builds
 * this markup - restyle freely, but do not rename anything here.
 */

.conn {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 9px 14px;
  margin: 0 0 16px;
  border-radius: var(--r-3);
  font-size: 13px;
  border: 1px solid transparent;
}

.conn-dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.conn-main { flex: 1 1 auto; min-width: 220px; }
/* The off state dims its own text, so the emphasis inside it needs raising. */
.conn-main strong { color: var(--text); font-weight: 650; }

/* Each state is a tinted panel, not a light card: the hue carries the meaning
 * and the surface stays on the same dark ramp as everything around it. */
.conn.ok   { background: #10281a; border-color: #1d5330; color: #9fdcb4; }
.conn.ok .conn-dot { background: var(--accent); box-shadow: 0 0 0 3px rgba(59, 165, 93, 0.18); }
.conn.warn { background: #241d0e; border-color: #5a4718; color: #e3c079; }
.conn.warn .conn-dot { background: var(--warn); box-shadow: 0 0 0 3px rgba(217, 164, 65, 0.18); }
.conn.off  { background: var(--panel); border-color: var(--line); color: var(--dim); }
.conn.off .conn-dot { background: #5b6472; }

.conn-input, .conn-team select {
  padding: 5px 8px;
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  font: inherit;
  font-size: 13px;
  background: #0d0f13;
  color: var(--text);
  width: auto;
}
.conn-input { width: 140px; }
.conn-input::placeholder { color: var(--dim); }
/* It is a <label>, so it has to undo the block/dim defaults set for labels. */
.conn-team {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 0;
  font-size: 13px;
  color: inherit;
}

.conn-btn {
  padding: 6px 13px;
  border: 1px solid transparent;
  border-radius: var(--r-2);
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
}
.conn-btn:hover { background: var(--accent-hi); }
.conn.warn .conn-btn { background: var(--warn); color: #251b06; }
.conn.warn .conn-btn:hover { background: #e6b458; }

.conn-err {
  flex: 1 1 100%;
  color: var(--err);
  font-size: 12.5px;
}

/* ------------------------------------------------------- the synced copy
 *
 * A fourth state, added with the phone bridge: the numbers on this page are
 * not live, they are the copy the desktop published, and the reader has to be
 * told so in words rather than by the absence of a claim. The class names are
 * fixed by js/connection.js along with the three above.
 *
 * `.conn.stale` is the loud one, and it is loud on purpose. It fires when the
 * synced squads are past the age js/cloud.js calls stale, which means every
 * number below the bar is out of date — the same situation, and the same
 * answer, as the time machine's archive banner. Red, not amber: amber is
 * already "connected but not yet reading a league", which is a prompt rather
 * than a warning, and a reader who has learnt to skim one will skim the other.
 */
.conn.stale {
  background: #2a1214;
  border-color: #6b2429;
  color: #f0b3b6;
}
.conn.stale .conn-dot { background: var(--err); box-shadow: 0 0 0 3px rgba(209, 74, 74, 0.2); }
.conn.stale .conn-btn { background: var(--err); color: #ffffff; }
.conn.stale .conn-btn:hover { background: #e06a6a; }

/* One chip per shape, because they do not decay at the same rate: js/cloud.js
 * calls the waiver wire stale after a day and the squads after a week. Showing
 * one timestamp for all of it would flatten exactly the distinction that
 * matters — four-day-old squads are fine and a four-day-old wire lists men who
 * were claimed on Tuesday. */
.conn-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  flex: 0 1 auto;
}
.conn-chip {
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid currentColor;
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  opacity: 0.8;
}
/* A stale chip stops being a quiet label. It keeps its own colour even inside
 * the green bar, because a fresh roster set and a day-old wire is a real and
 * common combination — the whole point of splitting them. */
.conn-chip.is-stale {
  border-color: var(--err);
  background: rgba(209, 74, 74, 0.16);
  color: #f0b3b6;
  font-weight: 600;
  opacity: 1;
}
.conn-chip.is-missing { opacity: 0.55; }

/* The cloud's own line: who is signed in, when it last published. Secondary to
 * the league sentence beside it, so it is dimmer and it wraps last. */
.conn-note {
  font-size: 12px;
  opacity: 0.85;
  flex: 0 1 auto;
}
.conn-bad { color: var(--err); opacity: 1; }

/* Sign in / Send to phone / Sign out sit beside Connect and Sync now, which
 * are the page's primary actions. An outlined button keeps them available
 * without competing for the eye. */
.conn-btn-ghost {
  background: transparent;
  border-color: currentColor;
  color: inherit;
  font-weight: 600;
}
.conn-btn-ghost:hover { background: rgba(255, 255, 255, 0.08); }
.conn.stale .conn-btn-ghost,
.conn.warn .conn-btn-ghost {
  background: transparent;
  color: inherit;
}

/* ====================================================================== phone
 *
 * The site was designed on a wide screen and every page is a dense table, so
 * the question here is not "how do these tables become phone-shaped" — they
 * cannot, and turning ten teams by twenty columns into stacked cards would
 * destroy the one thing the tables are for, which is comparing a column down
 * the league. The answer is the one the desktop layout already reaches for:
 * `.table-scroll` scrolls sideways with the team frozen down the left. What
 * this block does is make everything AROUND that fit a 390px screen, and make
 * every affordance that was behind a hover reachable with a thumb.
 *
 * Two media features, deliberately separate, because they are different facts:
 *
 *   `max-width: 760px`  — the screen is narrow. About layout.
 *   `hover: none`       — there is no pointer to hover. About capability.
 *
 * An iPad in landscape is the second without being the first, and a narrow
 * desktop window is the first without being the second. Keeping them apart is
 * what stops a resized laptop window losing its hover card.
 */

/* ------------------------------------------------------------ touch: any size
 *
 * Nothing on this site may be reachable ONLY by hovering. Where a hover reveals
 * something, the same thing has to be reachable by tapping — see the tip card
 * in analysis.html, which becomes a sheet a tap opens.
 */
@media (hover: none) {
  /* A finger has no precision and no hover to aim with, so every control that
   * is a control in its own right gets a real target.
   *
   * The two exceptions are both buttons that live INSIDE a table row, where
   * 44px of height is 44px on every row of a long table: `button.slot-tag` is
   * the roster detail's swap control, and `.mark` is the draft board's two
   * Mine/Taken buttons on every one of a couple of hundred rows. Both get a
   * smaller bump of their own instead — in analysis.html and draft.css. */
  button:not(.slot-tag):not(.mark),
  .conn-btn,
  .segmented button {
    min-height: 44px;
  }

  /* Sorting a table is a tap on its header, and a header cell is ~24px tall.
   * Padding is the only thing that can grow it without changing the type. */
  th.sortable { padding-top: 10px; padding-bottom: 10px; }

  /* 16px is not a taste decision and it is not about width either, which is why
   * it lives here: iOS zooms the whole page in when a field smaller than 16px
   * takes focus, and then leaves it zoomed. That happens in landscape too,
   * where a phone is 844px wide and none of the narrow-screen rules below
   * apply — so a single tap on the league-ID box would leave every table half
   * off the screen on the one orientation that was meant to show more of it. */
  input, select, textarea,
  .conn-input, .conn-team select { font-size: 16px; }
}

/* ------------------------------------------------------------ narrow screens */
@media (max-width: 760px) {
  :root {
    /* One token moves the panel padding AND the table bleed together — they
     * are defined as exact negatives of each other, so changing only one
     * would pull every table out of line with its panel edge. */
    --pad-panel: 13px;
  }

  body { padding-bottom: 48px; }

  /* 20px of gutter each side is 10% of the screen. The tables want it back. */
  .wrap, .wrap.wide { padding: 0 14px; }

  h1 { font-size: 20px; }
  .sub { font-size: 13.5px; margin-bottom: 18px; }
  section.panel { margin-bottom: 12px; }
  section.panel > h2 { margin-bottom: 11px; }

  /* --- header ---
   * The nav is seven links and cannot sit on the brand's line at this width.
   * Given its own row it wraps to two, and `flex: 1` spreads the links so each
   * one is a full-width-sharing target rather than a 40px word — which also
   * keeps them all VISIBLE. A horizontally scrolling strip would fit on one
   * line and hide the last two pages behind a gesture nothing advertises. */
  header.site { padding: 10px 0; margin-bottom: 18px; }
  header.site .wrap { gap: 9px; }
  header.site .brand { flex: 1 1 100%; }
  header.site nav {
    margin-left: 0;
    width: 100%;
    flex-wrap: wrap;
    gap: 5px;
  }
  header.site nav a {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 38px;
    padding: 6px 12px;
    background: var(--panel-2);
  }

  /* --- form controls ---
   * (The 16px that stops iOS zooming on focus is in the `hover: none` block
   * above, not here: it has to hold in landscape too, where a phone is wider
   * than this query.)
   *
   * Every fixed-width control is a desktop measurement. In a wrapping .row a
   * full-width field is one clean line per control instead of two ragged ones.
   * (Page-local widths — .select-md, .select-wide — are overridden in their own
   * pages: a page's <style> comes after this file and wins the tie.) */
  .form-select, .form-narrow { width: 100%; flex: 1 1 100%; }
  .form-grow { flex: 1 1 100%; }
  .row { gap: 9px; }
  .row.spread { justify-content: flex-start; }

  /* --- segmented controls ---
   * Eight position buttons do not fit on one line, and the control is built as
   * a single track with `overflow: hidden` and left borders between the
   * buttons, so simply letting it wrap leaves hairlines in the wrong places. As
   * a grid with a 1px gap over a --line background, the gaps ARE the dividers,
   * every row is separated correctly, and each button is a proper target. */
  .segmented {
    display: grid;
    /* 72px gives four tracks on a 390px phone, which is what makes the two
     * eight-button position filters fall as a tidy 4 + 4 and the seven-button
     * one as 4 + 3. At 62px they fit five and broke 5 + 3, which reads as two
     * unrelated controls. Two- and three-button toggles have fewer items than
     * tracks, so auto-fit collapses the empties and they still fill the width. */
    grid-template-columns: repeat(auto-fit, minmax(72px, 1fr));
    gap: 1px;
    background: var(--line);
    width: 100%;
  }
  .segmented button {
    background: var(--panel);
    padding: 8px 6px;
    /* A grid item's `min-width` is `auto`, so a button whose longest word is
     * wider than its track overflows it — and the track is `overflow: hidden`,
     * which clips the word dead with no ellipsis rather than wrapping it.
     * Nothing clips today; this is what keeps the next label added from
     * silently losing its tail. */
    min-width: 0;
  }
  /* The track's own border drew these; the grid gap draws them now, and the
   * leftovers would double up on the first button of every row. */
  .segmented button + button { border-left: none; }
  /* On desktop a segmented button is transparent, so `opacity: 0.45` only dims
   * its ink against the panel. Here it is an opaque --panel over a --line
   * track, and at 45% it composites LIGHTER than its neighbours — a disabled
   * button would read as the selected one. */
  .segmented button:disabled { opacity: 1; color: var(--line-2); }

  /* --- tables ---
   * Denser type and tighter cells, because the width saved here is a column
   * that does not have to be scrolled to. Everything else about the table is
   * unchanged: it still scrolls sideways with the name frozen down the left. */
  table { font-size: 12.5px; }
  th, td { padding: 5px 7px; }
  th { font-size: 10.5px; letter-spacing: 0.04em; }
  tr.colgroup th { font-size: 9.5px; }

  /* The frozen column is the only one that cannot be scrolled away from, so it
   * is the only one that must not be allowed to eat the screen. ~44vw is about
   * 27 characters at this size — longer than any name in the league — so this
   * is a ceiling that normally does not bite rather than a truncation.
   *
   * ALL THREE DECLARATIONS OR NONE. A `max-width` alone caps the box and does
   * nothing to the text: the table is `white-space: nowrap`, so a long name
   * overflows to the right — and because this column is sticky with an opaque
   * background, the overflowing glyphs paint on top of the number columns
   * scrolling underneath. `overflow: hidden` clips CHILDREN, never the cell's
   * own drop shadow, so the "there is more to the right" cue survives.
   * (`#takenTable td.owner` in waivers.html is the same three together, for the
   * same reason.) */
  td.name, th.name {
    max-width: 44vw;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* `dvh`, not `vh`: on iOS `vh` is measured against the viewport with the
   * address bar COLLAPSED, so 70vh is most of the screen while the bar is
   * still showing and the page around the table is unreachable until the
   * table's own scroll bottoms out. `dvh` follows the bar. The `vh` line stays
   * as the fallback for anything that does not know `dvh`. */
  .table-scroll {
    max-height: 70vh;
    max-height: 62dvh;
    /* Stops a sideways flick that runs off the end of a table from scrolling
     * the PAGE sideways behind it, and on Android Chrome stops it becoming a
     * pull-to-navigate. It does NOT stop iOS Safari's back gesture — that is a
     * system edge swipe resolved before any scrolling begins, and no value of
     * overscroll-behavior cancels it. Worth knowing, because the tables bleed
     * to the panel edge and a phone's gutter is only 14px, so a flick that
     * starts at x≈0 will navigate back whatever this file says.
     * Only the X axis: vertical overscroll still has to chain to the page, or a
     * thumb gets stuck inside a table it has already read to the end of. */
    overscroll-behavior-x: contain;
  }

  /* --- misc --- */
  .stat-row { gap: 16px; }
  .stat .v { font-size: 19px; }
  .empty { padding: 20px 10px; }
  .panel-note { font-size: 12.5px; }
  .kv { gap: 5px 12px; font-size: 13.5px; }
  /* debug.html prints raw ESPN JSON in here, whose lines are far wider than a
   * phone: it is the only horizontal scroller on the site that is not a table,
   * and it wants the same containment they get. */
  pre { font-size: 11.5px; max-height: 260px; overscroll-behavior-x: contain; }

  /* --- connection bar ---
   * Its 220px minimum is wider than the room left beside a Sync button, so the
   * bar reflows into a mess of one-word lines. Here the sentence takes a line
   * of its own and the controls share the one under it. */
  .conn { padding: 10px 12px; gap: 8px 10px; }
  .conn-main { flex: 1 1 auto; min-width: 0; }
  .conn-input { width: auto; flex: 1 1 130px; min-width: 0; }
  .conn-team { flex: 1 1 100%; }
  .conn-team select { flex: 1 1 auto; min-width: 0; width: auto; }
  .conn-btn { flex: 0 0 auto; }

  /* The ages and the cloud's note each take a full line of their own.
   * A phone is the device that READS synced data — it is the only one that
   * has to — so on this screen these are the most important words in the bar,
   * not an afterthought squeezed in beside a button. Squeezed in beside one,
   * "Squads 4 days ago" wraps to two words a line and stops being readable at
   * a glance, which is the only way it is any use. */
  .conn-chips, .conn-note { flex: 1 1 100%; }
  .conn-chip { font-size: 12px; padding: 3px 9px; }

  /* Sign in with Google is the widest label in the bar and on this screen it
   * is usually the ONLY thing to press, so it takes the row. Height comes
   * from the `hover: none` block above, which is the right place for it:
   * 44px of target is about there being no pointer, not about the screen
   * being narrow, and a mouse on a 700px window does not need it. */
  .conn-btn-ghost { flex: 1 1 100%; }
}

/* ------------------------------------------------ the tappable-title sheet
 *
 * What js/touch-titles.js opens when a `title` is tapped. A `title` draws a
 * native tooltip on a desktop and draws nothing at all on iOS, and this site
 * puts real content in them — seventeen column definitions on the stats page
 * alone — so on a touch screen they need somewhere to go.
 *
 * Pinned to the foot of the window rather than floated beside what was tapped:
 * the thing tapped is often a sticky table header inside a sideways scroller,
 * and there is no reliable room beside it. Deliberately its own class and NOT
 * a share of analysis.html's `.tipcard.sheet` — that one carries a chart, a
 * legend and a link and is positioned by its own code; this is two lines of
 * text. One of them growing a feature must not silently move the other.
 *
 * Outside the phone media query on purpose: it is opened by a POINTER test, not
 * a width one, so an iPad in landscape at 1100px must still be able to style it.
 */
.tipsheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 60;              /* over the sticky headers, which claim 2 and 3 */
  max-height: 70vh;
  max-height: 66dvh;
  overflow-y: auto;
  padding: 15px 16px 17px;
  background: var(--panel-2);
  border-top: 1px solid var(--line-2);
  border-radius: var(--r-4) var(--r-4) 0 0;
  box-shadow: 0 -14px 34px -10px rgba(0, 0, 0, 0.8);
  color: var(--text);
  font-size: 13.5px;
  line-height: 1.55;
}
/* What was tapped, so the reader can see the sheet is about the "S+L" they
 * pressed and not about anything else. */
.tipsheet .ts-label {
  font-weight: 700;
  font-size: 15px;
  margin-bottom: 3px;
}
.tipsheet .ts-text { color: var(--text); }
.tipsheet .ts-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 12px;
  padding-top: 11px;
  border-top: 1px solid var(--line);
}
.tipsheet .ts-close { min-height: 44px; padding: 8px 20px; }

/* A phone turned sideways has about 320px of usable height, and 62% of that is
 * seven rows. The cap is RAISED rather than removed: the max-height is the only
 * reason the sticky header sticks at all (it is what gives the header something
 * to scroll inside), and on the screen with the least room to spare, a header
 * that stays put is worth more than the two extra rows losing it would buy.
 *
 * `max-width` is in here and it is not decoration: a 1600x540 DESKTOP window
 * matches short-and-landscape perfectly well, and without the width clause
 * those users would silently pick this rule up instead of the 70vh they are
 * meant to have. 940px covers a landscape phone (844px) and stops short of any
 * real desktop. The `vh` line is the fallback, for the same reason as above —
 * without one, a browser that does not know `dvh` gets no cap at all here, and
 * no cap means the sticky header stops sticking. */
@media (max-width: 940px) and (max-height: 560px) and (orientation: landscape) {
  .table-scroll { max-height: 78vh; max-height: 78dvh; }
}
