/* Drilldown — dense, keyboard-first UI for breaking down film. */

:root {
  --accent: #c8102e;
  --accent-soft: color-mix(in srgb, var(--accent) 12%, transparent);

  --bg: #f4f5f7;
  --surface: #ffffff;
  --surface-2: #fafbfc;
  --line: #e2e5ea;
  --line-strong: #cbd1da;

  --ink: #14181f;
  --ink-2: #4a5563;
  --ink-3: #8b95a3;

  /*
   * The sidebar is a slate rather than a near-black: dark enough to sit clearly
   * behind the page (10.2x against the body), light enough not to read as a
   * black bar down the screen.
   *
   * It cannot go much lighter than this. The active nav item is the crimson
   * accent, and by #424d5e the pill only separates from the ground by 1.45x --
   * it stops looking like a selected item and starts looking like a smudge.
   * 1.89x here is the margin that buys.
   *
   * `raise` and `edge` are steps *up* from the ground, for anything that should
   * sit on top of it. They have to be lighter than --sidebar-bg; the values
   * they replaced were lighter than the old near-black and would have sunk.
   */
  --sidebar-bg: #333c4a;
  --sidebar-raise: #3e4959;
  /*
   * The brand red as it has to be on the sidebar's slate.
   *
   * #c8102e is the mark's colour everywhere it sits on white. On this ground
   * it measures 1.9:1 -- a dark red on a dark grey, which reads as a smudge
   * rather than as a football. Lifted to 3.2:1, which clears the bar for a
   * graphic and still reads as the same crimson rather than as a pink.
   */
  --accent-on-sidebar: #f5495f;
  --sidebar-edge: #4b5768;
  --sidebar-ink: #c9d1d9;
  --sidebar-ink-dim: #9aa5b4;

  --offense: #1f6feb;
  --defense: #d1631a;
  --special: #6f42c1;
  /* A play a coach marked as worth watching again: the star, and its tab. */
  --highlight: #b8860b;
  --good: #1a7f37;
  /* The statistics graphs: us, and them. See components/charts.js. */
  --chart-us: #3b6ea5;
  --chart-us-light: #a7bed6;
  --chart-them: #9aa5b1;
  --chart-them-dark: #76828f;
  --bad: #b42318;

  --radius: 10px;
  --radius-sm: 6px;
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 4px 16px rgba(16, 24, 40, 0.06);
  --shadow-lg: 0 12px 40px rgba(16, 24, 40, 0.18);

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

html, body { height: 100%; }
/* A hidden element is hidden, whatever display its own class asks for. */
[hidden] { display: none !important; }
/* The page's scrollbar space is always kept, so switching between a long page
   and a short one (the two Statistics tabs, say) does not slide everything
   aligned to the right sideways by the width of a scrollbar. */
html { scrollbar-gutter: stable; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 14px;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

/*
 * The demo's strip across the top (app.js, showDemoBar) takes this much of the
 * window; everything sized to the window gives it back. Nothing outside a demo.
 */
:root { --demo-bar-h: 0px; }

.app { display: grid; grid-template-columns: 248px 1fr; min-height: calc(100vh - var(--demo-bar-h)); }

/* ---------- Sidebar ---------- */

.sidebar {
  background: var(--sidebar-bg);
  color: var(--sidebar-ink);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: var(--demo-bar-h);
  height: calc(100vh - var(--demo-bar-h));
  overflow-y: auto;
}

.brand {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 18px 18px 14px;
  font-weight: 700;
  font-size: 16px;
  letter-spacing: -0.01em;
  color: #fff;
}
.team-switch { padding: 4px 12px 6px; }
.team-switch label {
  display: block;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--sidebar-ink-dim);
  margin: 0 6px 6px;
}
.team-switch select {
  width: 100%;
  padding: 8px 10px;
  background: var(--sidebar-raise);
  color: #fff;
  border: 1px solid var(--sidebar-edge);
  border-radius: var(--radius-sm);
  font-size: 13px;
  font-family: inherit;
}

.nav { padding: 2px 12px 6px; display: flex; flex-direction: column; gap: 2px; }
.nav-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--sidebar-ink-dim);
  padding: 14px 12px 6px;
}
.nav a {
  display: flex;
  align-items: center;
  /* Indented past the section heading above them, so the grouping is visible
     without a rule or a box: TEAM holds the left margin and the pages under it
     step in. The highlight on the active item still runs the full width -- it
     is the row that is selected, not the word. */
  padding: 8px 12px 8px 24px;
  border-radius: var(--radius-sm);
  color: var(--sidebar-ink);
  text-decoration: none;
  font-size: 13.5px;
}
.nav a:hover { background: var(--sidebar-raise); color: #fff; }
.nav a.active { background: var(--accent); color: #fff; font-weight: 600; }
.nav a .count {
  margin-left: auto;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  opacity: 0.6;
}

.sidebar-foot {
  margin-top: auto;
  padding: 14px 18px;
  font-size: 11px;
  color: var(--sidebar-ink-dim);
  border-top: 1px solid var(--sidebar-edge);
}

/* ---------- Main ---------- */

.main { min-width: 0; display: flex; flex-direction: column; }

.page { padding: 24px 28px 64px; max-width: 1500px; width: 100%; }

/*
 * The cap above keeps a page of prose from running to a line nobody can track
 * back from. A page that is mostly a wide table has the opposite problem: the
 * breakdown carries fourteen columns and still scrolls sideways, so every
 * pixel the cap leaves grey on the right is a column the coach has to go and
 * find. Those pages opt out by name rather than the cap being lifted for all
 * of them.
 */
.page.is-wide { max-width: none; }

.page-head {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}
.page-head h1 {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.page-head .sub { color: var(--ink-2); font-size: 13px; margin-top: 4px; }
.page-head .actions { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }

.crumb {
  font-size: 12px;
  color: var(--ink-3);
  margin-bottom: 6px;
}
.crumb a { color: var(--ink-2); text-decoration: none; }
.crumb a:hover { text-decoration: underline; }

/* ---------- Buttons & controls ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 13px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
}
.btn:hover { background: var(--surface-2); border-color: var(--ink-3); }
.btn:active { transform: translateY(1px); }
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.btn-primary:hover { filter: brightness(1.08); background: var(--accent); border-color: var(--accent); }
.btn-danger { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 35%, var(--line)); }
.btn-danger:hover { background: color-mix(in srgb, var(--bad) 8%, #fff); }
.btn-sm { padding: 5px 9px; font-size: 12px; }
.btn-ghost { border-color: transparent; background: transparent; }
.btn-ghost:hover { background: rgba(0, 0, 0, 0.05); border-color: transparent; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

/*
 * Every input that holds typing, said as an exclusion rather than as a list.
 *
 * It was a list -- text, number, date, url, search -- and the list was wrong
 * twice, in the same way both times. An <input> with no type attribute is not
 * matched by input[type="text"], so several boxes carrying .input looked roughly
 * right and were in fact wearing the browser default; and a time field added
 * later was not on the list at all, so it turned up 12px shorter than the date
 * beside it, in monospace, with a 2px inset border.
 *
 * A list has to be remembered every time somebody adds a field. Naming the four
 * inputs that are not boxes of text does not.
 */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="file"]):not([type="color"]),
select, textarea, .input {
  width: 100%;
  padding: 7px 9px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font: inherit;
  font-size: 13px;
  color: var(--ink);
}

/*
 * A date and a time side by side are the same box.
 *
 * Their native pickers are different sizes, so left alone they sit at different
 * heights with their labels level -- which reads as one of them being broken.
 * Fixing the content box makes the two agree whatever each browser puts inside.
 */
input[type="date"], input[type="time"], input[type="datetime-local"] {
  height: 35px;
}
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
textarea { resize: vertical; min-height: 64px; }

/* Derived fields: readable, obviously not yours to type in. */
input[readonly], select:disabled {
  background: var(--surface-2);
  color: var(--ink-2);
  cursor: default;
}
input[readonly]:focus { border-color: var(--line-strong); box-shadow: none; }
label.field { display: block; margin-bottom: 12px; }
label.field > span {
  display: block;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-2);
  margin-bottom: 5px;
}
.field-row { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
.hint { font-size: 11.5px; color: var(--ink-3); margin-top: 4px; }

/*
 * Searching for a place.
 *
 * The results sit under the box rather than floating over it: the form is
 * inside a dialog that scrolls, and a floating list in a scrolling dialog ends
 * up beside the thing it belongs to. Pushing the fields down is honest about
 * the room it takes.
 */
.place-input { display: flex; gap: 8px; }
.place-input input { flex: 1 1 auto; min-width: 0; }
.place-input .btn { flex: 0 0 auto; }

.place-list {
  margin-top: 6px; border: 1px solid var(--line); border-radius: 8px;
  overflow: hidden; max-height: 220px; overflow-y: auto;
}
.place-row {
  display: block; width: 100%; text-align: left; background: none;
  border: 0; border-bottom: 1px solid var(--line); padding: 7px 10px;
  font: inherit; color: inherit; cursor: pointer;
}
.place-row:last-child { border-bottom: 0; }
.place-row:hover { background: var(--surface-2); }
.place-row.is-note { color: var(--ink-3); font-size: 12px; cursor: default; }
.place-row.is-note:hover { background: none; }
.place-name { display: block; font-size: 13px; font-weight: 500; }
.place-addr { display: block; font-size: 11.5px; color: var(--ink-3); margin-top: 1px; }
/* In a table cell an address is a hint, not the column: one line, then stop. */
td .place-addr {
  max-width: 220px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* An address that goes somewhere looks like it does. */
a.place-addr { color: var(--accent); text-decoration: none; }
a.place-addr:hover { text-decoration: underline; }
.place-open { margin-top: 6px; font-size: 12px; }
.place-open a { color: var(--accent); text-decoration: none; }
.place-open a:hover { text-decoration: underline; }

/*
 * Which days a thing repeats on.
 *
 * Seven letters rather than seven checkboxes with names: a coach reading
 * "Tue Thu" reads it as a pattern, and the pattern is the thing being set.
 * Two of them say T and two say S, which is the price of the shape and is why
 * each one also says its full name to a screen reader.
 */
.rep-days { display: flex; gap: 4px; }
.rep-day {
  width: 30px; height: 30px; border-radius: 999px;
  border: 1px solid var(--line-strong); background: var(--surface);
  font: inherit; font-size: 12px; font-weight: 600; color: var(--ink-2);
  cursor: pointer;
}
.rep-day:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.rep-day[aria-pressed="true"] {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.rep-day:disabled { opacity: 0.45; cursor: not-allowed; }

/* ---------- Roster contacts ---------- */

/*
 * The people to tell alongside somebody.
 *
 * A list rather than a table: it is two or three rows, and a table header over
 * three rows is more furniture than content.
 */
.contact-list {
  border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
}
.contact-row {
  display: flex; align-items: center; gap: 10px; justify-content: space-between;
  padding: 7px 10px; border-bottom: 1px solid var(--line);
}
.contact-row:last-child { border-bottom: 0; }
.contact-who { min-width: 0; }
.contact-name { font-size: 13px; font-weight: 500; }
.contact-meta { font-size: 11.5px; color: var(--ink-3); margin-top: 1px; }
.contact-acts { display: flex; gap: 4px; flex: 0 0 auto; }

.roster-check {
  display: flex; gap: 7px; align-items: center;
  margin-top: 8px; font-size: 13px; color: var(--ink-2);
}


/* ---------- The calendar ---------- */

/*
 * A month, on a wall.
 *
 * Six rows always. A grid that is five rows in one month and six in the next
 * makes everything below it jump as you page through a season, and an empty
 * row costs less than that.
 */
.cal-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 4px 0 10px;
}
.cal-month { font-size: 17px; font-weight: 650; color: var(--ink); }
.cal-nav { display: flex; gap: 6px; }
.cal-nav .btn { min-width: 34px; }

.cal-grid {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px; background: var(--line);
  border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
}
.cal-weekday {
  background: var(--surface-2); padding: 6px 8px;
  font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-3); font-weight: 600;
}
.cal-day {
  background: var(--surface); min-height: 104px; padding: 5px 6px 7px;
  display: flex; flex-direction: column; gap: 3px; cursor: pointer;
}
.cal-day:hover { background: var(--surface-2); }
/* A day either side of the month is context, not somewhere to look. */
.cal-day.is-outside { background: var(--surface-2); }
.cal-day.is-outside .cal-date { color: var(--ink-3); opacity: 0.6; }
.cal-date { font-size: 12px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.cal-day.is-today .cal-date {
  align-self: flex-start; background: var(--accent); color: #fff;
  border-radius: 999px; min-width: 20px; padding: 1px 6px; text-align: center;
  font-weight: 650;
}

/*
 * An entry, small enough that five of them fit on a Friday.
 *
 * The time leads because that is what separates two things on the same day,
 * and the name is allowed to run out of room rather than wrap -- a square that
 * grows to fit its longest title drags the whole week's height with it.
 */
.cal-chip {
  display: flex; align-items: baseline; gap: 5px; width: 100%;
  text-align: left; border: 0; border-left: 3px solid currentColor;
  border-radius: 3px; padding: 2px 5px; font: inherit; font-size: 11.5px;
  cursor: pointer; overflow: hidden;
}
.cal-chip:hover { filter: brightness(0.96); }
.cal-time { font-size: 10.5px; opacity: 0.75; white-space: nowrap; font-variant-numeric: tabular-nums; }
.cal-what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.cal-repeat { margin-left: auto; opacity: 0.6; font-size: 10px; }

/*
 * One colour per kind, and they have to be told apart at a glance on a busy
 * week. Games take the accent because they are the thing the season is made
 * of; the rest are the semantic hues already in the palette, which are as far
 * apart as anything here gets.
 */
.cal-chip.is-game       { color: var(--accent);  background: color-mix(in srgb, var(--accent) 10%, #fff); }
.cal-chip.is-scrimmage  { color: var(--special); background: color-mix(in srgb, var(--special) 10%, #fff); }
.cal-chip.is-practice   { color: var(--offense); background: color-mix(in srgb, var(--offense) 10%, #fff); }
.cal-chip.is-team_event { color: var(--good);    background: color-mix(in srgb, var(--good) 10%, #fff); }

.cal-key { display: flex; flex-wrap: wrap; gap: 14px; margin: 0 0 12px; }
.cal-key-item { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-2); }
.cal-dot { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
.cal-dot.is-game       { background: var(--accent); }
.cal-dot.is-scrimmage  { background: var(--special); }
.cal-dot.is-practice   { background: var(--offense); }
.cal-dot.is-team_event { background: var(--good); }

@media (max-width: 720px) {
  .cal-day { min-height: 72px; }
  .cal-what { display: none; }      /* a dot and a time still says there is something */
}

/*
 * A settings row: what it is, where its value came from, and the box.
 *
 * The source badge sits on the label line rather than under the box, because
 * "this is coming from the environment, not from what you typed" is something
 * to know before typing rather than after saving.
 */
.set-list { display: grid; gap: 18px; }
.set-row { display: grid; gap: 6px; }
.set-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.set-head label { font-size: 12px; font-weight: 600; color: var(--ink-2); }
.set-row .hint { margin-top: 0; }
.badge.is-none { background: transparent; color: var(--ink-3); border: 1px dashed var(--line-strong); }

/*
 * The game's own details, read back as densely as they can be read.
 *
 * A row of short facts on one grid and the long ones stacked under it. Putting
 * an address in the same grid as a start time makes every column as wide as the
 * address, which spreads six values across a screen with an acre of nothing
 * between them -- and this sits above the work, so every row it takes is a row
 * of film pushed down the page.
 */
.gd-card { margin-bottom: 14px; }
.gd-card .card-body { padding: 10px 14px; }
/* Edit in the corner of what it edits, without a heading row to hold it: the
   details grow, the button stays pinned to the top right. */
.gd-body { display: flex; align-items: flex-start; gap: 14px; }
.gd-main { flex: 1 1 auto; min-width: 0; }
.gd-body > .btn, .gd-acts { flex: 0 0 auto; }
.gd-acts { display: flex; gap: 8px; }
/* Folded: the first line of facts only. */
.gd-card.is-collapsed .gd-wide { display: none; }
.gd-toggle { min-width: 30px; font-size: 13px; line-height: 1; }

/* A column heading that sorts. The caret is the only thing that moves, so a
   heading does not jump a pixel when it becomes the sorted one. */
th.is-sortable { cursor: pointer; user-select: none; white-space: nowrap; }
th.is-sortable:hover { color: var(--ink); }
th.is-sortable:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
th.is-sorted { color: var(--ink); }
.th-caret { display: inline-block; min-width: 10px; font-weight: 400; }
.gd-grid {
  display: flex; flex-wrap: wrap; gap: 6px 26px;
}
.gd-wide {
  display: grid; gap: 4px;
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line);
}
.gd-item { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
.gd-item.is-wide { gap: 9px; }
.gd-label {
  font-size: 10px; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--ink-3); white-space: nowrap;
}
.gd-value { font-size: 13px; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
.gd-item.is-wide .gd-label { min-width: 44px; }

/* The film link, and what its play links can do with it. */
.film-row-body { display: grid; gap: 4px; min-width: 0; }
.film-row-line { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 13px; }
.film-support {
  display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; white-space: nowrap;
  color: var(--ink-2); background: var(--surface-2); border: 1px solid var(--line);
}
.film-support.is-good { color: var(--good); background: color-mix(in srgb, var(--good) 9%, #fff); border-color: color-mix(in srgb, var(--good) 25%, #fff); }
.film-support.is-warn { color: #8a5a00; background: color-mix(in srgb, #b7791f 12%, #fff); border-color: color-mix(in srgb, #b7791f 30%, #fff); }
.film-support.is-bad { color: var(--bad); background: color-mix(in srgb, var(--bad) 8%, #fff); border-color: color-mix(in srgb, var(--bad) 25%, #fff); }
/*
 * The same chip, as the button that re-tests the link.
 *
 * A button brings its own font, padding and alignment, so those are reset
 * back to the span the chip has always been -- the point is that it looks no
 * different, only behaves differently. The colour it is already wearing says
 * what the film does; borrowing a button look as well would say it twice.
 */
button.film-support.is-checkable {
  font: inherit; font-size: 11.5px; font-weight: 600; line-height: inherit;
  cursor: pointer;
}
button.film-support.is-checkable:hover:not(:disabled) { filter: brightness(0.96); }
button.film-support.is-checkable:disabled { cursor: progress; opacity: 0.7; }
button.film-support.is-checkable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/*
 * The title-card switch, under the film's own controls.
 *
 * A line of its own rather than another button on the row: it is a setting
 * this film carries, not an action, and the row above it is already four
 * controls wide.
 */
/*
 * A row on an archived shelf.
 *
 * Nothing to open behind it, so it does not offer a pointer or a hover lift --
 * the row is a record of something put away, and its buttons are the only
 * things on it that still do anything.
 */
tr.is-archived { cursor: default; }
tr.is-archived td { color: var(--ink-3); }
tr.is-archived td:first-child, tr.is-archived td:nth-child(2) { opacity: 0.85; }

/*
 * Confirming what a film's scoreboard reads.
 *
 * The picture carries the screen: it is the coach's own board with the boxes
 * drawn on it, and every question below refers to a number printed on it. So
 * it is shown as large as the dialog allows, on a dark ground because a lit
 * LED board against white is a glare rather than a photograph.
 */
.sb-intro { margin: 0 0 10px; font-size: 13px; color: var(--ink-2); }
.sb-shot {
  background: #14181f; border-radius: var(--radius-sm); padding: 10px;
  margin-bottom: 14px; text-align: center;
}
.sb-shot img { max-width: 100%; image-rendering: pixelated; }
/*
 * A rendered bar is thirty times wider than it is tall. Fitted to the
 * dialog's width like a board it is a line twenty pixels high, and the whole
 * point of the picture is to read numbers off it. Shown at its own height
 * instead and scrolled sideways; the picture says which it is on load.
 */
.sb-shot.is-wide { overflow-x: auto; text-align: left; }
.sb-shot.is-wide img { max-width: none; height: 160px; width: auto; }

.sb-fields { display: grid; gap: 8px; }
.sb-row { display: grid; grid-template-columns: 110px 1fr auto; gap: 10px; align-items: center; }
.sb-name { font-size: 13px; color: var(--ink); }
.sb-row select { width: 100%; }
/* What the film made of the field, beside the field. */
.sb-verdict { font-size: 11.5px; white-space: nowrap; }
.sb-verdict.is-good { color: var(--good); }
.sb-verdict.is-bad { color: var(--bad); }
.sb-verdict.is-plain { color: var(--ink-3); }

.film-intro {
  display: flex; gap: 7px; align-items: center;
  /*
   * A little clear of the facts it follows.
   *
   * The row it is on already puts 26px between its items; this adds to that
   * rather than replacing it, because the switch is a different kind of thing
   * from the values beside it and should not read as one more of them.
   */
  margin-left: 12px;
  font-size: 12.5px; color: var(--ink-2); cursor: pointer;
  /* The row aligns on the baseline; a control should sit with the text, not
     hang below it on the strength of its box. */
  align-self: center;
}
.film-intro input { width: auto; margin: 0; }
.film-intro-wait { color: var(--ink-3); }

.film-note { font-size: 12px; color: var(--ink-3); max-width: 70ch; line-height: 1.45; }

/* The title row over the details: no room for a subtitle that is not there. */
.page-head.is-tight { margin-bottom: 12px; align-items: center; }

/*
 * The fixture at the top of a game: two schools and the word between them.
 *
 * The names take the sidebar's own colour rather than the page's near-black.
 * It is the darkest thing on screen already, so the heading reads as part of
 * the same furniture as the menu beside it instead of competing with it.
 *
 * "vs" is not a name and should not be read as loudly as one. Smaller, and at
 * the weight of body text rather than the heading's, so the eye goes to the
 * two schools and takes the join on the way past. Sized in `em` so it stays in
 * proportion if the heading ever changes size.
 */
.gd-fixture { color: var(--sidebar-bg); }
.gd-vs {
  font-size: 0.62em;
  font-weight: 500;
  letter-spacing: 0.04em;
  /* A little air on top of the real spaces either side of it in the markup. */
  margin: 0 0.1em;
  /* Lifted a touch: a smaller word on a shared baseline sits low beside caps. */
  vertical-align: 0.08em;
}


/* ---------- Printing ---------- */

/*
 * A report on paper is the page a coach is looking at, minus the application
 * around it. Everything here either removes furniture or stops the browser
 * making decisions that are right on a screen and wrong on paper.
 *
 * This is why the report is printed rather than drawn: charts are SVG, tables
 * are tables, and the browser already knows how to put both on a page at the
 * right size. Redrawing them somewhere else would be a second version of the
 * report that disagreed with this one the first time either changed.
 */
@media print {
  /*
   * Backgrounds and fills are the report.
   *
   * Browsers drop them by default, on the sound theory that most pages are
   * mostly decoration. Here a bar chart with no fill is a row of empty
   * rectangles and a red result badge is the word "L" on white.
   */
  *, *::before, *::after {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  @page { margin: 14mm 12mm; }

  html, body { background: #fff; }

  /* The application, as opposed to the report. */
  .sidebar,
  .page-head .actions,
  .toast-root,
  #modal-root,
  .crumb,
  .seg,
  .play-toolbar,
  input[type="search"],
  .chart-tip,
  .no-print { display: none !important; }

  /*
   * A chart's scroller is a way of reading a wide chart on a narrow screen.
   * Paper has neither a narrow screen nor a way to scroll, so the figure is
   * given its full width and the mechanism goes.
   */
  .chart-figure { overflow: visible !important; }
  .chart-figure svg { width: 100% !important; height: auto !important; }

  /* One column, no sticky anything, and no invented scrollbars. */
  .app { display: block; }
  .main { display: block; }
  .page { padding: 0; max-width: none; }
  .table-wrap, .dg-frame, .card { overflow: visible !important; }
  .pm-bar, .pm-side, thead th { position: static !important; }

  /*
   * A card is a unit of the report, so it is not split between two sheets
   * unless it is taller than one. `avoid` is a request rather than a rule --
   * the browser will break a card that cannot fit, which is the right answer.
   */
  .card, .tile, .chart-card { break-inside: avoid; page-break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; page-break-after: avoid; }
  tr { break-inside: avoid; page-break-inside: avoid; }
  thead { display: table-header-group; }   /* repeats over a long table */

  /* Paper is not a screen: hairlines and shadows read as smudges. */
  .card { box-shadow: none; border: 1px solid #d8dce2; }

  /* Ink, at a size that survives being printed. */
  body { font-size: 11px; }
  h1 { font-size: 19px; }
  h2 { font-size: 13px; }
  a { color: inherit; text-decoration: none; }

  /*
   * A title for the sheet, since the breadcrumb and the sidebar that said whose
   * report this is are both gone. Filled in by the page before it prints.
   */
  .print-head { display: block !important; margin: 0 0 14px; }
  .print-head h1 { margin: 0 0 3px; }
  .print-head .sub { font-size: 11px; color: #4a5563; }
}

/* Only ever on paper. */
.print-head { display: none; }

/*
 * A player tagged on a play, as a line rather than a row of controls.
 *
 * What was recorded is read far more often than it is changed -- a coach
 * working down the film is checking the tag is right, not editing it -- so the
 * row states it and the editing is a click away.
 */
.tagged-row {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 10px; margin-bottom: 6px;
  border: 1px solid var(--line); border-radius: 8px; background: var(--surface);
}
/* Did something on the play -- carried, caught, tackled -- not only on it. */
.tagged-row.is-highlight {
  border-color: #e0b100;
  background: #fffbe6;
  box-shadow: 0 0 0 1px #e0b100;
}
.tagged-who { flex: 1 1 auto; min-width: 0; }
.tagged-name { font-size: 13px; font-weight: 600; }
.tagged-meta { font-size: 11.5px; color: var(--ink-3); margin-top: 1px; }
.tagged-acts { flex: 0 0 auto; display: flex; gap: 4px; }

/*
 * Picking every role a player had on one snap.
 *
 * Chips rather than a multi-select, because the list is fifteen long and the
 * question is "which of these" rather than "which one" -- a coach reads the
 * lit ones back at a glance and a multi-select makes them scroll to check.
 */
.role-pick { display: flex; flex-wrap: wrap; gap: 6px; }
.role-chip {
  padding: 5px 11px; border-radius: 999px;
  border: 1px solid var(--line-strong); background: var(--surface);
  font: inherit; font-size: 12px; color: var(--ink-2); cursor: pointer;
  white-space: nowrap;
}
.role-chip:hover { border-color: var(--accent); color: var(--accent); }
/* A hidden lookup is still listed here, and reads as switched off. */
/*
 * Narrowing a long lookup. The selects are sized to their content rather than
 * stretched: a filter that spans half the page reads as the subject of the
 * screen, and here it is a way of getting past a hundred rows.
 */
.lookup-filters { display: flex; gap: 8px; align-items: center; margin: 0 0 12px; flex-wrap: wrap; }
.lookup-filters select { width: auto; min-width: 150px; }
.lookup-filters .count-of { font-size: 13px; color: var(--ink-3); white-space: nowrap; }

tr.is-hidden-row td:first-child > div { color: var(--ink-3); }
tr.is-hidden-row .badge { margin-left: 8px; }

.role-chip[aria-pressed="true"] {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600;
}

/* ---------- Cards & tables ---------- */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.card-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--line);
}
.card-head h2 { margin: 0; font-size: 14px; font-weight: 650; }
.card-head .actions { margin-left: auto; display: flex; gap: 6px; }

/*
 * A card folded to its heading.
 *
 * The heading's rule is the line between it and a body; with no body under it
 * the rule is the card's own bottom edge drawn twice, so it goes. The summary
 * beside the title is what the section is currently set to -- the reason a
 * shut page is still worth reading.
 */
.card.is-folded .card-head { border-bottom: none; }
.card-head .fold-summary { font-weight: 500; color: var(--ink-3); }
/* A count of what is under the heading, set quietly so the heading still reads
   as the heading. */
.card-count {
  font-size: 12px; font-weight: 600; color: var(--ink-3);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* Four buttons and a heading do not fit a narrow window on one line. */
.card-head, .card-head .actions { flex-wrap: wrap; }
.card-body { padding: 16px; }
.card + .card { margin-top: 18px; }
/* Inside a grid the gap already separates them; the stacking margin would push
   every second card down a row and break the tops out of line. */
.grid > .card + .card { margin-top: 0; }

.grid { display: grid; gap: 18px; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }

.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th {
  text-align: left;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-2);
  font-weight: 650;
  padding: 9px 10px;
  border-bottom: 1px solid var(--line-strong);
  background: var(--surface-2);
  white-space: nowrap;
  position: sticky;
  top: 0;
  z-index: 1;
}
td { padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; }
tbody tr:last-child td { border-bottom: none; }
tbody tr.clickable { cursor: pointer; }
tbody tr.clickable:hover { background: var(--surface-2); }
.num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--mono); font-size: 12.5px; }
.muted { color: var(--ink-3); }
.nowrap { white-space: nowrap; }

/* ---------- Stat tiles ---------- */

.stat-tiles { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.tile {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
  box-shadow: var(--shadow);
}
.tile .label {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
  font-weight: 650;
}
.tile .value {
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin-top: 4px;
  font-variant-numeric: tabular-nums;
}
.tile .value small { font-size: 13px; font-weight: 500; color: var(--ink-3); }
.tile .delta { font-size: 12px; margin-top: 3px; color: var(--ink-2); }
.tile.win { border-left: 3px solid var(--good); }
.tile.loss { border-left: 3px solid var(--bad); }

/* Comparison rows: us vs them with a shared bar. */
.compare { display: flex; flex-direction: column; gap: 2px; }
.compare-row { display: grid; grid-template-columns: 62px 1fr 130px 1fr 62px; align-items: center; gap: 10px; padding: 7px 0; }
.compare-row + .compare-row { border-top: 1px solid var(--line); }
.compare-row .label {
  text-align: center;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-2);
  font-weight: 650;
}
.compare-row .v { font-variant-numeric: tabular-nums; font-weight: 650; font-size: 14px; }
.compare-row .v.left { text-align: right; }
.compare-row .bar { height: 7px; border-radius: 4px; background: var(--line); overflow: hidden; display: flex; }
.compare-row .bar.left { justify-content: flex-end; }
.compare-row .bar i { display: block; height: 100%; }
.compare .v.left.team-label { color: var(--accent); }
.compare-row .bar.left i { background: var(--chart-us); }
.compare-row .bar.right i { background: var(--chart-them); }

/* ---------- Badges ---------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.02em;
  background: var(--line);
  color: var(--ink-2);
  white-space: nowrap;
}
.badge.offense { background: color-mix(in srgb, var(--offense) 14%, #fff); color: var(--offense); }
.badge.defense { background: color-mix(in srgb, var(--defense) 16%, #fff); color: var(--defense); }
.badge.special_teams { background: color-mix(in srgb, var(--special) 14%, #fff); color: var(--special); }
.badge.good { background: color-mix(in srgb, var(--good) 14%, #fff); color: var(--good); }
/* A breakdown only the coaches can see yet. */
.publish-chip {
  display: inline-flex; align-items: center; vertical-align: middle;
  padding: 2px 9px; border-radius: 999px; font-size: 11.5px; font-weight: 600; letter-spacing: 0;
}
h1 .publish-chip { margin-left: 12px; font-size: 12px; }
.publish-chip.is-draft { background: #eef0f4; color: #4a5563; box-shadow: inset 0 0 0 1px #cbd1da; }
/* Its opposite. Green rather than another grey: the pair is read at a glance
   from across the page, and two neutrals would need the words to tell apart. */
.publish-chip.is-published {
  background: color-mix(in srgb, var(--good) 12%, #fff); color: var(--good);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--good) 32%, #fff);
}
/* A flag on the play: the yellow of a penalty marker. */
.badge.penalty { background: #fdf0b3; color: #7a5a00; box-shadow: inset 0 0 0 1px #e0b100; }
.badge.bad { background: color-mix(in srgb, var(--bad) 12%, #fff); color: var(--bad); }
.badge.pos { background: #eef2f7; color: #46536b; font-family: var(--mono); font-size: 10.5px; }
/* Where a game is played. Colours kept clear of the unit colours and of the
   win/loss green and red that sit a few columns over. */
.badge.loc-home { background: color-mix(in srgb, #0e7c86 14%, #fff); color: #0b6b74; }
.badge.loc-away { background: color-mix(in srgb, #b7791f 16%, #fff); color: #8a5a00; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface-2);
  font-size: 11.5px;
}
.chip button {
  border: none; background: none; cursor: pointer;
  color: var(--ink-3); font-size: 13px; line-height: 1; padding: 0;
}
.chip button:hover { color: var(--bad); }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }

/* ---------- Play grid ---------- */

.play-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  background: var(--surface-2);
  border-radius: var(--radius) var(--radius) 0 0;
}
.play-toolbar .spacer { margin-left: auto; }
/* The play count, now sitting with the tabs that narrow it. A little air on
   the left so it reads as a figure about the row rather than another tab. */
.play-toolbar .toolbar-count { margin-left: 4px; }
/*
 * Room between the two sets of tabs.
 *
 * When and what are separate questions -- the quarter narrows the film, the
 * unit narrows the kind of play -- and at the toolbar's own 8px the two sets
 * of buttons ran together into one strip a reader had to parse before they
 * could use it. This is the only thing saying they are two controls.
 *
 * An element rather than a margin on the second set, because the toolbar
 * wraps: a margin would survive the line break and indent the unit tabs under
 * the quarters, which reads as a hierarchy that is not there. A spacer simply
 * ends up at the end of the first line and takes no space on the second.
 */
.play-toolbar .toolbar-gap { flex: 0 0 8px; }
.seg { display: inline-flex; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); overflow: hidden; }
.seg button {
  border: none;
  background: var(--surface);
  padding: 6px 11px;
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  color: var(--ink-2);
}
.seg button + button { border-left: 1px solid var(--line-strong); }
.seg button.active { background: var(--ink); color: #fff; font-weight: 600; }

table.plays { font-size: 12.5px; }
table.plays th, table.plays td { padding: 6px 8px; }
table.plays td.end-time { cursor: default; }
/* The play just added, picked out for a moment as it scrolls into view. */
table.plays tr.just-added td { animation: just-added 1.6s ease-out; }
@keyframes just-added { from { background: color-mix(in srgb, #e0b100 28%, transparent); } to { background: transparent; } }
@media (prefers-reduced-motion: reduce) { table.plays tr.just-added td { animation: none; } }
table.plays td.seq { color: var(--ink-3); font-family: var(--mono); font-size: 11.5px; width: 56px; white-space: nowrap; }

table.plays tr.unit-offense td.unit-cell { box-shadow: inset 3px 0 0 var(--offense); }
table.plays tr.unit-defense td.unit-cell { box-shadow: inset 3px 0 0 var(--defense); }
table.plays tr.unit-special_teams td.unit-cell { box-shadow: inset 3px 0 0 var(--special); }
/*
 * The highlight star, beside the play number.
 *
 * Both halves of the cell keep a fixed width so the column has two invisible
 * tracks: numbers left, stars right. The number's track fits three digits,
 * which is every play any film has ever held; the star's is held open whether
 * there is a star in it or not, so the ones that exist line up down the page
 * instead of stepping in and out with the width of the number beside them.
 *
 * Bigger than the row's type on purpose -- it is a mark on the play, not a
 * value in a column, and at the table's own 11.5px it read as punctuation.
 * Line-height 1 keeps it from setting the row's height.
 */
table.plays td.seq .seq-n { display: inline-block; min-width: 21px; }
table.plays td.seq .seq-star {
  display: inline-block;
  width: 16px;
  margin-left: 4px;
  color: var(--highlight);
  font-size: 15px;
  line-height: 1;
  vertical-align: -2px;
  text-align: center;
}
table.plays tr.scoring { background: color-mix(in srgb, var(--good) 7%, transparent); }

/*
 * A punt's two distances, in the column a single yardage used to occupy.
 *
 * Smaller and not bold: it is two facts rather than one, and it should read
 * as a pair of small labels rather than compete with the gains above and
 * below it in the same column.
 */
table.plays td.punt-yards {
  white-space: nowrap; font-size: 11.5px; font-variant-numeric: tabular-nums;
  color: var(--ink-2, var(--ink-3));
}

/* Punt distance and return distance, side by side in the play editor. */
.punt-pair { display: flex; gap: 12px; }
.punt-pair > * { flex: 1 1 0; min-width: 0; }

/*
 * A value nobody has charted, while the quality check is on.
 *
 * The cell itself is tinted rather than a mark being added inside it: a dot or
 * an icon would move the number beside it, and a column that shifts when the
 * check is switched on is a column that has to be re-read. This way the grid
 * keeps its exact shape and the gaps light up in place, so a run of missing
 * end times reads as a stripe down one column -- which is how a coach decides
 * what to fix first.
 *
 * The left edge is scoped away from the unit cell, which already spends its
 * inset shadow on the unit colour and must keep it.
 */
table.plays td.qc-missing {
  background: color-mix(in srgb, var(--warn, #d79a3a) 16%, transparent);
  color: var(--warn-ink, #8a5a00);
}
table.plays td.qc-missing:not(.unit-cell) {
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--warn, #d79a3a) 70%, transparent);
}
/* The dash keeps its place but stops reading as "nothing to see here". */
table.plays td.qc-missing.muted { color: var(--warn-ink, #8a5a00); }
table.plays tr.scoring td.qc-missing,
table.plays tr:hover td.qc-missing {
  background: color-mix(in srgb, var(--warn, #d79a3a) 24%, transparent);
}

/* The summary the check opens with. */
.qc-lines { display: flex; flex-direction: column; gap: 1px; margin: 4px 0 2px; }
.qc-line {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  padding: 7px 10px; border-radius: var(--radius-sm);
}
.qc-line:nth-child(odd) { background: var(--surface-2); }
.qc-label { color: var(--ink-2, var(--ink-3)); }
.qc-count {
  font-variant-numeric: tabular-nums; font-weight: 650;
  color: var(--warn-ink, #8a5a00);
}
.qc-clear { margin: 0; line-height: 1.55; font-weight: 600; color: var(--good); }
table.plays tr.selected { background: var(--accent-soft) !important; }
table.plays td.players { max-width: 200px; overflow: hidden; text-overflow: ellipsis; }
.players-more { color: var(--ink-3); font-weight: 600; letter-spacing: 0.05em; }
table.plays td.notes { width: 1%; text-align: center; color: var(--ink-3); }
.note-mark { display: inline-flex; vertical-align: middle; cursor: help; }
.note-mark:hover { color: var(--ink); }
.ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.half-divider td {
  background: var(--ink);
  color: #fff;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 5px 10px;
}

/* ---------- Drawer ---------- */

.drawer-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(12, 16, 22, 0.42);
  display: flex;
  justify-content: flex-end;
  z-index: 50;
}
.drawer {
  width: min(560px, 100%);
  background: var(--surface);
  height: 100%;
  overflow-y: auto;
  box-shadow: var(--shadow-lg);
  display: flex;
  flex-direction: column;
}
.drawer-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  background: var(--surface);
  z-index: 2;
}
.drawer-head h2 { margin: 0; font-size: 16px; }
/*
 * The head's right-hand column: the buttons, and under them anything that
 * marks the record as a whole.
 *
 * Centred against the title while the buttons are all there is; pinned to the
 * top the moment something sits under them, so the row grows downward from
 * the close button rather than pushing it off centre.
 */
.drawer-head-side { margin-left: auto; display: flex; flex-direction: column; align-items: flex-end; gap: 7px; }
.drawer-head-buttons { display: flex; gap: 4px; }
.drawer-head.has-aside { align-items: flex-start; }
.drawer-body { padding: 20px; flex: 1; }
.drawer-foot {
  display: flex;
  gap: 8px;
  padding: 14px 20px;
  border-top: 1px solid var(--line);
  position: sticky;
  bottom: 0;
  background: var(--surface);
  /*
   * Above the content, the same way the head is.
   *
   * Every `th` in the application is sticky with a z-index of its own, so a
   * table scrolled through the drawer -- the analyser's breakdown, on a play
   * it filled in -- parked its column headings on top of Save and Cancel. The
   * foot was positioned but never given a level, and `z-index: auto` loses to
   * any number at all, whatever the DOM order says. The head has carried a 2
   * since it was written; the foot is the same piece of furniture and gets
   * the same one.
   */
  z-index: 2;
}
.drawer-foot .spacer { margin-left: auto; }

/* Full-screen mode: the drawer takes the viewport and the form's panels flow
   into as many columns as fit, so a long play form stops being a scroll. */
.drawer.expanded { width: 100%; }
.drawer.expanded .drawer-head,
.drawer.expanded .drawer-foot,
.drawer.expanded .drawer-body { padding-left: 28px; padding-right: 28px; }

.drawer-expand { font-size: 15px; line-height: 1; padding: 6px 9px; }

.play-form > .form-panel + .form-panel { margin-top: 4px; }

.drawer.expanded .play-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(310px, 1fr));
  gap: 4px 32px;
  align-items: start;
  max-width: 1700px;
  margin: 0 auto;
}
/* The roster rows need more room than a single narrow column gives them. */
.drawer.expanded .play-form > .form-panel.wide { grid-column: 1 / -1; }
/* Side by side, a panel's top is the grid's; the stacking margin would drop the
   second one below the first. */
.drawer.expanded .play-form > .form-panel + .form-panel { margin-top: 0; }
/*
 * Situation and The play share their row lines, so a field on the left sits
 * level with the one across from it however tall either side's row is (a hint
 * under Playbook play, a two-line unit button). Both panels are built as the
 * same five rows for this. When the columns stack, each simply keeps its own.
 */
.drawer.expanded .play-form > .form-panel.is-rows {
  display: grid;
  grid-row: span 5;
  grid-template-rows: subgrid;
  row-gap: 0;
  align-content: start;
}
.drawer.expanded .play-form > .form-panel.is-rows > * { align-self: start; min-width: 0; }
.drawer.expanded .play-form > .form-panel.wide .section-title { margin-top: 18px; }
.drawer.expanded textarea { min-height: 96px; }

@media (max-width: 700px) {
  .drawer.expanded .drawer-head,
  .drawer.expanded .drawer-foot,
  .drawer.expanded .drawer-body { padding-left: 16px; padding-right: 16px; }
}

.section-title {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--ink-3);
  margin: 22px 0 10px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--line);
}
.section-title:first-child { margin-top: 0; }

/*
 * The highlight mark: one control that is both the switch and its state.
 *
 * A filled star and the word "Highlight" when the play is marked, an outline
 * and "Mark highlight" when it is not -- so the button says what is true now
 * and what clicking it would do, without a separate label to read.
 */
.star-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-3);
  font: inherit;
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  line-height: 1.7;
  cursor: pointer;
  white-space: nowrap;
}
.star-toggle .star-glyph { font-size: 13px; line-height: 1; }
.star-toggle:hover { border-color: var(--highlight); color: var(--highlight); }
.star-toggle.is-on {
  border-color: var(--highlight);
  color: var(--highlight);
  background: color-mix(in srgb, var(--highlight) 10%, #fff);
}

/* ---------- Modal ---------- */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(12, 16, 22, 0.42);
  display: grid;
  place-items: center;
  /*
   * A narrow margin rather than a comfortable one. The gap around a dialog is
   * decoration; the bottom of a dialog is where its buttons are, and a window
   * that spends sixty pixels on breathing room at each end can push the one
   * button somebody came to press off the end of the scroll.
   */
  padding: 12px;
  z-index: 60;
  /*
   * A track that ignores what is in the dialog.
   *
   * The default `auto` track grows to the dialog's max-content width, and a
   * scroll container inside the dialog does not shrink that: the dialog kept
   * its 620 pixels, but the track behind it grew to the width of a 3,300
   * pixel scoreboard strip scrolling inside, and centring a 620 pixel box in
   * a 3,300 pixel track put its left edge 1,400 pixels in -- most of it past
   * the right of the screen, Confirm button included. Measured, not inferred.
   * `minmax(0, 1fr)` is the track's own width whatever the dialog holds.
   */
  grid-template-columns: minmax(0, 1fr);
}
.modal {
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  width: min(520px, 100%);
  max-height: 94vh;
  display: flex;
  flex-direction: column;
}
.modal-head { padding: 18px 20px 0; }
.modal-head h2 { margin: 0 0 4px; font-size: 17px; }
.modal-body { padding: 16px 20px; overflow-y: auto; }
.modal-foot { display: flex; gap: 8px; justify-content: flex-end; padding: 14px 20px; border-top: 1px solid var(--line); }

/* ---------- Toast ---------- */

.toast-root {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 80;
  pointer-events: none;
}
.toast {
  background: var(--ink);
  color: #fff;
  padding: 10px 16px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  box-shadow: var(--shadow-lg);
  animation: toast-in 0.18s ease-out;
}
.toast.error { background: var(--bad); }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ---------- Empty & misc ---------- */

.empty {
  text-align: center;
  padding: 48px 20px;
  color: var(--ink-3);
}
.empty .big { font-size: 34px; margin-bottom: 10px; }
.empty h3 { margin: 0 0 6px; color: var(--ink); font-size: 15px; }
.empty p { margin: 0 auto 16px; max-width: 380px; font-size: 13px; line-height: 1.5; }

.loading { padding: 40px; text-align: center; color: var(--ink-3); font-size: 13px; }

.bar-list { display: flex; flex-direction: column; gap: 8px; }
.bar-item { display: grid; grid-template-columns: 108px 1fr 46px; gap: 10px; align-items: center; font-size: 12.5px; }
.bar-item .track { height: 8px; background: var(--line); border-radius: 4px; overflow: hidden; }
.bar-item .track i { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
.bar-item .n { text-align: right; font-variant-numeric: tabular-nums; color: var(--ink-2); }

.split-bar { display: flex; height: 26px; border-radius: var(--radius-sm); overflow: hidden; font-size: 11px; font-weight: 650; }
.split-bar i { display: grid; place-items: center; color: #fff; font-style: normal; }
.split-bar i.run { background: var(--offense); }
.split-bar i.pass { background: var(--accent); }

/* ---------- Season report ---------- */

.scope-picker {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 12px;
  color: var(--ink-2);
}
.scope-picker select { width: auto; min-width: 170px; }


.panel-note {
  margin: -4px 0 14px;
  font-size: 12.5px;
  color: var(--ink-3);
  line-height: 1.5;
}

/* Findings */
.finding-list { display: flex; flex-direction: column; gap: 10px; }
.finding {
  border: 1px solid var(--line);
  border-left: 3px solid var(--line-strong);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  background: var(--surface-2);
}
.finding.major { border-left-color: var(--ink-2); }
.finding-head { display: flex; align-items: baseline; gap: 10px; font-size: 13px; }
.finding-gap {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.finding-body { font-size: 12.5px; color: var(--ink-2); margin-top: 4px; line-height: 1.5; }

.driver-list { display: flex; flex-direction: column; gap: 6px; }
.driver {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 13px;
  padding: 6px 0;
  border-bottom: 1px solid var(--line);
}
.driver:last-child { border-bottom: none; }
.driver .muted { margin-left: auto; font-size: 12px; }

/* Trends */
.trend-list { display: flex; flex-direction: column; gap: 2px; }
.trend {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line);
  font-size: 13px;
}
.trend:last-child { border-bottom: none; }
.trend-values {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  font-variant-numeric: tabular-nums;
}
.trend-arrow { color: var(--ink-3); font-size: 11px; }

/* Charts */
/*
 * No scroll box around a chart. One that could scroll sideways also scrolled
 * up and down, so a hover tooltip or a bar's value poking a few pixels past
 * the edge put a scrollbar beside the bars. Only a phone is narrow enough for
 * the line chart's minimum width to need the sideways scroll.
 */
.chart-figure { position: relative; }
@media (max-width: 480px) {
  .chart-figure:not(.hbars) { overflow-x: auto; }
}
.chart { width: 100%; min-width: 320px; height: auto; display: block; }
.chart-axis-label { font-size: 11px; fill: var(--ink-3); font-family: var(--font); }
.chart-end-label {
  font-size: 11.5px;
  fill: var(--ink-2);
  font-family: var(--font);
  font-weight: 650;
}
.chart .chart-axis-label, .chart .chart-end-label { font-variant-numeric: tabular-nums; }
.chart rect:focus-visible, .chart circle:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

.chart-legend {
  display: flex;
  gap: 16px;
  margin-bottom: 10px;
  font-size: 12px;
  color: var(--ink-2);
}
.chart-legend .key { display: inline-flex; align-items: center; gap: 6px; }
.chart-legend .key i { width: 10px; height: 10px; border-radius: 2px; display: block; }

.chart-tip {
  position: absolute;
  transform: translate(-50%, -100%) translateY(-8px);
  background: var(--ink);
  color: #fff;
  padding: 6px 9px;
  border-radius: var(--radius-sm);
  font-size: 12px;
  line-height: 1.45;
  pointer-events: none;
  white-space: nowrap;
  z-index: 5;
  box-shadow: var(--shadow);
}

.chart-table { margin-top: 12px; }

/* Share bar */
.share-bar { display: flex; flex-direction: column; gap: 6px; }
.share-track {
  display: flex;
  height: 20px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  gap: 2px;
}
.share-track i { display: block; }
.share-ends {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* Grayscale mode: for pages that should not editorialise with colour. */
/*
 * Us and them, in one grey ramp.
 *
 * The two sides read as a pair rather than as a team colour against a stranger,
 * which is what a comparison is: the same measurement taken twice. Ours is the
 * lighter of the two -- the eye goes to the dark bar, and on a page about how a
 * season is going the number worth looking at is the one you are being beaten
 * by.
 *
 * Both sit clear of the track they run in, which is lighter again.
 */
.neutral .compare .v.left.team-label { color: var(--ink); }
.neutral .compare-row .bar.left i { background: var(--chart-us); }
.neutral .compare-row .bar.right i { background: var(--chart-them); }
.neutral .bar-item .track i { background: var(--ink-2); }
.neutral .split-bar i.run { background: var(--ink-2); }
.neutral .split-bar i.pass { background: var(--ink-3); }
.neutral .tile.win, .neutral .tile.loss { border-left-color: var(--line-strong); }

a.link { color: var(--offense); text-decoration: none; }
a.link:hover { text-decoration: underline; }

/* Film timestamps: subtle in the grid, obviously clickable on hover. */
a.film-link {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border-radius: 4px;
  padding: 1px 4px;
  margin-left: -4px;
  white-space: nowrap;
}
a.film-link .play-icon {
  font-size: 8px;
  color: var(--accent);
  opacity: 0;
  transition: opacity 0.12s ease;
}
a.film-link:hover {
  color: var(--accent);
  background: var(--accent-soft);
  text-decoration: underline;
}
a.film-link:hover .play-icon { opacity: 1; }
a.film-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
tbody tr:hover a.film-link .play-icon { opacity: 0.55; }

a.film-jump {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  height: auto;
  min-height: 0;
  color: var(--accent);
  text-decoration: none;
  padding: 0 12px;
  line-height: 1;
}

/* Beside a film time: reads the viewer's position into the box. */
.film-capture { flex: 0 0 auto; height: auto; min-height: 0; padding: 0 10px; line-height: 1; }

@media (max-width: 860px) {
  .app { grid-template-columns: 1fr; }
  .sidebar { position: static; height: auto; }
  .page { padding: 16px; }
}

/* ---------------------------------------------------------------------------
   Smart Video Analyzer
   --------------------------------------------------------------------------- */

/*
 * Tighter than the rest of the application's panels, on purpose.
 *
 * This dialog is four stacked blocks and a button, and the button is the whole
 * point of it. Every pixel of padding here is a pixel of the Analyze button's
 * chance of being on screen when somebody opens this on a laptop, and padding
 * loses that argument.
 */
.analyze .panel {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 11px 14px;
  margin-bottom: 10px;
}
.analyze .panel:last-child { margin-bottom: 0; }
/*
 * The one panel that is a decision rather than a description, held against the
 * bottom of the scroll so it is on screen whatever is above it. The shadow is
 * what says there is more behind it rather than that the list has ended.
 */
.analyze .panel.is-action {
  position: sticky;
  bottom: -1px;
  z-index: 1;
  background: var(--surface);
  box-shadow: 0 -10px 18px -12px rgb(16 24 40 / 0.28);
}
.analyze .section-title { margin-bottom: 8px; }

.row.gap { display: flex; gap: 10px; align-items: flex-start; }
.row.gap.between { justify-content: space-between; align-items: center; }

/*
 * A setup panel folded to a line while the run has the window.
 *
 * The whole row is the control -- it is one strip of text with nothing else on
 * it to click, and a chevron on the end would be a smaller target for the same
 * job. "Show" is there to say the row does something, not to be aimed at.
 */
/*
 * An offer: the button, and beside it the terms it commits you to.
 *
 * Side by side rather than stacked, so the button is the thing on the row and
 * the words are its footnote. The note takes whatever width is left and wraps
 * there; on a narrow window the row folds and the words go back underneath,
 * which is the only place they fit.
 */
.offer { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.offer > .btn { flex: 0 0 auto; }
.offer-note {
  flex: 1 1 260px;
  margin: 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink-2);
}

.analyze .panel.is-folded {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 9px 14px;
  background: var(--surface-2);
}
/* The heading at the start of the line is the button now (foldToggle), not the whole line. */
.analyze .panel.is-folded .section-title { margin: 0; flex: 0 0 auto; }
/* The filename is the one part that can be long, so it is the part that gives. */
.fold-say {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/*
 * Show and Hide are the same control in the same place: one is a word inside
 * the folded row, the other a button in the heading of the panel it opens into.
 * They are styled together so that toggling does not move anything.
 */
.fold-more {
  flex: 0 0 auto;
  font-size: 12px;
  color: var(--accent);
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
button.fold-more:hover { text-decoration: underline; }

/* A heading with that control on the end of it. */
.analyze .section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
}
.analyze .section-head .section-title { margin-bottom: 0; }

/* ---------------------------------------------------------------------------
   The banner at the top of the analyzer, and the play it runs

   Three states live in one box of fixed shape: the mark, the mark spinning,
   and the field. Fixed shape because they cross-fade over each other and a
   box that resized between them would shove the whole dialog up and down --
   so the height comes from an aspect ratio and every layer fills it.

   The field's colours are borrowed from the diagram tokens rather than
   reinvented: red is the ball, black is a man's path, and those mean something
   on a whiteboard. The grass is a tint of the interface's own green and not a
   picture of turf -- this sits above a table of results, and a saturated field
   would take the page away from them.
   --------------------------------------------------------------------------- */

.ab {
  --td-carry: #e8112d;
  --td-route: #111111;
  position: relative;
  aspect-ratio: 600 / 135;
  /*
   * The shape comes from the ratio; the floor comes from what is inside it.
   * On a narrow window the ratio alone made the box shorter than the mark, the
   * name and the line under them, and the line was clipped off the bottom.
   */
  min-height: 104px;
  margin-bottom: 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface-2);
}

/* Both layers fill the box; which one is visible is the only thing that moves. */
.ab-plate, .ab .td-svg {
  position: absolute;
  inset: 0;
  transition: opacity 380ms ease;
}
.ab-plate {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.ab .td-svg { width: 100%; height: 100%; display: block; opacity: 0; }

.ab[data-state="scoring"] .ab-plate { opacity: 0; }
.ab[data-state="scoring"] .td-svg { opacity: 1; }

/* Perspective on the parent, so the ball turns rather than squashing. */
/*
 * The lockup as the corner of the application draws it, at this size.
 *
 * Same three decisions: no gap, because the football's own box already leaves
 * room to its right; semibold rather than bold, so the name's stems sit near
 * the weight of the stroke beside them; and a name sized so the capitals keep
 * their fixed relationship to the ball -- 46px against a 58px mark is the
 * sidebar's 24-against-30, scaled.
 */
.ab-lockup {
  display: flex;
  align-items: center;
  gap: 0;
  perspective: 420px;
}
.ab-mark { display: block; color: var(--accent); }
.ab-word {
  font-weight: 600;
  font-size: 46px;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--ink);
}

/*
 * The ball turning -- a football stood on its point, or a baseball on nothing
 * in particular. One axis, no wobble and no easing:
 * a top that slowed down and sped up again would read as a stutter in the work
 * going on behind it, which is the opposite of what it is there to say.
 *
 * Only the ball. The word beside it does not move, so this is the mark turning
 * rather than the brand being used as a spinner.
 */
@keyframes ab-spin { to { transform: rotateY(360deg); } }
/*
 * Turning while the film is read, and still turning while the line is written.
 * The spin means "not finished", and the line being typed is the last part of
 * not being finished -- so it runs through both and stops on the last letter.
 */
.ab[data-state="running"] .ab-mark,
.ab[data-state="typing"] .ab-mark {
  animation: ab-spin 2.8s linear infinite;
  transform-style: preserve-3d;
}

/* Earned, not decoration: it appears when a run has come back with plays in it. */
.ab-tag {
  font-size: 13px;
  letter-spacing: 0.06em;
  /* Set as written. A lowercase transform here turned "AI X" into "ai x". */
  color: var(--ink-3);
  opacity: 0;
  display: flex;
  align-items: baseline;
  justify-content: center;
}
.ab[data-state="typing"] .ab-tag,
.ab[data-state="proud"] .ab-tag { opacity: 1; }

/*
 * One character, waiting its turn.
 *
 * Hidden rather than absent: every letter holds its place from the start, so
 * the line does not grow outwards from its own centre and shuffle everything
 * already typed sideways on each keystroke. `visibility` rather than `opacity`
 * because a character that fades in is a character arriving softly, and a
 * keystroke is not a soft thing.
 */
.ab-key { visibility: hidden; white-space: pre; }
.ab-key.is-in { visibility: visible; }
/* The name the whole line is about, from AI to the end of it. */
.ab-key.is-ai { color: var(--accent); font-weight: 650; }

/*
 * The cursor, blinking where the next character will land, and gone once
 * there is no next character.
 */
@keyframes ab-caret { 50% { opacity: 0; } }
.ab-caret {
  position: relative;
  display: inline-block;
  /* No width of its own, so travelling between letters moves nothing. */
  width: 0;
  align-self: stretch;
  opacity: 0;
}
.ab-caret::after {
  content: '';
  position: absolute;
  left: 0;
  top: 12%;
  bottom: 12%;
  width: 1px;
  background: var(--ink-3);
}
.ab[data-state="typing"] .ab-caret {
  opacity: 1;
  animation: ab-caret 900ms steps(1, end) infinite;
}

/* The field itself */
.td-grass { fill: color-mix(in srgb, var(--good) 6%, var(--surface)); }
.td-endzone {
  fill: color-mix(in srgb, var(--good) 15%, var(--surface));
  transition: fill 260ms ease;
}
.td-yard { stroke: var(--line); stroke-width: 1; }
.td-hash { stroke: var(--line); stroke-width: 1; }
.td-goal { stroke: var(--line-strong); stroke-width: 2; }
.td-los { stroke: var(--line-strong); stroke-width: 1.5; stroke-dasharray: 6 5; }

/* Painted on the grass: part of the field until it is earned, then lit. */
.td-word {
  /*
   * Sized to the window, not to the end zone. The zone is 200 deep on the field
   * this is drawn on and 135 of that is ever shown, so a word set to fill the
   * zone reads as a word with its ends cut off -- which is exactly how the
   * first version of this came out.
   */
  font: 800 14px/1 var(--font);
  letter-spacing: 0.08em;
  fill: color-mix(in srgb, var(--good) 34%, var(--surface));
  transition: fill 260ms ease;
}

.td-route {
  fill: none;
  stroke: var(--td-route);
  stroke-width: 3.5;
  stroke-linecap: round;
}
.td-carry {
  fill: none;
  stroke: var(--td-carry);
  stroke-width: 4;
  stroke-linecap: round;
}
.td-flight {
  stroke: var(--td-carry);
  stroke-width: 2.5;
  stroke-dasharray: 9 7;
  stroke-linecap: round;
}

.td-mark {
  /* The same token the play diagrams use, so both react to a theme together. */
  fill: var(--dg-player, #ffffff);
  stroke: var(--td-route);
  stroke-width: 2.5;
  filter: drop-shadow(0 2px 2px rgb(0 0 0 / 0.22));
}
.td-letter { font: 700 10px/1 var(--font); fill: var(--td-route); }
.td-cross { stroke: var(--dg-assign, #8b8b8b); stroke-width: 3.5; stroke-linecap: round; }
.td-flash { fill: none; stroke: var(--td-carry); stroke-width: 2.5; }
.td-tip { fill: var(--td-carry); opacity: 0; }

/* The end zone lights up, and the word on it stops being paint. */
.td-svg.is-scored .td-endzone { fill: var(--accent-soft); }
.td-svg.is-scored .td-word { fill: var(--accent); }

/*
 * Asked for less motion: the ball stands still and the play is already over.
 * The cross-fades stay -- an element appearing is not the kind of movement the
 * setting is about, and cutting between the two layers would be harsher than
 * the fade it replaced.
 */
@media (prefers-reduced-motion: reduce) {
  .ab[data-state="running"] .ab-mark,
  .ab[data-state="typing"] .ab-mark { animation: none; }
  .ab-caret { display: none; }
  .td-endzone, .td-word { transition: none; }
}

/* Metadata about the linked film: label over value, wrapping as it needs to. */
/* One row, always. Five of these six values are short and fixed-width, so they
   take their natural size; the filename is the only variable one and is the
   only cell allowed to flex and truncate. Its full text stays on hover. */
.meta-grid {
  display: flex;
  align-items: flex-start;
  gap: 20px;
  margin-top: 9px;
  padding-top: 9px;
  border-top: 1px solid var(--line);
  overflow-x: auto;
}
.meta-cell { flex: 0 0 auto; min-width: 0; }
.meta-cell:first-child { flex: 1 1 90px; }
.meta-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-3); white-space: nowrap; }
.meta-value {
  font-size: 13.5px;
  font-weight: 600;
  margin-top: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A statement about the run rather than about the app: what an engine is, what
   it is measured to do, why something failed. */
.notice {
  margin-top: 10px;
  padding: 8px 11px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--surface-2);
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--ink-2);
}
.notice.good { border-color: color-mix(in srgb, var(--good) 35%, var(--line)); }
.notice.bad  { border-color: color-mix(in srgb, var(--bad) 40%, var(--line)); color: var(--bad); }

.progress {
  height: 6px;
  border-radius: 3px;
  background: var(--surface-2);
  overflow: hidden;
  margin: 12px 0;
}
.progress-bar {
  height: 100%;
  background: var(--accent);
  border-radius: 3px;
  transition: width 0.3s ease;
}

/*
 * The same bar with nothing to measure: work with no percentage to report.
 *
 * Used while a film is searched for its scoreboard, which samples frames
 * across the whole film and either finds one or does not. A determinate bar
 * there would be inventing a number; a stripe travelling the track says
 * "working" and claims nothing else.
 */
.progress.is-seeking .progress-bar {
  width: 32%;
  transition: none;
  animation: seeking 1.5s ease-in-out infinite;
}
@keyframes seeking {
  0% { margin-left: -32%; }
  100% { margin-left: 100%; }
}
/* Somebody who has asked for less movement gets a still bar that still reads
 * as occupied, rather than a track that looks finished. */
@media (prefers-reduced-motion: reduce) {
  .progress.is-seeking .progress-bar { animation: none; width: 100%; opacity: 0.45; }
}

/* The diagnostic trail, folded away until someone wants it. */
details.log { margin-top: 10px; }
details.log summary {
  cursor: pointer;
  font-size: 12px;
  color: var(--ink-3);
  user-select: none;
}
.log-body {
  margin-top: 8px;
  max-height: 180px;
  overflow-y: auto;
  background: var(--surface-2);
  border-radius: 8px;
  padding: 8px 10px;
}
.log-line {
  display: flex;
  gap: 8px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--ink-2);
}
.log-level {
  flex: 0 0 auto;
  width: 42px;
  text-transform: uppercase;
  font-size: 10px;
  letter-spacing: 0.04em;
  padding-top: 2px;
  color: var(--ink-3);
}
.log-level[data-level='error'] { color: var(--bad); }
.log-level[data-level='warn']  { color: var(--ink-1); }

table.mini { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: 12.5px; }
table.mini th {
  text-align: left;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-3);
  padding: 4px 8px 4px 0;
  border-bottom: 1px solid var(--line);
}
table.mini td { padding: 5px 8px 5px 0; border-bottom: 1px solid var(--line); }
table.mini .num { text-align: right; font-variant-numeric: tabular-nums; }

/* Drilldown models: one block per installed version. */
.model-row {
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 12px 14px;
  margin-top: 12px;
}
.model-row.active { border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); }
.model-caps { font-size: 12px; color: var(--ink-2); margin-top: 6px; }
.model-row table.mini { margin-top: 10px; }

/* The wordmark lockup at the top of the sidebar. The mark inherits the sidebar's
   text colour, so it needs no second asset for the dark ground. */
/*
 * User Mode / Admin Mode, under the logo in the Admin Portal.
 *
 * Two halves of one control: each names a place, so it says where you can go
 * as well as where you are. Sized down and quiet -- it is a thing somebody
 * uses twice a day, not a thing they look at.
 */
.portal-modes {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
  /*
   * Beneath the logo, not beside it. The brand row is a flex line, so the
   * toggle claims the whole width and pushes itself onto the next one --
   * which is where it belongs: it is about the page, not about the mark.
   */
  flex: 0 0 100%;
  margin-top: 10px;
  padding: 2px;
  border-radius: 7px;
  background: var(--sidebar-raise);
}
.portal-modes button {
  border: 0;
  padding: 5px 4px;
  border-radius: 5px;
  background: transparent;
  color: var(--sidebar-ink-dim);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}
.portal-modes button:hover { color: var(--sidebar-ink); }
.portal-modes button.is-on {
  background: var(--sidebar-edge);
  color: #fff;
}
.portal-modes button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/*
 * Mark and name as one thing, not two things side by side.
 *
 * No gap at all, which is not as tight as it sounds. The football is drawn
 * 20 units wide inside a 32-unit box, so at a 30px mark there is already
 * about 5px of empty box to the right of the ball's widest point. Setting the
 * gap to zero closes the space the box was wasting and leaves the space the
 * drawing itself provides -- the D lands just past the curve, where the ball
 * has already begun falling away from it.
 *
 * The mark rides taller than the capitals on purpose: 22.5px of football
 * against 16.5px of cap. It reads as an emblem the name is set against rather
 * than as a word with a picture in front of it, which is the point of making
 * it bigger. Tracking stays tight so the name holds together under it.
 */
.brand-lockup { display: flex; align-items: center; gap: 0; }
.brand-lockup .logo-mark { flex: 0 0 auto; display: block; }
/*
 * The football is red in the corner, the word stays white.
 *
 * The mark is drawn with `currentColor`, so this is the whole of it: the
 * sidebar sets white for the wordmark and the mark takes the brand red back
 * off its own colour. Two colours rather than one because the football is the
 * thing being recognised and the word is what it says -- the same division
 * the sign-in screen and the analyser's banner already make.
 */
.brand .logo-mark { color: var(--accent-on-sidebar); }
/*
 * Semibold, not bold, so the name sits at the mark's weight.
 *
 * The football is drawn at 2.4 in a 32-unit grid, which is 1.65px on screen at
 * 22px. Bold put 3.75px of stem beside it -- more than twice the line it was
 * standing next to. The mark could not come up to meet it without losing the
 * three rules inside the ball, so the name comes down instead, which costs
 * nothing but a digit.
 */
.brand-word { font-weight: 600; font-size: 24px; letter-spacing: -0.02em; line-height: 1; }

/* An input and its button as a single control. Stretching the row means the
   button inherits the input's height instead of being nudged level with it —
   the previous hand-alignment sat 12px low, because the label's bottom margin
   is part of the column that flex-end was measuring against. */
.input-with-button { display: flex; align-items: stretch; gap: 8px; }
.input-with-button > input { flex: 1; min-width: 0; width: auto; }
.input-with-button > .btn { flex: 0 0 auto; }

/* ---------------------------------------------------------------------------
 * Play diagrams
 *
 * The drawing keeps its own colour tokens rather than borrowing the interface's.
 * Red for the ball and black for motion are the coaching convention -- they mean
 * something on a whiteboard -- so they must not shift when the surrounding
 * palette is retuned. The field, by contrast, is chrome and follows the theme.
 * ------------------------------------------------------------------------- */
.dg {
  --dg-carry: #e8112d;
  --dg-motion: #111111;
  --dg-assign: #8b8b8b;
  --dg-line: var(--line-strong);
  --dg-ghost: #b3b8c0;
  --dg-ink: #111111;
  --dg-player: #ffffff;
  display: block;
  width: 100%;
  height: auto;
  background: var(--surface);
  border-radius: 6px;
}
.dg .dg-label { font: 600 15px/1 ui-sans-serif, system-ui, sans-serif; }
/*
 * A third character does not fit any of the marks at full size. It sets smaller
 * rather than the marks growing, because a mark's size is set by the distance
 * between the men either side of it and a letter is not a reason to move them.
 */
.dg .dg-label.is-tight, .dg .dg-name.is-tight { font-size: 11.5px; }
/*
 * A name written outside the mark rather than in it, which is where a
 * defender's goes: a cross has no inside. Lighter than the letter in a circle,
 * because at a glance the shapes should still be what reads first.
 */
.dg .dg-name { font: 600 12px/1 ui-sans-serif, system-ui, sans-serif; }
.dg .dg-gap { font: 400 15px/1 ui-serif, Georgia, serif; fill: var(--ink-3); }
.dg .dg-text {
  font: 700 17px/1 ui-serif, Georgia, serif;
  fill: var(--dg-ink);
  letter-spacing: 0.02em;
}
.dg .dg-player { filter: drop-shadow(0 2px 2px rgb(0 0 0 / 0.28)); }
.dg .dg-player.is-ghost { filter: none; }
.dg.is-interactive .dg-player { cursor: grab; }
.dg.is-interactive .dg-player:active { cursor: grabbing; }

.dg-frame { position: relative; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.dg-frame.is-thumb { pointer-events: none; }

/* The editor */
.pm-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 208px; gap: 14px; align-items: start; }

/*
 * The settings that describe the field rather than the work: how deep the
 * quarterback stands, how much open field there is, whether the gaps are
 * numbered, and where the ball goes. Set once and left, so they sit beside the
 * picture they describe instead of in the bar of things reached for every few
 * seconds. The football is last because it is the last thing done: the players
 * go down, they get their assignments, and then the ball is put on them.
 */
/*
 * Parked under the bar rather than at the top of the window, so the two do not
 * overlap. The height comes from the bar itself, measured as it changes: it is
 * two rows on a play and one on a formation, and guessing would leave a gap on
 * one of them and a collision on the other.
 */
.pm-side {
  --pm-control: 24px;
  display: flex; flex-direction: column; gap: 10px;
  position: sticky; top: calc(var(--pm-bar-h, 0px) + 12px);
  /*
   * Never taller than the room left under the bar. Without this the column is
   * as tall as the play beside it, which leaves it nothing to travel through --
   * a sticky box that already fills its own row can never come unstuck from it.
   */
  max-height: calc(100vh - var(--demo-bar-h) - var(--pm-bar-h, 0px) - 28px);
  overflow-y: auto;
}
.pm-side .pm-group { padding: 7px 9px 9px; }
.pm-side .pm-group h4 { margin-bottom: 6px; }
.pm-side .pm-btns { gap: 4px; }
.pm-side .pm-btns button { flex: 0 0 auto; min-width: 0; padding: 4px 7px; font-size: 11.5px; }
.pm-side .pm-hint { font-size: 11.5px; }
.pm-side .pm-group > * + * { margin-top: 6px; }
/* The labels in the side column share a width, the same as the ones in the
   bar do, so the boxes beside them line up down the group. */
.pm-side .pm-inline { width: 100%; }
.pm-side .pm-inline > span { min-width: 66px; }
.pm-side .pm-inline > select { flex: 1 1 auto; width: auto; min-width: 0; }

/*
 * A switch, for the things that are simply on or off.
 *
 * A pressed button says the same and says it in the same red as every other
 * pressed button, so a coach has to work out that this one is a state rather
 * than an action. A switch is only ever a state.
 */
.pm-switch {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 0; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: 11.5px; color: var(--ink-2);
}
.pm-track {
  position: relative; flex: 0 0 auto;
  width: 30px; height: 17px; border-radius: 999px;
  background: var(--line-strong); transition: background 120ms ease;
}
.pm-knob {
  position: absolute; top: 2px; left: 2px;
  width: 13px; height: 13px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / 0.25);
  transition: transform 120ms ease;
}
.pm-switch[aria-checked="true"] .pm-track { background: var(--accent); }
.pm-switch[aria-checked="true"] .pm-knob { transform: translateX(13px); }
.pm-switch:disabled { cursor: default; color: var(--ink-3); }
.pm-switch:disabled .pm-track { background: var(--line); }

@media (prefers-reduced-motion: reduce) {
  .pm-track, .pm-knob { transition: none; }
}

/*
 * Every control, across the top, in fixed groups.
 *
 * They used to be a column beside the field that appeared and vanished as the
 * selection changed, so the panel a coach reached for was rarely the panel that
 * was there. Groups wrap rather than scroll: a control below the fold is only
 * a slower kind of hidden.
 */
/*
 * Stuck to the top, because a control a coach has scrolled past is hidden
 * again -- which was the whole complaint. The play runs under it.
 *
 * The negative inline margin pulls the background out to the card's edges: the
 * card body is padded, and without it the play would show through the gutters
 * either side as it scrolled beneath.
 */
/* Under the demo's strip when the window is what scrolls; see --demo-bar-h. */
html:not(.fill-page) .pm-bar { top: var(--demo-bar-h); }
html:not(.fill-page) .pm-side { top: calc(var(--demo-bar-h) + var(--pm-bar-h, 0px) + 12px); }
.pm-bar {
  position: sticky; top: 0; z-index: 5;
  background: var(--surface);
  /* Close under the name row: the bar's own border already separates it from
     the play, and a rule as well was a line drawn to say what the gap said. */
  margin-inline: -16px; padding: 6px 16px 10px;
  border-bottom: 1px solid var(--line);
  /*
   * Flex, not a grid of equal columns. The grid gave every group the same width
   * whether it wanted it or not, which left dead space beside the narrow ones;
   * growing to fill the row leaves none.
   *
   * Not stretched, though: matching every group in a row to the tallest of them
   * only moves the waste inside the boxes, where it is emptier still.
   */
  display: flex; flex-wrap: wrap; gap: 8px;
  align-items: flex-start; margin-bottom: 14px;
  /*
   * One height for every control in the bar. A button and a text box are built
   * differently enough that the same padding gives two different heights, and
   * two pixels out is exactly the sort of thing that reads as untidy without
   * anyone being able to say why.
   */
  --pm-control: 24px;
}
/*
 * A basis well under the row width, so several groups share a row and then grow
 * to fill it. Sized from their content instead, each one asked for the whole
 * row and took it.
 */
.pm-bar .pm-group { flex: 1 1 240px; padding: 7px 9px 9px; }

/*
 * The Player group in two columns: who he is on the left, what he is called on
 * the right. The labels share a width so the boxes beside them line up, which
 * is the whole reason a column reads as a column.
 */
.pm-cols { display: flex; gap: 12px; align-items: flex-start; }
.pm-stack { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }
/*
 * The last row of the column: what ends the placement sits at its right edge,
 * under the boxes rather than under the labels, because a control that ends
 * something belongs at the end of what it ends. Anything marked `is-away` goes
 * to the far side of the same row -- the gap beside the role buttons, which
 * was empty and is exactly one button wide.
 */
.pm-stack > .pm-btns { align-self: stretch; justify-content: flex-end; }
.pm-stack > .pm-btns > .is-away { margin-right: auto; }
/* The pair that ends a group sits at the right of what it ends. */
.pm-btns.is-quads + .pm-btns,
.pm-btns.is-triples + .pm-btns,
.pm-btns.is-pairs + .pm-btns { justify-content: flex-end; }
.pm-stack > .pm-inline > span { min-width: 38px; }
/*
 * Every box in the column to one width, so it has one edge down both sides.
 * The width is the widest thing that has to fit -- "Focus ring" in the Extra
 * list -- and the name box takes it too even though three characters do not
 * need it: a short box in a column of long ones reads as a mistake.
 */
.pm-stack > .pm-inline > input.input,
.pm-stack > .pm-inline > select { width: 92px; }


/* Smaller here than in a panel: ten groups of controls only fit if each one
 * takes the room it needs and no more. */
.pm-bar .pm-btns { gap: 4px; }
.pm-bar .pm-group > .pm-btns + .pm-btns { margin-top: 4px; }
.pm-bar .pm-btns button { height: var(--pm-control); padding: 0 7px; font-size: 11.5px; }
.pm-bar .pm-hint { font-size: 11.5px; }
.pm-bar .field > label, .pm-bar label { font-size: 10px; }

/*
 * In the bar a button is its own width. Stretching them to fill made every
 * group as tall as the widest one, which is how ten groups became a wall a
 * coach had to scroll past to reach the field.
 */
.pm-bar .pm-btns button { flex: 0 0 auto; min-width: 0; }
/*
 * One row rhythm across the whole bar.
 *
 * A heading occupies exactly one row of it -- a control's height plus the gap
 * that follows every other row -- so a group without one simply starts a row
 * higher and everything below still lines up: Select where a heading would be,
 * then Add player level with Lineman and with Name, all the way down.
 *
 * The text sits at the bottom of that row so it stays close to what it labels.
 */
.pm-bar .pm-group h4 {
  height: var(--pm-control); margin: 0 0 4px;
  display: flex; align-items: flex-end;
}

/* Controls that belong to one thought, read across rather than down. */
.pm-row { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: end; }
.pm-row > * { margin: 0; }
/*
 * The name box, sized to what goes in it: three characters, at the height of
 * the buttons it sits among. Its label reads across rather than stacking above,
 * so the pair is one more control on the row instead of a column of its own.
 *
 * No placeholder either -- an empty box with grey example text in it looks like
 * a box with something already in it, and this one is three characters wide.
 */
.pm-inline { display: inline-flex; align-items: center; gap: 6px; margin: 0; }
.pm-inline > span {
  font-size: 10px; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--ink-2);
}
.pm-inline > input.input, .pm-inline > select {
  height: var(--pm-control); padding: 0 7px; font-size: 11.5px;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--ink);
}
.pm-inline > input.input { width: 46px; text-align: center; }
/*
 * Off looks off, box or dropdown alike. The rule that gave these their size
 * also gave them a background, which was specific enough to beat the general
 * disabled styling -- so the name box sat there looking typeable beside two
 * greyed dropdowns that were not.
 */
.pm-inline > input.input:disabled,
.pm-inline > select:disabled {
  background: var(--surface-2); border-color: var(--line); color: var(--ink-3);
  cursor: default;
}

/*
 * A group that does not apply now still says where it lives. Muted rather than
 * faded to nothing: being told a thing exists but is not for this moment is
 * worth more than being told nothing at all.
 */
.pm-group.is-off { background: var(--surface-2); border-style: dashed; }
.pm-group.is-off h4 { color: var(--ink-3); }
.pm-group.is-off .pm-hint { color: var(--ink-3); }

/* The tags: touched twice a play, so they sit out of the way. */
.pm-foot { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; margin-top: 16px; }
.pm-foot > *:first-child { flex: 1 1 260px; max-width: 420px; }
.pm-group { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; background: var(--surface); }
.pm-group h4 {
  margin: 0 0 8px; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-3); font-weight: 600;
}
.pm-btns { display: flex; flex-wrap: wrap; gap: 6px; }
.pm-btns button {
  flex: 1 1 auto; min-width: 62px; padding: 6px 8px; font-size: 12px; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: 6px; background: var(--surface);
  color: var(--ink-2);
}
.pm-btns button:hover { background: var(--surface-2); }

/*
 * One under another, all the width of the longest label.
 *
 * `fit-content` sizes the column to its widest child and `stretch` then gives
 * every other child that same width, so the set matches without any of them
 * being measured or a width being typed in that would go stale the moment a
 * label changed.
 */
.pm-btns.is-stacked { flex-direction: column; align-items: stretch; width: fit-content; }

/*
 * Two by two, on the same row rhythm as everything else in the bar. Both
 * columns take the width of the widest button so the block has two clean edges
 * rather than six buttons ending wherever their labels happen to.
 */
.pm-btns.is-pairs {
  display: grid; width: fit-content; gap: 4px;
  grid-template-columns: repeat(2, minmax(max-content, 1fr));
}
/*
 * Left, not centred. The buttons are all one width because the block wants two
 * edges, but that width is the longest label's -- centring the shorter ones in
 * it scatters the swatches across the column and there is nothing left to read
 * down.
 */
.pm-btns.is-pairs button { width: 100%; text-align: left; }

/*
 * Three across, for Draw.
 *
 * Seven buttons in two columns ran four rows deep and left the box hanging
 * below the ones beside it. Three columns bring it to three rows, level with
 * Player and Selected line. Nine buttons fill the grid exactly, in the order
 * DRAW_LAYOUT in views/playmaker.js writes them, so nothing is placed by hand.
 */
.pm-btns.is-triples {
  display: grid; width: fit-content; gap: 4px;
  grid-template-columns: repeat(3, minmax(max-content, 1fr));
}
.pm-btns.is-triples button { width: 100%; text-align: left; }

/* The same idea four across, for a group with more to say. */
.pm-btns.is-quads {
  display: grid; width: fit-content; gap: 4px;
  grid-template-columns: repeat(4, minmax(max-content, 1fr));
}
.pm-btns.is-quads button { width: 100%; text-align: left; }

/* A group that wants to be its contents' size, not a share of the row. */
.pm-bar .pm-group.is-snug { flex: 0 0 auto; }
.pm-btns.is-stacked button { text-align: center; }
.pm-btns button[aria-pressed="true"] {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600;
}

/*
 * Switched off, in colour rather than in opacity.
 *
 * Fading the whole button was the quick way and it was wrong twice: text at
 * 45% is hard to read whatever it sits on, and a red button faded to 45% is a
 * hollow red outline that still shouts for attention while saying nothing.
 * Muted ink on a muted ground stays legible and stops asking.
 */
.pm-btns button:disabled, .pm-btns button:disabled:hover {
  cursor: default;
  background: transparent;
  border-color: var(--line);
  color: var(--ink-3);
}

/*
 * Off is off, and every button off looks the same.
 *
 * Two goes were spent trying to keep the current choice legible while the row
 * was disabled -- first white, then a grey fill -- and both had the same fault:
 * a button that looks different looks like it does something different. A row
 * nobody can press has nothing to say about which of them is current, so it
 * says nothing, and the whole row reads as one dead thing.
 */
.pm-btns button[aria-pressed="true"]:disabled,
.pm-btns button[aria-pressed="true"]:disabled:hover {
  background: transparent;
  border-color: var(--line);
  color: var(--ink-3);
  font-weight: 400;
}

/* Including the colour chips: a vivid square is the loudest thing in a row
 * that is meant to be quiet. */
.pm-btns button:disabled .pm-swatch { filter: grayscale(1); opacity: 0.5; }

/* The same for a box that cannot be typed in: greyed, not merely inert. */
.input:disabled, select:disabled {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--ink-3);
  cursor: default;
}
.pm-hint { font-size: 12px; color: var(--ink-2); line-height: 1.5; margin: 0; }
.pm-swatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 5px; vertical-align: -1px; }

@media (max-width: 900px) {
  .pm-wrap { grid-template-columns: minmax(0, 1fr); }
  .pm-side { position: static; flex-direction: row; flex-wrap: wrap; }
}

/* Playbook list */
.pb-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
.pb-card {
  border: 1px solid var(--line); border-radius: 10px; background: var(--surface);
  overflow: hidden; display: flex; flex-direction: column;
}
.pb-card .pb-head { padding: 10px 12px 4px; }
.pb-card h3 { margin: 0; font-size: 14px; }
.pb-card .pb-meta { font-size: 12px; color: var(--ink-3); margin-top: 2px; }
.pb-card .pb-acts { display: flex; gap: 6px; padding: 8px 12px 12px; margin-top: auto; flex-wrap: wrap; }
.pb-card .pb-note { padding: 0 12px; }

/*
 * The Playmaker card's buttons, all one size.
 *
 * Every button is as wide as "Duplicate", the longest label, without a width
 * typed in anywhere: a hidden copy of that word sits in a zero-height second
 * row of each button, so its column is the wider of the two. Change the font
 * or the label and the width follows. They still wrap on a narrow card.
 */
.pb-acts.is-even .btn {
  display: inline-grid;
  grid-template-rows: auto 0;
  gap: 0;
  justify-items: center;
  align-content: center;
}
.pb-acts.is-even .btn::after {
  content: 'Duplicate';
  grid-row: 2;
  grid-column: 1;
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: 600;
}

/*
 * Card size on the Playmaker list: three, five or eight across.
 *
 * The counts are fixed rather than fitted, because "five across" is the thing
 * being asked for -- but only while five across is readable. Below the widths
 * underneath, each size falls back to fitting as many as the screen can hold,
 * so a narrow window shows small cards rather than eight slivers.
 */
.pb-grid.is-lg { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.pb-grid.is-md { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.pb-grid.is-sm { grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 10px; }

/* Eight across is a thumbnail, so the chrome around the picture gives way. */
.pb-grid.is-sm .pb-card h3 { font-size: 12.5px; }
.pb-grid.is-sm .pb-card .pb-head { padding: 8px 8px 3px; }
.pb-grid.is-sm .pb-card .pb-teams,
.pb-grid.is-sm .pb-card .pb-note { padding: 0 8px; }
.pb-grid.is-sm .pb-card .pb-meta { font-size: 11px; }
.pb-grid.is-sm .pb-card .pb-acts { padding: 6px 8px 8px; gap: 4px; }
.pb-grid.is-sm .pb-card .pb-acts .btn { padding: 3px 7px; font-size: 11px; }

@media (max-width: 1500px) { .pb-grid.is-sm { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } }
@media (max-width: 1150px) { .pb-grid.is-md { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } }
@media (max-width: 780px)  { .pb-grid.is-lg { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); } }

/* Animation player */
.anim-bar { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.anim-bar input[type="range"] { flex: 1; }
.anim-time { font-variant-numeric: tabular-nums; font-size: 12px; color: var(--ink-2); min-width: 78px; white-space: nowrap; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .dg { --dg-player: #f2f4f7; }
}

/* Team chips on a play card, and the assignment picker */
.pb-teams { display: flex; flex-wrap: wrap; gap: 5px; padding: 0 12px; }
.pb-chip {
  font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
  padding: 2px 7px; border-radius: 999px; text-decoration: none;
  background: var(--accent-soft); color: var(--accent); border: 1px solid transparent;
}
.pb-chip:hover { border-color: var(--accent); }
.pb-chip.is-none {
  background: transparent; color: var(--ink-3); border: 1px dashed var(--line-strong);
  font-weight: 400;
}

.pick-list { max-height: 340px; overflow-y: auto; border: 1px solid var(--line); border-radius: 8px; }
.pick-row {
  display: flex; align-items: center; gap: 10px; padding: 8px 11px; cursor: pointer;
  border-bottom: 1px solid var(--line); font-size: 13px;
}
.pick-row:last-child { border-bottom: 0; }
.pick-row:hover { background: var(--surface-2); }
.pick-row.is-already { cursor: default; color: var(--ink-3); }
.pick-row.is-already:hover { background: transparent; }
.pick-name { font-weight: 500; }
.pick-meta { margin-left: auto; font-size: 12px; color: var(--ink-3); }

.pm-team {
  display: block; font-size: 12px; padding: 5px 8px; border-radius: 5px;
  text-decoration: none; color: var(--ink-2); background: var(--surface-2);
  border: 1px solid var(--line);
}
.pm-team:hover { border-color: var(--accent); color: var(--accent); }

/*
 * The playback window.
 *
 * Capped by height rather than width: the diagram is 16:9, so a window sized
 * only by width runs off a laptop screen and puts the transport controls below
 * the fold, which is where the Play button is. Bounding the width by the height
 * it implies keeps the whole thing on screen at any window size.
 */
.dg-frame.is-playback { margin: 0 auto; }   /* max-width is set per play, from its own aspect */
.dg-frame.is-playback .dg { width: 100%; height: auto; }
.anim-bar .btn { min-width: 84px; }

.dg .dg-ball { filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.35)); }

/* Editing a drawn route */
.dg .dg-path.is-selected { filter: drop-shadow(0 0 4px var(--accent)); }
.pm-handle {
  fill: var(--surface); stroke: var(--accent); stroke-width: 4; cursor: grab;
}
.pm-handle:hover { fill: var(--accent); }
.pm-handle:active { cursor: grabbing; }
.pm-handle.is-anchor {
  fill: var(--accent); stroke: var(--surface); stroke-width: 3; cursor: default;
}

/* The route tree: ten short names, so let them pack tighter than normal buttons. */

/*
 * The row that describes a play: its name, then the three things that classify
 * it. The name gets twice the width because it is the only one a coach writes
 * rather than picks, and "Counter Trey Weak" does not fit in a quarter of a row.
 *
 * A select is a little taller than a text box by default -- the arrow well --
 * so the height is set on both rather than left to the browser to disagree.
 */
.play-meta {
  display: grid;
  gap: 12px;
  /* minmax(0, ...) because an fr column will not shrink below its content, and
     a select is as wide as its longest option -- "Trips Right" was quietly
     taking width from the name it was supposed to be narrower than. */
  grid-template-columns: minmax(0, 2.2fr) minmax(0, 1fr) minmax(0, 1.3fr) minmax(0, 1fr);
  align-items: end;
  margin-bottom: 4px;
}
/* The grid's own gap does the spacing; a label's stock bottom margin was
   adding twelve more pixels under the row than anybody asked for. */
.play-meta .field { margin-bottom: 0; }
/* With a Team first -- a scouted play -- five on the row, the team narrow. */
.play-meta.has-team {
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1.3fr) minmax(0, 1fr);
}
.play-meta .field > input,
.play-meta .field select { height: 34px; }
@media (max-width: 820px) {
  .play-meta, .play-meta.has-team { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
}

/*
 * Inside the tool panel a stacked label costs a whole line each, and the panel
 * is 250px wide with a lot to say. The label goes in front of its control
 * instead: a select is short enough to share the row, and three of these in the
 * football group were three lines that are now none.
 *
 * Only in the panel. The header row keeps its labels above, where there is
 * width to spare and the stack reads as a form rather than a list of settings.
 */
.pm-group label.field { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
.pm-group label.field > span {
  display: block;
  margin-bottom: 0;
  flex: 0 0 auto;
  white-space: nowrap;
}
.pm-group label.field > select,
.pm-group label.field > .input { flex: 1 1 auto; min-width: 0; }

/*
 * The handle on the end of a motion line. Invisible until the pointer finds it,
 * because the diagram should read as a drawing rather than as a set of controls
 * -- but it has to be findable, so it lights up rather than merely working.
 */
.dg .dg-tail { fill: transparent; stroke: transparent; stroke-width: 2; }
.dg.is-interactive .dg-tail { cursor: crosshair; }
.dg.is-interactive .dg-tail:hover {
  fill: color-mix(in srgb, var(--accent) 16%, transparent);
  stroke: var(--accent);
}

/* A play card you can open by double-clicking it. */
.pb-card.is-openable > .dg-frame,
.pb-card.is-openable > .pb-head { cursor: pointer; }

/* The on/off switch on a formation card. */
.fm-toggle {
  font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
  padding: 3px 9px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink-3);
}
.fm-toggle[aria-pressed="true"] {
  background: var(--accent-soft); color: var(--accent); border-color: transparent;
}
.fm-toggle:hover { border-color: var(--accent); }
/*
 * Bringing a diagram in from a picture.
 *
 * The picture is shown at the size it will actually be read at, because that is
 * the thing a coach can judge: a photograph too blurred to read here is one the
 * reader will struggle with too, and seeing it beats being told afterwards.
 */
.import-drop {
  display: grid; gap: 10px; padding: 12px;
  border: 1px dashed var(--line-strong); border-radius: 10px;
  background: var(--surface-2, var(--surface));
}
.import-drop.is-over { border-color: var(--accent); border-style: solid; }
.import-preview {
  display: block; width: 100%; max-height: 260px;
  object-fit: contain; border-radius: 8px;
  background: #fff; border: 1px solid var(--line);
}
.import-drop .pm-hint { margin: 0; }
.import-drop .pm-hint.is-bad { color: var(--accent); }

/*
 * Picking from the formations a program has just published.
 *
 * Tiles rather than a list of names: a formation is a shape, and a coach knows
 * one by looking at it far quicker than by reading what it is called. Each is a
 * button, so the keyboard reaches them and the pressed state is real rather
 * than drawn.
 */
.fm-pick-grid {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.fm-pick {
  position: relative; display: flex; flex-direction: column; gap: 2px;
  padding: 8px; text-align: left; cursor: pointer;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--surface); color: inherit; font: inherit;
}
.fm-pick:hover { border-color: var(--line-strong); }
.fm-pick.is-on { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.fm-pick .dg-frame { width: 100%; }
.fm-pick-name { font-size: 13px; font-weight: 600; }
.fm-pick-unit { font-size: 11.5px; color: var(--ink-3); }

/* The tick only exists once it means something. */
.fm-pick-mark {
  position: absolute; top: 6px; right: 6px; z-index: 1;
  width: 18px; height: 18px; border-radius: 999px;
  display: grid; place-items: center;
  font-size: 11px; line-height: 1; color: #fff;
  background: var(--accent); opacity: 0; transform: scale(0.7);
  transition: opacity 120ms ease, transform 120ms ease;
}
.fm-pick.is-on .fm-pick-mark { opacity: 1; transform: scale(1); }

@media (prefers-reduced-motion: reduce) {
  .fm-pick-mark { transition: none; }
}

/* A formation that is off still has to be readable -- it is the thing you came
   here to switch back on -- so it is dimmed rather than greyed out. */
.pb-card.is-off .dg-frame, .pb-card.is-off .pb-head { opacity: 0.5; }

/*
 * The placement guide: where a line would sit, on a field that has none yet.
 * Faint enough to read as a suggestion rather than as players, and gone as soon
 * as there are real linemen to derive gap numbers from.
 */
.dg .dg-guide-spot {
  fill: none;
  stroke: var(--dg-line, #c8ccd2);
  stroke-width: 2;
  stroke-dasharray: 4 5;
}
.dg .dg-gap.is-guide { opacity: 0.55; }

/* One-sided figures: everything summed from our own tagged players. */
.tagged-groups {
  display: grid; gap: 18px 28px;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
.tagged-stat {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; padding: 5px 0; border-bottom: 1px solid var(--line);
}
.tagged-group .tagged-stat:last-child { border-bottom: 0; }
.tagged-stat-label { color: var(--ink-2); font-size: 13px; }
.tagged-stat-value { font-weight: 700; font-variant-numeric: tabular-nums; }

/* Field goals by distance: five bands that add up to the total. */
.fg-ranges { display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: 10px; }
.fg-band {
  display: flex; flex-direction: column; gap: 2px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 6px; background: var(--surface-2);
}
.fg-band-label { font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--ink-3); }
.fg-band-value { font-weight: 700; font-variant-numeric: tabular-nums; }

/* Raw data a calculation needs, entered in a form it cannot use. */
.field-warning {
  margin: -4px 0 10px; padding: 6px 10px; font-size: 12px; line-height: 1.4;
  color: var(--warn-ink, #8a5a00); background: var(--warn-bg, #fdf3e0);
  border-left: 3px solid var(--warn, #d79a3a); border-radius: 0 4px 4px 0;
}

/* A figure the application knows about and nobody has charted yet. */
.compare-row.is-blank, .tagged-stat.is-blank { opacity: 0.45; }
.compare-row.is-blank .bar i { background: var(--ink-3); }

/* A measurement typed in the units it is spoken in. */
.height-input { display: flex; align-items: center; gap: 6px; }
.height-input input { width: 72px; }
.height-input .unit { color: var(--ink-3); font-size: 12px; }
.check-row { display: flex; align-items: center; gap: 8px; margin: 10px 0 2px; }
.check-row input { width: auto; }

/*
 * The delete-reason list: every reason at once, ticked rather than Ctrl-clicked.
 *
 * Bordered like the field it replaced, so it still reads as one list and not as
 * ten loose questions -- but with no height cap, because the whole point is
 * that nothing is hidden. Ten rows is about 300px, which the dialog carries
 * without scrolling on any screen a coach charts film on.
 */
.reason-list {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 4px;
}
.reason-row {
  display: flex;
  /* Not centred: a label that wraps should hang off its box, not straddle it. */
  align-items: flex-start;
  gap: 9px;
  padding: 6px 8px;
  border-radius: calc(var(--radius) - 2px);
  cursor: pointer;
  line-height: 1.4;
}
.reason-row input { width: auto; margin: 2px 0 0; flex: 0 0 auto; }
.reason-row:hover { background: var(--surface-2); }
/* The whole row is the target, so the focus ring belongs to the row. */
.reason-row:focus-within { background: var(--surface-2); outline: 2px solid var(--accent); outline-offset: -2px; }
.badge.captain {
  margin-left: 6px; font-weight: 700; letter-spacing: 0.02em;
  background: var(--accent-soft, #fbe9ea); color: var(--accent, #b5262c);
}

/* A game's plays, folded away until somebody asks for them. */
tr.game-group { cursor: pointer; background: var(--surface-2); }
tr.game-group:hover { background: var(--line); }
tr.game-group td {
  font-weight: 650; padding: 9px 12px;
  border-top: 1px solid var(--line-strong, var(--line));
}
.disclosure {
  display: inline-block; width: 14px; color: var(--ink-3);
  font-size: 11px; transition: none;
}
.game-group-name { margin-right: 8px; }
.game-group-date, .game-group-count {
  font-weight: 400; font-size: 12px; color: var(--ink-3); margin-right: 10px;
}

/* The depth chart on paper: a sheet to hand round, not a screen shot. */
.print-only { display: none; }
@media print {
  .print-only { display: block; }
  .print-head h1 { margin: 0 0 2px; font-size: 18pt; }
  .print-head .sub { color: #444; margin-bottom: 10px; }
  /* A select prints as a control with a chevron; on paper it is just a name. */
  .card select {
    appearance: none; -webkit-appearance: none;
    border: 0; padding: 0; background: none; font-size: 10pt; color: #000;
  }
}

/* Grouped bars lying down: a row per category, a thin bar per side. */
.hbars { position: relative; display: flex; flex-direction: column; gap: 12px; margin: 8px 0 6px; }
.hbar-row { display: grid; grid-template-columns: 48px 1fr; align-items: center; gap: 12px; }
.hbar-label { font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.hbar-pair { display: flex; flex-direction: column; gap: 3px; }
.hbar { display: flex; align-items: center; gap: 8px; height: 11px; cursor: default; }
/* The track is what a hover or focus lands on, so the tooltip finds its label. */
.hbar > * { pointer-events: none; }
.hbar-fill { display: block; height: 100%; border-radius: 0 3px 3px 0; }
.hbar-value {
  font-size: 11.5px; line-height: 1; color: var(--ink-2);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.hbar:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }


/* The Admin Portal's name beside the wordmark: a quiet label, not a badge. */
.brand-tag {
  font-size: 10.5px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
  opacity: 0.6; align-self: center;
  /*
   * Its own space, because the lockup's is now zero.
   *
   * The mark and the name are deliberately hard against each other -- the
   * football's own box supplies the only gap they need. ADMIN has no such box
   * and sat straight against the final N, reading as one word. This is the
   * gap the row used to give it, given here instead.
   */
  margin-left: 7px;
}

/*
 * Tabs across a page (Statistics: Team | Player). Underlined rather than boxed,
 * so they read as views of one page rather than as buttons that do something.
 */
.page-tabs {
  display: flex; gap: 4px; margin: -4px 0 18px;
  border-bottom: 1px solid var(--line);
}
.page-tabs a {
  padding: 8px 14px; margin-bottom: -1px;
  font-size: 13.5px; font-weight: 600; color: var(--ink-3); text-decoration: none;
  border-bottom: 2px solid transparent;
}
.page-tabs a:hover { color: var(--ink); }
.page-tabs a.active { color: var(--ink); border-bottom-color: var(--accent); }
.page-tabs a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 4px; }

/* Players on this play, in the play viewer: a section title that folds. */
details.pv-players > summary {
  display: flex; align-items: center; gap: 8px;
  cursor: pointer; list-style: none; user-select: none;
}
details.pv-players > summary::-webkit-details-marker { display: none; }
details.pv-players > summary::before {
  content: ''; width: 0; height: 0; flex: 0 0 auto;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  transition: transform 0.15s ease;
}
details.pv-players[open] > summary::before { transform: rotate(90deg); }
details.pv-players > summary:hover { color: var(--ink); }
details.pv-players > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
details.pv-players:not([open]) > summary { margin-bottom: 0; }
.pv-count { font-weight: 600; letter-spacing: 0; color: var(--ink-3); }
@media (prefers-reduced-motion: reduce) {
  details.pv-players > summary::before { transition: none; }
}

/* A button that is only an icon: square-ish, the icon centred, the same height
   as the text buttons beside it (the icon is no taller than their line). */
.btn.btn-icon { padding-left: 7px; padding-right: 7px; justify-content: center; }
.btn.btn-icon .icon { display: block; flex: 0 0 auto; }

/*
 * The refresh beside a count, and what it does while it is working.
 *
 * Pulled back against the count it belongs to -- the head's own gap is set for
 * a heading next to a title, which is too much air between a number and the
 * button that re-reads it.
 *
 * The arrow turns while the request is out. Nothing else on the page says a
 * refresh is happening: the rows do not empty and the count does not clear, so
 * without this a slow network looks like a button that did nothing.
 */
.card-head .count-refresh { margin-left: -4px; color: var(--ink-3); }
.card-head .count-refresh:hover:not(:disabled) { color: var(--ink); }
@keyframes icon-spin { to { transform: rotate(360deg); } }
.btn.is-spinning .icon-refresh {
  animation: icon-spin 0.8s linear infinite;
  transform-origin: 50% 50%;
}
/* Asked for less motion: the button still disables, which is the other half of
   the answer and the half that does not move. */
@media (prefers-reduced-motion: reduce) {
  .btn.is-spinning .icon-refresh { animation: none; }
}
.icon-sparkle { fill: var(--accent); stroke: var(--accent); }
.btn:disabled .icon-sparkle { fill: currentColor; stroke: currentColor; }

/* The quieter end of a page title ("Games & Scrimmages"): charcoal, lighter. */
.title-aside { color: var(--ink-2); font-weight: 500; }

/* A window title with an icon in front of it (Smart Video Analyzer). */
.modal-head h2.has-icon { display: flex; align-items: center; gap: 9px; }
/*
 * A tagline riding after a dialog's title, set the way the banner sets it: the
 * same size, weight and grey, so the phrase reads as one thing wherever it
 * appears. Lighter than the title on purpose -- it is what the thing does, not
 * what it is called, and a bold second clause would make the heading a
 * sentence with two subjects.
 */
.modal-head h2 .title-tag {
  margin-left: 9px;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0.06em;
  color: var(--ink-3);
}
/* The name it introduces, coloured the way the banner colours it. */
.modal-head h2 .title-tag-name { color: var(--accent); font-weight: 650; }
.modal-head h2.has-icon .icon { display: block; flex: 0 0 auto; }

/* ---------- Statistic abbreviations ---------- */

/*
 * A chip that explains itself: grey, bordered, a hint of lift, and a help
 * cursor -- so it reads as something to point at rather than a label.
 */
.stat-chip {
  display: inline-flex; align-items: center;
  padding: 1px 6px; border-radius: 5px;
  background: #f1f3f6; color: var(--ink-2);
  border: 1px solid #d9dee5; box-shadow: 0 1px 0 rgb(20 24 31 / 0.06);
  font-size: 11px; font-weight: 650; letter-spacing: 0.02em; line-height: 1.5;
  text-transform: none; white-space: nowrap;
  cursor: help;
}
.stat-chip:hover, .stat-chip:focus-visible {
  background: #e7eaf0; border-color: #c3cad4; color: var(--ink); outline: none;
}
.stat-chip:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent); }

.stat-tip {
  position: fixed; z-index: 1000; max-width: 260px;
  padding: 8px 10px; border-radius: 8px;
  background: #1b2029; color: #fff;
  box-shadow: 0 6px 18px rgb(0 0 0 / 0.22);
  font-size: 12px; line-height: 1.45; pointer-events: none;
}
.stat-tip-name { font-weight: 700; margin-bottom: 2px; }
.stat-tip-about { color: #d3d8e0; }

/* A player's line: each figure its chip and its number, wrapping as needed. */
.stat-line { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; }
.stat-pair { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.stat-val { font-variant-numeric: tabular-nums; font-weight: 600; }

/* Table headings keep their own weight; the chip sets its own. */
th .stat-chip { vertical-align: middle; }

/* The link beside the Statistics tabs. */
.page-tabs a.page-tabs-aside {
  margin-left: auto; border-bottom: 0; margin-bottom: 0; align-self: center; padding: 6px 2px;
  font-size: 12.5px; font-weight: 600; color: var(--ink-3); text-decoration: none;
}
.page-tabs a.page-tabs-aside:hover { color: var(--accent); }

/* The glossary page. */
.glossary-search { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
.glossary-search .input { flex: 1 1 auto; min-width: 0; max-width: 420px; }
.glossary-group + .glossary-group { margin-top: 6px; }
.glossary-row {
  display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 12px;
  padding: 10px 2px; border-bottom: 1px solid var(--line);
}
.glossary-row:last-child { border-bottom: 0; }
.glossary-abbr { padding-top: 1px; }
.glossary-name { font-weight: 600; font-size: 13.5px; }
.glossary-about { color: var(--ink-2); font-size: 13px; margin-top: 2px; max-width: 65ch; }
@media (max-width: 520px) {
  .glossary-row { grid-template-columns: 1fr; gap: 4px; }
  .glossary-search { flex-wrap: wrap; }
}
@media (prefers-reduced-motion: no-preference) {
  .stat-chip { transition: background-color 0.1s ease, border-color 0.1s ease; }
}

/* Tabs can be buttons as well as links (Calendar | Schedule switch in place). */
.page-tabs button {
  padding: 8px 14px; margin-bottom: -1px; background: none; border: 0;
  border-bottom: 2px solid transparent; font: inherit;
  font-size: 13.5px; font-weight: 600; color: var(--ink-3); cursor: pointer;
}
.page-tabs button:hover { color: var(--ink); }
.page-tabs button.active { color: var(--ink); border-bottom-color: var(--accent); }
.page-tabs button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 4px; }
.cal-tabs { margin: 0 0 14px; }

/* ---------- Schedule: the month as a printed list ---------- */

.sched { display: grid; gap: 12px; }
.sched-row {
  display: grid; grid-template-columns: 170px 150px minmax(0, 1fr);
  border: 1.5px solid var(--ink); border-radius: 12px; overflow: hidden;
  background: var(--surface); cursor: pointer;
  transition: box-shadow 0.12s ease, transform 0.12s ease;
}
.sched-row:hover { box-shadow: 0 4px 14px rgb(20 24 31 / 0.12); }
.sched-row:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
/* Away games shaded, as a printed schedule marks them. */
.sched-row.loc-away .sched-kind,
.sched-row.loc-away .sched-what { background: #e4e6ea; }

.sched-when {
  background: #0d0f13; color: #fff;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; padding: 16px 10px; text-align: center;
}
.sched-dow, .sched-date {
  font-weight: 800; letter-spacing: -0.01em; line-height: 1.15; text-transform: uppercase;
  font-stretch: condensed;
}
.sched-dow { font-size: 17px; }
.sched-date { font-size: 17px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.sched-time {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 6px;
  font-size: 13.5px; font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap;
}
.sched-time .icon { display: block; }

.sched-kind {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; padding: 12px 8px;
  border-left: 1.5px solid var(--ink); border-right: 1.5px solid var(--ink);
}
.sched-loc {
  padding: 2px 10px; border-radius: 999px; font-size: 11.5px; font-weight: 650;
  background: #e3e6ea; color: var(--ink);
}
.sched-loc.is-away { background: #2b3038; color: #fff; }
.sched-loc.is-neutral { background: #fff; color: var(--ink-2); box-shadow: inset 0 0 0 1px #c3cad4; }
.sched-loc.is-none { visibility: hidden; }
.sched-kind-icon { line-height: 0; }
.sched-kind-icon .icon { display: block; }
.sched-row.is-game .sched-kind-icon { color: var(--accent); }
.sched-row.is-scrimmage .sched-kind-icon { color: var(--special); }
.sched-row.is-practice .sched-kind-icon { color: var(--offense); }
.sched-row.is-team_event .sched-kind-icon { color: var(--good); }
/* Colour marks what is still to come; anything already held goes grey,
   whatever kind it was. Declared after the kinds so it wins. */
.sched-row.is-past .sched-kind-icon { color: var(--ink-3); }
.sched-kind-label { font-size: 12px; font-weight: 700; letter-spacing: 0.06em; color: var(--ink); text-align: center; }

.sched-what {
  display: flex; flex-direction: column; justify-content: center; gap: 3px;
  padding: 14px 24px; min-width: 0;
}
.sched-level { font-size: 13px; color: var(--ink-2); }
.sched-name {
  font-size: 21px; font-weight: 800; letter-spacing: -0.01em; line-height: 1.2;
  color: var(--ink); font-stretch: condensed; overflow-wrap: anywhere;
}
.sched-addr {
  display: inline-flex; align-items: center; gap: 6px; align-self: flex-start;
  margin-top: 4px; font-size: 13.5px; color: var(--ink); text-decoration: none; max-width: 100%;
}
.sched-addr span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sched-addr .icon { flex: 0 0 auto; }
.sched-addr:hover span { text-decoration: underline; }
.sched-repeat { font-size: 11.5px; color: var(--ink-3); margin-top: 2px; }

/* Already played: still readable, a step back from what is coming up. */
.sched-row.is-past { opacity: 0.72; }
.sched-row.is-past:hover, .sched-row.is-past:focus-visible { opacity: 1; }

@media (max-width: 720px) {
  .sched-row { grid-template-columns: 1fr; }
  .sched-when { flex-direction: row; flex-wrap: wrap; gap: 4px 10px; padding: 10px 14px; justify-content: flex-start; }
  .sched-time { margin-top: 0; margin-left: auto; }
  .sched-kind { flex-direction: row; justify-content: flex-start; gap: 10px; padding: 8px 14px;
    border-left: 0; border-right: 0; border-bottom: 1.5px solid var(--ink); }
  .sched-loc.is-none { display: none; }
  .sched-what { padding: 12px 14px; }
  .sched-name { font-size: 18px; }
}
@media (prefers-reduced-motion: reduce) {
  .sched-row { transition: none; }
}

/* An entry's repeat pattern in its edit dialog: a summary line until opened. */
.rep-pattern {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 2px 0 14px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 8px; background: var(--surface-2);
}
.rep-pattern.is-open { display: block; background: var(--surface); }
.rep-pattern-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.rep-pattern-label {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink-3); margin-bottom: 2px;
}
.rep-pattern-text { font-size: 13px; color: var(--ink); min-width: 0; }
.rep-pattern .hint { margin-top: 4px; }

/* Notes on a schedule row: the coach's own words, kept line breaks and all. */
.sched-notes {
  margin-top: 6px; padding: 6px 10px; max-width: 70ch;
  border-left: 3px solid var(--line-strong); border-radius: 0;
  font-size: 13px; line-height: 1.45; color: var(--ink-2);
  white-space: pre-line; overflow-wrap: anywhere;
}

/* The final score beside a played fixture's name on the schedule. */
.sched-score { margin-left: 10px; vertical-align: 3px; }
.sched-score:empty { display: none; }

/* ---------- Signing in ---------- */

/* The sign-in screen owns the window: no sidebar, nothing behind it. */
.app.is-signed-out { grid-template-columns: minmax(0, 1fr); }
.app.is-signed-out .sidebar { display: none; }

.si-page { display: flex; justify-content: center; padding: 8vh 20px 40px; }
.si-card {
  width: 100%; max-width: 380px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 14px; box-shadow: var(--shadow); padding: 26px 26px 22px;
}
.si-brand { display: flex; align-items: center; gap: 10px; color: var(--accent); font-weight: 700; font-size: 17px; }
.si-brand span { color: var(--ink); }
.si-card h1 { font-size: 20px; margin: 18px 0 4px; }
.si-sub { margin: 0 0 18px; color: var(--ink-2); font-size: 13px; line-height: 1.5; }
.si-form { display: grid; gap: 12px; }
.si-field { display: grid; gap: 5px; }
.si-field > span {
  font-size: 11px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-3);
}
.si-note { font-size: 12.5px; color: var(--ink-2); line-height: 1.5; }
.si-message { font-size: 13px; line-height: 1.45; padding: 8px 10px; border-radius: 8px; }
.si-message.is-error { color: var(--bad); background: color-mix(in srgb, var(--bad) 8%, #fff); }
.si-go { width: 100%; justify-content: center; margin-top: 2px; }
.si-help { margin: 16px 0 0; font-size: 12px; color: var(--ink-3); line-height: 1.5; }

/* Who is signed in, at the foot of the sidebar. */
.sidebar-who { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
.sidebar-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-who .btn { color: var(--sidebar-ink); border-color: transparent; background: transparent; }
.sidebar-who .btn:hover { background: var(--sidebar-raise); color: #fff; }

/* ---------- Subscriptions ---------- */

/* A page this team's tier does not license: named in the menu, not hidden. */
.nav a.is-locked { opacity: 0.62; }
.nav a.is-locked:hover { opacity: 0.85; }
.nav-lock { margin-left: auto; font-size: 10px; opacity: 0.8; }

/* Where the licence is, at the foot of the sidebar. */
.sidebar-sub {
  display: block; margin-top: 10px; font-size: 11.5px;
  color: var(--sidebar-ink-dim); text-decoration: none;
}
.sidebar-sub:hover { color: #fff; }

/* The season is away: said once, across the top. */
.off-season {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 9px 28px; font-size: 13px;
  background: color-mix(in srgb, var(--warn, #d79a3a) 14%, #fff);
  border-bottom: 1px solid color-mix(in srgb, var(--warn, #d79a3a) 35%, var(--line));
  color: var(--warn-ink, #8a5a00);
}
.off-season-tag {
  font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 2px 8px; border-radius: 999px;
  background: color-mix(in srgb, var(--warn, #d79a3a) 30%, #fff);
}
.off-season a { color: inherit; font-weight: 600; }

/* The subscription page. */
.sub-now {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
.sub-tier { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; }
.sub-seats { text-align: right; font-size: 13px; }
.sub-season { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; padding-top: 14px; }
.sub-season p { margin: 0; font-size: 13px; max-width: 60ch; }

.tiers { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.tier {
  display: flex; flex-direction: column; gap: 8px;
  border: 1px solid var(--line); border-radius: 12px; padding: 16px;
  background: var(--surface);
}
.tier.is-current { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.tier-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.tier-head h3 { margin: 0; font-size: 17px; }
.tier-blurb { margin: 0; font-size: 13px; color: var(--ink-2); }
.tier-teams { font-size: 12px; font-weight: 650; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.05em; }
/* The price, set to read before the feature list rather than under it. */
.tier-price { font-size: 22px; font-weight: 700; color: var(--ink); letter-spacing: -0.01em; }

/*
 * An add-on: what it is on the left, what it costs and how to get it on the
 * right. A row rather than a card, because these are a list of small decisions
 * made one at a time and cards would give each of them the weight of a tier.
 */
.addon {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 13px 0;
  border-bottom: 1px solid var(--line);
}
.addon:last-child { border-bottom: 0; padding-bottom: 0; }
.addon-name { font-weight: 650; font-size: 14px; }
.addon-about { font-size: 13px; color: var(--ink-2); margin-top: 2px; max-width: 58ch; }
.addon-buy { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
.addon-price { font-size: 14px; font-weight: 650; color: var(--ink); white-space: nowrap; }
.tier-price .muted { font-size: 13px; font-weight: 400; }
.tier-offseason { font-size: 12px; margin-top: -4px; }
.tier-list { margin: 0; padding-left: 18px; font-size: 13px; color: var(--ink-2); display: grid; gap: 4px; }
.tier .btn { margin-top: auto; justify-content: center; }

/* Sign-up: choosing a tier is three buttons rather than a dropdown. */
.si-hint { display: block; font-size: 11.5px; color: var(--ink-3); font-weight: 400; text-transform: none; letter-spacing: 0; margin-top: 2px; }
/* One column per tier, however many there are: four since Plus. */
.si-tiers { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 6px; }
.si-tier {
  display: grid; gap: 2px; padding: 8px 6px; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  background: var(--surface); font: inherit; text-align: center;
}
.si-tier:hover { border-color: var(--ink-3); }
.si-tier.is-on { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.si-tier-name { font-size: 13px; font-weight: 650; color: var(--ink); }
.si-tier-teams { font-size: 11px; color: var(--ink-3); }
.si-message.is-ok { color: var(--good); background: color-mix(in srgb, var(--good) 8%, #fff); }

/* The account page. */
.acct-acts { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
.acct-unverified {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  margin: -4px 0 12px; padding: 8px 10px; font-size: 13px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--warn, #d79a3a) 12%, #fff);
  color: var(--warn-ink, #8a5a00);
}
a.sidebar-name { color: inherit; text-decoration: none; }
a.sidebar-name:hover { color: #fff; text-decoration: underline; }

/* Above the formation tiles: how many there are, and one control for all. */
.fm-pick-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 0 0 10px; font-size: 12.5px;
}

/* ---------- A team's crest ---------- */
/*
 * `contain` rather than `cover`: a crest cropped to a square is a crest with
 * its edges cut off, and a badge is mostly edges.
 */
.team-logo-mark {
  width: 26px; height: 26px; flex: 0 0 auto;
  object-fit: contain; border-radius: 4px; background: var(--surface-2);
}
.team-logo-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.team-logo-preview {
  width: 56px; height: 56px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: 6px; background: var(--surface-2);
  overflow: hidden;
}
.team-logo-preview img { width: 100%; height: 100%; object-fit: contain; }
.team-logo-empty { font-size: 10.5px; color: var(--ink-3); }
.team-logo-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.team-logo-actions .hint { flex: 1 1 100%; margin: 0; }

/*
 * A player's picture: round, filled to the edge (`cover` -- a face, unlike a
 * crest, survives losing its corners), and an outline of a person when there
 * is no picture yet.
 */
.player-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto; border-radius: 50%; overflow: hidden;
  background: var(--surface-2); border: 1px solid var(--line);
  vertical-align: middle;
}
.player-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.player-avatar svg { width: 78%; height: 78%; margin-top: 18%; fill: var(--ink-3); opacity: 0.55; }
.avatar-cell { width: 44px; padding-right: 0 !important; }
.avatar-button {
  padding: 0; border: 0; background: none; cursor: pointer; border-radius: 50%; display: inline-flex;
}
.avatar-button:hover .player-avatar { border-color: var(--accent); }
.avatar-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.player-photo-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.player-photo-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; flex: 1; }
.player-photo-actions .hint { flex: 1 1 100%; margin: 0; }

/* ---------- Admin: accounts ---------- */
.acct-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.badge.acct-admin { background: color-mix(in srgb, var(--accent) 12%, #fff); color: var(--accent); }
.badge.acct-locked { background: color-mix(in srgb, var(--warn, #d79a3a) 20%, #fff); color: var(--warn-ink, #8a5a00); }
.acct-grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); margin-bottom: 14px; }
.acct-grid > div { display: grid; gap: 4px; font-size: 13.5px; }
.acct-k { font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); }
.acct-buttons { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

/* ---------- Settings (a person's own) ---------- */
.prefs-page { max-width: 980px; }
.prefs-page .page-head { align-items: center; }
.pref-status { margin-left: auto; font-size: 12px; color: var(--ink-3); min-height: 1em; }
.prefs-page .card + .card { margin-top: 16px; }
.prefs-page .card.is-muted .pref-rows,
.prefs-page .card.is-muted .pref-preview { opacity: 0.55; }

.pref-hero { display: flex; gap: 24px; align-items: flex-start; padding: 20px 22px; }
.pref-hero-say { flex: 1 1 auto; min-width: 0; }
.pref-hero h2 { margin: 0 0 6px; font-size: 16px; letter-spacing: -0.01em; }
.pref-hero p { margin: 0; max-width: 66ch; color: var(--ink-2); font-size: 13px; line-height: 1.5; }
.pref-hero p + p { margin-top: 8px; }
.pref-fine { font-size: 12px !important; color: var(--ink-3) !important; }
.pref-hero-switch { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; padding-top: 2px; }
.pref-state { font-size: 12px; font-weight: 600; color: var(--ink-2); min-width: 22px; }

.pref-split { display: grid; grid-template-columns: minmax(0, 1fr) 210px; gap: 24px; padding: 4px 22px 20px; align-items: start; }
.prefs-page .card > .pref-rows { padding: 4px 22px 16px; }
.pref-rows { display: grid; }
.pref-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 16px; align-items: center;
  padding: 12px 0; border-top: 1px solid var(--line);
}
/* The label and its control share a line; the explanation runs under both. */
.pref-say { display: contents; }
.pref-label { grid-column: 1; grid-row: 1; }
.pref-control { grid-column: 2; grid-row: 1; }
.pref-about { grid-column: 1 / -1; grid-row: 2; }
.pref-row:first-child { border-top: none; }
.pref-label { font-size: 13px; font-weight: 600; color: var(--ink); }
.pref-about { font-size: 12px; color: var(--ink-3); margin-top: 4px; max-width: 64ch; line-height: 1.45; }
.pref-control { display: flex; justify-content: flex-end; }
.pref-seg button { white-space: nowrap; }
.pref-seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.pref-switch { position: relative; display: inline-flex; cursor: pointer; }
.pref-switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.pref-track {
  width: 38px; height: 22px; border-radius: 999px; background: var(--line-strong);
  position: relative; transition: background 0.15s ease;
}
.pref-track::after {
  content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(16, 24, 40, 0.25);
  transition: transform 0.15s ease;
}
/* On is green, not the brand red: a switch should report a state, and red
   reads as a warning next to controls that also use it for danger. */
.pref-switch input:checked + .pref-track { background: var(--good); }
.pref-switch input:checked + .pref-track::after { transform: translateX(16px); }
.pref-switch input:focus-visible + .pref-track { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .pref-track, .pref-track::after { transition: none; }
}

/* The viewer in miniature. */
.pref-preview {
  border: 1px solid var(--line-strong); border-radius: 8px; overflow: hidden;
  background: var(--surface-2); margin-top: 12px;
}
.pv-bar { display: flex; gap: 4px; padding: 6px 8px; border-bottom: 1px solid var(--line); background: var(--surface); }
.pv-bar b { width: 6px; height: 6px; border-radius: 50%; background: var(--line-strong); }
.pv-body { display: flex; gap: 6px; padding: 8px; height: 132px; }
.pv-film, .pv-data { flex-basis: 0; min-width: 0; border-radius: 4px; position: relative; display: flex; flex-direction: column; }
.pv-film { background: #1d232c; color: #c9d1d9; align-items: center; justify-content: center; gap: 4px; font-size: 10.5px; }
.pv-play { color: var(--accent); font-size: 18px; line-height: 1; }
.pv-data { background: var(--surface); border: 1px solid var(--line); padding: 8px; gap: 6px; }
.pv-data i { display: block; height: 6px; border-radius: 3px; background: var(--line); }
.pv-data i.short { width: 60%; }
.pv-body.is-compact .pv-data { gap: 3px; }
.pv-body.is-compact .pv-data i { height: 4px; }
.pv-data span { margin-top: auto; font-size: 10.5px; color: var(--ink-3); }

.film-viewer-open { margin-left: 2px; }

@media (max-width: 760px) {
  .pref-split { grid-template-columns: 1fr; }
  .pref-row { grid-template-columns: 1fr; row-gap: 8px; }
  .pref-control { grid-column: 1; grid-row: 2; justify-content: flex-start; }
  .pref-about { grid-row: 3; }
  .pref-hero { flex-direction: column-reverse; gap: 12px; }
}

/* ---------- One scroll: the page fits the window, its list scrolls ---------- */
/*
 * A page marked .is-fill does not scroll as a page. It is exactly the height
 * of the window; the heading, the filters and whatever sits above the list
 * stay where they are; and the one element marked .fill-scroll -- the roster,
 * the film's plays, the schedule -- takes the height that is left and scrolls
 * inside it. One scrollbar, where the rows are, with the column headings
 * pinned above them.
 *
 * Everything between the page and that list is stretched to pass the height
 * down (the :has rules), so a page opts in with two class names and no
 * wrapper of its own. When the top part alone is taller than the window --
 * a short laptop screen, the game's details expanded -- the list keeps a
 * usable minimum and the page scrolls after all, rather than squeezing the
 * rows to nothing. A short list stays its own height rather than stretching
 * a card of empty space down to the bottom of the window.
 *
 * Wide screens only: on a phone the sidebar sits above the page, and one long
 * page is what a phone does best.
 */
@media (min-width: 861px) {
  html.fill-page { scrollbar-gutter: auto; overflow: hidden; }
  html.fill-page body { height: 100vh; overflow: hidden; }
  html.fill-page .app { height: calc(100vh - var(--demo-bar-h)); min-height: 0; }
  html.fill-page .main { height: calc(100vh - var(--demo-bar-h)); overflow-y: auto; }

  .page.is-fill {
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column;
    padding-bottom: 20px;
  }
  /* Grows no taller than its content; shrinks to the window when longer. */
  .page.is-fill :has(.fill-scroll) {
    display: flex; flex-direction: column;
    flex: 0 1 auto; min-height: 0;
  }
  /* Whatever is beside the list's column keeps its own height. */
  .page.is-fill > :not(:has(.fill-scroll)):not(.fill-scroll),
  .page.is-fill :has(.fill-scroll) > :not(:has(.fill-scroll)):not(.fill-scroll) {
    flex-shrink: 0;
  }
  .page.is-fill .fill-scroll {
    flex: 0 1 auto;
    min-height: 180px;
    overflow: auto;
  }
  /*
   * What is in the list keeps its size and the list scrolls. A list that is
   * itself a flex column -- the schedule -- would otherwise squeeze every row
   * to fit the box rather than scroll it.
   */
  .page.is-fill .fill-scroll > * { flex-shrink: 0; }
  /* The same for a list laid out as a grid: each row its full height, from the top. */
  .page.is-fill .fill-scroll { grid-auto-rows: max-content; align-content: start; }
}

/* ---------- Uploading a film ---------- */
.film-drop {
  display: grid; gap: 3px; padding: 13px 16px; cursor: pointer; text-align: center;
  border: 1.5px dashed var(--line-strong); border-radius: var(--radius); background: var(--surface-2);
}

/*
 * The same control once it has a film: a line rather than a target.
 *
 * It is still the file input -- the whole row is the label, so clicking
 * anywhere on it opens the picker -- but it has stopped asking. What it does
 * now is state which film this is and offer to change it, and that is a
 * sentence, not a drop zone.
 */
.film-chosen {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 8px 12px;
  cursor: pointer;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.film-chosen:hover { border-color: var(--line-strong); }
.film-chosen:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
.film-chosen input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.film-chosen-name {
  flex: 1;
  min-width: 0;
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.film-chosen-swap { flex: 0 0 auto; font-size: 12px; color: var(--accent); }
.film-drop:hover { border-color: var(--ink-3); }
.film-drop:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
.film-drop input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.film-drop-say { font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
.film-drop-meta { font-size: 12px; color: var(--ink-3); }
.upload-progress { display: grid; gap: 6px; margin-top: 14px; font-size: 12px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.upload-bar { height: 8px; border-radius: 4px; background: var(--line); overflow: hidden; }
.upload-bar i { display: block; height: 100%; background: var(--accent); transition: width 0.2s ease; }
@media (prefers-reduced-motion: reduce) { .upload-bar i { transition: none; } }

/* ---------- Statistics: head to head beside the panels that explain it ---------- */
.h2h-layout {
  display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: 18px; align-items: stretch;
}
/*
 * Both columns end on the same line: each is as tall as the taller one, and
 * the last panel in the shorter one grows to meet it.
 */
.h2h-main, .h2h-side { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
.h2h-main > :last-child, .h2h-side > :last-child { flex: 1 1 auto; }
.h2h-main > :last-child.card, .h2h-side > :last-child > .card { height: 100%; box-sizing: border-box; }
/* Two panels that fit side by side in the right column, and stack when not. */
.h2h-pair { gap: 18px; }
/* The comparison, tightened to its column: shorter rows, a narrower label. */
.h2h-main .compare-row {
  grid-template-columns: 58px minmax(0, 1fr) 112px minmax(0, 1fr) 58px;
  gap: 8px; padding: 5px 0; font-size: 12.5px;
}
.h2h-main .compare-row .label { font-size: 10.5px; }
@media (max-width: 1180px) {
  .h2h-layout { grid-template-columns: minmax(0, 1fr); }
}
@media print {
  .h2h-layout { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- Auditing the analyser (Drilldown admins only) ---------- */
/*
 * A toggle that is currently on, inverted so it reads as pressed.
 *
 * Two entry points to one look. `.btn.active` on its own was never styled,
 * which nobody noticed while every toggle carried a word -- the label stayed
 * readable whether or not the button changed. An icon has no such fallback:
 * without this, an icon-only toggle looks identical on and off.
 */
.audit-toggle.active,
.btn.is-on { background: var(--ink); color: #fff; border-color: var(--ink); }
.audit-bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 14px; border-bottom: 1px solid var(--line); background: var(--surface-2);
}
.audit-tag {
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: #fff; background: var(--ink); border-radius: 999px; padding: 2px 9px;
}
.audit-progress { font-size: 12.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.audit-acts { margin-left: auto; display: flex; gap: 6px; }
table.plays td.audit-cell { white-space: nowrap; }
.audit-mark { padding: 2px 8px; font-size: 13px; line-height: 1.2; }
.audit-mark.is-pass { background: color-mix(in srgb, var(--good) 14%, #fff); color: var(--good); border-color: color-mix(in srgb, var(--good) 40%, #fff); }
.audit-mark.is-fail { background: color-mix(in srgb, var(--bad) 10%, #fff); color: var(--bad); border-color: color-mix(in srgb, var(--bad) 35%, #fff); }
.audit-note { padding: 2px 6px; color: var(--ink-3); }
.audit-note.has-note { color: #7a5a00; background: #fdf0b3; }

.audit-headline { display: grid; gap: 10px; margin-bottom: 4px; }
.audit-score { display: flex; align-items: center; gap: 14px; }
.audit-pct { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; }
.audit-grade {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 26px; padding: 1px 8px; border-radius: 999px;
  font-weight: 700; font-size: 12.5px; background: var(--surface-2);
  border: 1px solid var(--line-strong); color: var(--ink-2);
}
/* Wide enough for a plus or a minus without losing the square block a bare
   letter makes: the width is a floor, and the padding lets "B+" grow past it. */
.audit-grade.is-big {
  min-width: 52px; height: 52px; padding: 0 10px;
  border-radius: 12px; font-size: 26px; letter-spacing: -0.5px;
}
.audit-grade.is-good { color: var(--good); background: color-mix(in srgb, var(--good) 10%, #fff); border-color: color-mix(in srgb, var(--good) 30%, #fff); }
.audit-grade.is-warn { color: #8a5a00; background: color-mix(in srgb, #b7791f 12%, #fff); border-color: color-mix(in srgb, #b7791f 30%, #fff); }
.audit-grade.is-bad { color: var(--bad); background: color-mix(in srgb, var(--bad) 9%, #fff); border-color: color-mix(in srgb, var(--bad) 30%, #fff); }

/* How sure the analyser was, read against the verdict beside it. */
table.plays td.conf-cell { font-variant-numeric: tabular-nums; font-weight: 600; }
table.plays td.conf-cell.is-high { color: var(--good); }
table.plays td.conf-cell.is-mid { color: #8a5a00; }
table.plays td.conf-cell.is-low { color: var(--bad); }

/* The grid's mark: a filled diamond once every field has been judged, a hollow
   one while there is still something to do, and the count of fields beside it. */
.conf-mark { display: inline-flex; align-items: center; gap: 3px; font-size: 13px; }
.conf-mark-n { font-size: 11px; opacity: 0.75; }

/* ---------- Game diff ---------- */
/* One table for both versions, so the same play sits on the same line. The
   tint says what kind of row it is; the word in the middle says it in text. */
.diff-controls { display: grid; grid-template-columns: 1fr 1fr auto auto; gap: 14px; align-items: end; padding: 16px 18px 6px; }
.diff-pick .pref-row-name { margin-bottom: 6px; }
.diff-figures { grid-template-columns: repeat(6, 1fr); }
/* A play the auditor marked as missed by the analyser: the verdict this tool
   exists to surface, so it reads in the same red as a missing row. */
.badge.missed { background: color-mix(in srgb, var(--bad) 12%, #fff); color: var(--bad); border-color: color-mix(in srgb, var(--bad) 30%, #fff); }
.diff-figures .audit-figure.is-missing .audit-figure-value { color: var(--bad); }
.diff-figures .audit-figure.is-extra .audit-figure-value { color: #8a5a00; }
/* Where the page does not fill the window, the table is its own scroll box.
   Where it does (see .page.is-fill), the fill layout sizes it instead. */
html:not(.fill-page) .diff-wrap { max-height: 70vh; overflow: auto; }
/* Two header rows, both pinned: the game names at the very top, the column
   names just under them. A second row pinned at the same top would slide
   under the first, so it is offset by the first row's fixed height. */
table.diff th { position: sticky; background: var(--surface); z-index: 1; }
table.diff thead tr:first-child th { top: 0; height: 32px; text-align: center; border-bottom: 0; }
table.diff thead tr:last-child th { top: 32px; box-shadow: 0 1px 0 var(--line); }
table.diff a.diff-time { color: var(--accent); text-decoration: none; border-bottom: 1px dotted currentColor; }
table.diff a.diff-time:hover { border-bottom-style: solid; }
table.diff td.diff-status { text-align: center; font-size: 11px; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase; color: var(--ink-3); white-space: nowrap; }
table.diff tr.is-missing td { background: color-mix(in srgb, var(--bad) 10%, #fff); }
table.diff tr.is-missing td.diff-status { color: var(--bad); }
table.diff tr.is-extra td { background: color-mix(in srgb, #b7791f 10%, #fff); }
table.diff tr.is-extra td.diff-status { color: #8a5a00; }
table.diff tr.is-timing td.diff-status { color: #8a5a00; }
/*
 * Late: the one timing fault that costs the coach the play.
 *
 * Louder than the amber a plain offset gets, and carried on the offending
 * stamp rather than as a row wash -- a run that drifts late is late nearly
 * everywhere, and a table washed red row after row says no more than a table
 * washed nothing. The number is where the fault is, so the number is what is
 * marked; the word in the middle column names it.
 */
table.diff tr.is-late td.diff-status { color: var(--bad); }
table.diff td.mono.is-late-time {
  background: color-mix(in srgb, var(--bad) 13%, #fff);
  font-weight: 650;
  box-shadow: inset 2px 0 0 var(--bad);
}
table.diff td.mono.is-late-time,
table.diff td.mono.is-late-time .diff-time { color: var(--bad); }
.diff-figures .audit-figure.is-late .audit-figure-value { color: var(--bad); }
@media (max-width: 900px) { .diff-controls { grid-template-columns: 1fr 1fr; } .diff-figures { grid-template-columns: repeat(3, 1fr); } }

/* The breakdown inside a play. Its own panel so it reads as a separate job from
   charting the play, which is what it is. */
.audit-breakdown { margin-top: 12px; }
.audit-breakdown span.conf-cell { font-weight: 600; font-variant-numeric: tabular-nums; }
.audit-breakdown span.conf-cell.is-high { color: var(--good); }
.audit-breakdown span.conf-cell.is-mid { color: #8a5a00; }
.audit-breakdown span.conf-cell.is-low { color: var(--bad); }
.audit-breakdown .btn.is-pass { background: color-mix(in srgb, var(--good) 14%, #fff); color: var(--good); border-color: color-mix(in srgb, var(--good) 35%, #fff); }
.audit-breakdown .btn.is-fail { background: color-mix(in srgb, var(--bad) 12%, #fff); color: var(--bad); border-color: color-mix(in srgb, var(--bad) 35%, #fff); }

/* Marking a play the analyser never found, and the report's three figures. */
.audit-missed.is-missed { background: color-mix(in srgb, var(--bad) 10%, #fff); color: var(--bad); border-color: color-mix(in srgb, var(--bad) 35%, #fff); }
.audit-figures { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 4px 0 14px; }
.audit-figure { background: var(--surface-2); border-radius: var(--radius); padding: 10px 12px; }
.audit-figure-value { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
.audit-figure-label { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-3); }
.audit-figure-about { font-size: 11.5px; color: var(--ink-3); margin-top: 3px; line-height: 1.4; }

/* ---------- The audit report, on paper ---------- */
/*
 * Printing the dialog rather than the page behind it: the application, the
 * dimmed backdrop and the dialog's own buttons all go, and the report itself
 * runs down the sheet at full width instead of scrolling inside a box.
 */
@media print {
  /*
   * The dialog lives in #modal-root, which the print stylesheet above hides
   * along with the rest of the application -- so printing the report without
   * this gave a blank sheet.
   */
  body.printing-audit #modal-root { display: block !important; }

  body.printing-audit .app,
  body.printing-audit #toast-root,
  body.printing-audit .modal-head,
  body.printing-audit .modal-foot { display: none !important; }

  body.printing-audit .modal-backdrop {
    position: static; display: block; background: none; padding: 0;
  }
  body.printing-audit .modal {
    width: 100% !important; max-width: none; max-height: none;
    box-shadow: none; border: 0; border-radius: 0;
  }
  body.printing-audit .modal-body {
    overflow: visible; max-height: none; padding: 0;
  }
  /* Figures and tables read as blocks; none of them should break in half. */
  body.printing-audit .audit-figure,
  body.printing-audit .table-wrap { break-inside: avoid; }
  body.printing-audit .audit-figures { grid-template-columns: repeat(3, 1fr); }
}

/*
 * Film timestamps are typed, never stepped.
 *
 * The fields are plain text so no browser should offer a stepper on them, but
 * saying so costs two rules and covers the ones that do it anyway -- a spinner
 * on a time is a click that lands on the wrong digit and a scroll wheel that
 * quietly changes a play a coach was only reading.
 */
.time-input { appearance: textfield; -moz-appearance: textfield; }
.time-input::-webkit-outer-spin-button,
.time-input::-webkit-inner-spin-button { -webkit-appearance: none; appearance: none; margin: 0; }

/*
 * The pulse a dialog gives when somebody clicks beside it.
 *
 * Short, and only a shadow and a hair of scale -- enough to say the click
 * landed and the dialog is still there on purpose, without looking like an
 * error. Anyone who has asked for less motion gets none of it; the dialog
 * simply stays, which was the point.
 */
@keyframes dd-nudge {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.012); }
  100% { transform: scale(1); }
}
.modal.is-nudged,
.drawer.is-nudged {
  animation: dd-nudge 320ms ease-out;
  box-shadow: 0 0 0 2px var(--accent), 0 18px 48px rgba(0, 0, 0, 0.32);
}
@media (prefers-reduced-motion: reduce) {
  .modal.is-nudged,
  .drawer.is-nudged { animation: none; }
}

/*
 * The audit controls inside a diff row.
 *
 * Tight, because the row already carries two plays' worth of columns and the
 * marks have to fit beside them without pushing the times out of view. The
 * "missed" mark keeps its word rather than a tick: it says something different
 * from the other two -- that nothing was written here at all -- and a symbol
 * would read as a third opinion on a play that exists.
 */
/*
 * The down, while the down is what is being judged.
 *
 * The value keeps the column's alignment and the marks sit after it, so the
 * eye still reads a column of downs against the chart's column of downs and
 * the buttons are where the answer already is. Failing swaps the number for a
 * box in the same place, because the correction belongs where the wrong value
 * was rather than in a dialog somewhere else.
 */
/*
 * The unit, while the unit is what is being judged.
 *
 * The badge keeps its place and the marks sit after it, the same arrangement
 * as the down: the eye reads a column of units against the chart's column of
 * units, and the buttons are where the answer already is. Failing swaps the
 * badge for a picker rather than a text box, because there are only three
 * answers and one of them is "nobody can tell".
 */
.unit-audit { white-space: nowrap; }
.unit-audit .audit-mark { padding: 1px 5px; font-size: 12px; line-height: 1.3; margin-left: 3px; }
.unit-audit .unit-fix {
  /* max-width for the same reason the down's box uses it: the blanket rule
     that makes every input fill its container outranks a plain width here. */
  max-width: 116px;
  padding: 1px 4px;
  font-size: 11.5px;
  border-color: var(--accent);
}

.down-audit { white-space: nowrap; }
.down-audit .audit-mark,
.down-audit .audit-note { padding: 1px 5px; font-size: 12px; line-height: 1.3; margin-left: 3px; }
/*
 * The reason behind a failed down, on a button that appears only once there
 * is a verdict to explain. Coloured like every other note on the screen, so
 * an answered "why" reads the same here as it does in the audit column.
 */
.down-audit .down-why { padding: 1px 4px; }
.down-reason-hint { margin: 10px 0 0; }
.down-audit .down-said { display: inline-block; min-width: 10px; }
.down-audit .down-fix {
  /*
   * max-width, not width: the form rule that sets every input to fill its
   * container carries five :not() clauses and so outranks anything sensible
   * written here. A ceiling is not in that fight -- it holds the box to two
   * digits whatever the width underneath it says.
   */
  max-width: 42px;
  padding: 1px 4px;
  font-size: 12px;
  text-align: center;
  border-color: var(--accent);
}
.diff-audit-pick { align-self: end; }

.diff-audit { white-space: nowrap; }
.diff-audit .btn { padding: 1px 6px; font-size: 12px; line-height: 1.3; }
.diff-audit .audit-mark + .audit-mark,
.diff-audit .audit-note,
.diff-audit .diff-delete { margin-left: 3px; }

/*
 * Throwing out a detection, from the column where the detections are.
 *
 * Quiet until it is reached for: it sits beside two marks that get clicked all
 * the way down a game, and a red button among them would pull the eye to the
 * one action nobody should perform by reflex.
 */
.diff-audit .diff-delete { color: var(--ink-3); }
.diff-audit .diff-delete:hover:not(:disabled) {
  color: #fff;
  background: var(--accent);
  border-color: var(--accent);
}
.diff-audit .audit-missed,
.diff-audit .audit-mark.is-missed {
  background: color-mix(in srgb, var(--bad) 10%, #fff);
  color: var(--bad);
  border-color: color-mix(in srgb, var(--bad) 35%, #fff);
}

/*
 * The copy-across button in a diff row's middle column.
 *
 * Under the word rather than beside it: "Missing" is what the reader is
 * scanning for and the button is what they do about it, so the word keeps the
 * column's rhythm and the button sits out of the way until the eye stops on
 * that row.
 */
.diff-status .diff-add {
  display: block;
  margin: 3px auto 0;
  padding: 1px 6px;
  font-size: 11px;
  white-space: nowrap;
}
tr:not(:hover) .diff-status .diff-add { opacity: 0.55; }
.diff-status .diff-add:hover { opacity: 1; }

/*
 * The two wholesale judgements, under the diff's controls.
 *
 * On their own line rather than in the row of selectors: they act on every row
 * at once, and a control that does that should not sit shoulder to shoulder
 * with the ones that only change what is being looked at.
 */
.diff-bulk {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line);
}
.diff-bulk .hint { margin: 0; }

/*
 * The remove control at the end of an audited row.
 *
 * Faint until the row is under the pointer. It is the one destructive thing in
 * a grid whose rows are otherwise all clicked to open a play, so it should be
 * findable when wanted and invisible when not -- a column of crosses down the
 * edge of the breakdown would read as a warning about the plays themselves.
 */
.delete-cell { width: 1%; text-align: right; padding-right: 8px; }
.row-delete {
  padding: 1px 6px;
  color: var(--ink-3);
  opacity: 0;
  transition: opacity 120ms ease-out, color 120ms ease-out;
}
tr:hover .row-delete,
.row-delete:focus-visible { opacity: 1; }
.row-delete:hover { color: var(--bad); background: color-mix(in srgb, var(--bad) 8%, #fff); }
@media (hover: none) {
  /* Nothing to hover with: show it rather than hide it from touch entirely. */
  .row-delete { opacity: 0.6; }
}

/* ---------- A drawn play on game film (components/film-links.js) ---------- */
.fl-panel { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--line); }
.fl-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
/* The one message, with the same chrome as a filled table would have. */
.fl-empty {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 28px 16px; text-align: center; color: var(--ink-3);
  border: 1px dashed var(--line); border-radius: 10px; background: var(--surface);
}
.fl-empty p { margin: 0; max-width: 420px; font-size: 13px; line-height: 1.5; }

/* The Playbook page's two tabs: as wide as their words, with air before the filter bar. */
.pb-tabs { align-self: flex-start; margin-bottom: 12px; }

/* The play editor's picker: the playbook's cards, each one a button. */
.pb-grid.pb-pick { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); max-height: 62vh; overflow-y: auto; padding: 2px; }
.pb-pick-card { text-align: left; font: inherit; color: inherit; padding: 0; cursor: pointer; }
.pb-pick-card:hover, .pb-pick-card:focus-visible { border-color: var(--accent); outline: 2px solid var(--accent); outline-offset: -1px; }
.pb-pick-card .pb-head { padding-bottom: 10px; }

/* The fouls on a play in the editor: one chip each, with its own remove. */
.penalty-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
.penalty-chip { display: inline-flex; align-items: center; gap: 6px; }
.penalty-chip button { border: 0; background: none; color: inherit; font: inherit; line-height: 1; padding: 0 2px; cursor: pointer; opacity: 0.7; }
.penalty-chip button:hover { opacity: 1; }

/* The analyser's doubt about a row, beside its number. */
table.plays td.seq .seq-doubt {
  display: inline-block; margin-left: 4px; min-width: 14px; text-align: center;
  font-size: 11px; font-weight: 700; line-height: 1.3; border-radius: 7px;
  background: #fdf0b3; color: #7a5a00; box-shadow: inset 0 0 0 1px #e0b100;
}

/* A drawing on the play, made in the Film Viewer: beside the number, after the star. */
table.plays td.seq .seq-draw {
  display: inline-block; margin-left: 4px; font-size: 13px; line-height: 1; color: var(--accent); cursor: pointer;
}
table.plays td.seq .seq-draw:hover { text-decoration: underline; }

/* Whether a play is on a playlist: an empty circle, or a green tick. Click to change. */
.pl-mark {
  width: 24px; height: 24px; border-radius: 50%; padding: 0; cursor: pointer; line-height: 1;
  border: 2px solid var(--ink-3, #888); background: transparent; color: var(--ink-3, #888);
  display: inline-flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 700;
}
.pl-mark:hover { border-color: var(--ink); color: var(--ink); }
/* Softer than the star: a tinted ring rather than a solid fill, matching the star's own treatment. */
.pl-mark.is-on { border-color: #4caf7d; color: #2e8b5e; background: color-mix(in srgb, #4caf7d 16%, #fff); }
.pl-mark.is-on:hover { filter: brightness(1.08); }

/* Whether a play is a highlight: the star in a circle, gold when it is. Beside the playlist mark. */
.hl-mark {
  width: 24px; height: 24px; border-radius: 50%; padding: 0; cursor: pointer; line-height: 1;
  border: 2px solid var(--ink-3, #888); background: transparent; color: var(--ink-3, #888);
  display: inline-flex; align-items: center; justify-content: center; font-size: 14px;
}
.hl-mark .star-glyph { font-size: 14px; line-height: 1; }
.hl-mark:hover { border-color: var(--highlight); color: var(--highlight); }
.hl-mark.is-on { border-color: var(--highlight); color: var(--highlight); background: color-mix(in srgb, var(--highlight) 14%, #fff); }

/* The star state of the mark: gold, as the highlight has always been. */
.pl-mark.is-star { border-color: var(--highlight); color: var(--highlight); background: color-mix(in srgb, var(--highlight) 14%, #fff); }
.pl-mark.is-static { cursor: default; }
.pl-mark:disabled { opacity: 1; }
table.plays td.seq .seq-star { width: auto; margin-left: 6px; vertical-align: middle; }
table.plays td.seq .pl-mark { width: 20px; height: 20px; font-size: 12px; border-width: 1.5px; }

/* ---------- Recruiting (the editor; the shared page has recruit.css) ---------- */

/* Every track may shrink below its content: a table scrolls in its own wrap
 * rather than pushing its card under the next one. */
.recruit-layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: 18px; }
.recruit-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: 18px; align-items: start; }
.recruit-layout > *, .recruit-row > * { min-width: 0; }
.recruit-layout .card + .card, .recruit-row .card + .card { margin-top: 0; }
.share-row { display: flex; gap: 6px; align-items: center; }
.share-url { flex: 1 1 auto; min-width: 0; font-family: var(--mono); font-size: 12px; }
label.check { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.pick-game + .pick-game { margin-top: 14px; }
.pick-game-head { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.pick-game tr { cursor: pointer; }
.pick-game tr.is-chosen td { background: color-mix(in srgb, var(--accent) 7%, transparent); }

/* ---------- The Roster page's tabs (views/roster-page.js) ---------- */

/* The tabs on the left, the open tab's buttons on the right, one row. */
.roster-tab-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 14px;
}
.roster-tab-row .roster-tabs { margin-bottom: 0; }
.roster-tab-actions { margin-left: auto; display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.roster-tab-actions:empty { display: none; }
/* Swapping tabs dims what is going rather than blanking the page. */
.roster-panel { transition: opacity 120ms ease; }
.roster-panel.is-loading { opacity: 0.45; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .roster-panel { transition: none; } }
/* A tab's one line about itself, where a page's subtitle would have been. */
.tab-intro { margin: 0 0 12px; color: var(--ink-2); font-size: 13px; }
/* A record opened under a tab -- a player's recruiting profile. */
.tab-subhead { margin-bottom: 14px; }
.tab-subhead h2 { margin: 0; font-size: 20px; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.crumb .crumb-sep { color: var(--ink-3); }

/* ---------- A heading that folds what is under it (foldToggle, util.js) ---------- */
.fold-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; cursor: pointer; text-align: left;
}
.fold-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.fold-caret { width: 12px; color: var(--ink-3); font-size: 12px; }
/* The title inside a fold toggle keeps the look of the heading it replaces. */
.fold-toggle h2, .fold-toggle h3 { margin: 0; }
.fold-toggle .section-title { margin: 0; }

/* ---------- The demo's strip across the top (app.js, showDemoBar) ---------- */
.demo-bar {
  position: sticky; top: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap;
  padding: 8px 16px; background: #1b2230; color: #eef1f5; font-size: 14px;
  border-bottom: 3px solid var(--accent);
}
.demo-bar strong { color: #fff; }
.demo-bar .btn { background: #fff; color: #1b2230; border-color: #fff; }
@media print { .demo-bar { display: none; } }

/* The depth chart's special teams units sit under one heading of their own. */
.dc-group {
  margin: 28px 0 0; font-size: 13px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-2);
}
/* A shut section still prints: paper has no way to open it. */
@media print {
  .dc-fold .card-body[hidden] { display: block !important; }
  .dc-fold .fold-caret, .dc-fold .fold-summary { display: none; }
}
