/* Tokens are lifted verbatim from usl2w so the two sit in one visual family, and
   they match the matplotlib deck theme used across the football viz work. */
:root {
  --bg:       #0d1117;
  --surface:  #161b22;
  --surface2: #1c2128;
  --border:   #30363d;
  --text:     #e6edf3;
  --muted:    #8b949e;
  --accent:   #58a6ff;
  --green:    #3fb950;
  --amber:    #d29922;
  --red:      #f85149;
  --purple:   #bc8cff;
  --pink:     #f778ba;
  --radius:   8px;
  --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 15px; }
body { background: var(--bg); color: var(--text); font-family: var(--font); min-height: 100vh; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* Nav */
.navbar {
  position: sticky; top: 0; z-index: 100;
  display: flex; align-items: center; gap: 20px;
  padding: 0 24px; height: 52px;
  background: var(--surface); border-bottom: 1px solid var(--border);
}
.nav-brand { display: flex; align-items: center; gap: 7px; white-space: nowrap; }
.brand-main {
  display: flex; align-items: center; gap: 9px;
  font-size: 1rem; font-weight: 700; letter-spacing: 1.2px; color: var(--text);
}
.brand-main:hover { text-decoration: none; }
/*The mark is drawn on its own black field, so it is set as a tile rather than knocked
  out onto the navbar, and the border keeps that field from reading as a hole in it.*/
.brand-logo {
  width: 26px; height: 26px; display: block; border-radius: 6px;
  border: 1px solid var(--border);
}

/* Division is a MODE, styled as a segmented control rather than a dropdown, because
   it re-scopes the page. A dropdown implies it is one filter among many and invites
   an "all divisions" option, which would be an invalid table. It lives in the page
   control bar rather than the navbar, since it only means anything on the two pages
   whose tables it scopes. */
.divmode { display: flex; gap: 2px; background: var(--surface2);
           border: 1px solid var(--border); border-radius: var(--radius); padding: 2px; }
.divmode a {
  font-size: 0.78rem; font-weight: 600; letter-spacing: 0.4px;
  padding: 4px 11px; border-radius: 5px; color: var(--muted); text-decoration: none;
}
.divmode a:hover { color: var(--text); }
.divmode a.on { background: var(--accent); color: #06121f; }

.nav-tabs { display: flex; gap: 2px; }
.nav-tab {
  background: none; border: none; color: var(--muted); font: inherit;
  font-size: 0.85rem; padding: 6px 12px; border-radius: var(--radius);
  cursor: pointer; text-decoration: none; display: inline-block;
}
.nav-tab:hover { color: var(--text); background: var(--surface2); text-decoration: none; }
.nav-tab.active { color: var(--text); background: var(--surface2); }
/* Global search lives in the nav, so it takes the leftover width and pushes the page
   meta to the far right. It shrinks before it wraps, because the navbar is a fixed
   52px and a wrapped control would break the whole bar. */
.nav-search { margin-left: auto; flex: 0 1 340px; min-width: 150px; }
.nav-search .input { width: 100%; padding: 6px 11px; }

/* The build stamp is last in the bar because it is the only thing in it nobody is
   looking for, and it is the only thing in it that never changes. It is set quiet on
   purpose. It has to be legible when someone asks how old this is and invisible the
   rest of the time. */
.nav-asof {
  white-space: nowrap; flex: none; font-size: 0.75rem; color: var(--muted);
  border-left: 1px solid var(--border); padding-left: 14px; cursor: default;
}
.nav-asof .k {
  text-transform: uppercase; letter-spacing: 0.5px; font-size: 0.66rem; opacity: 0.7;
}

.main { max-width: 1240px; margin: 0 auto; padding: 22px 24px 60px; }

/* Head */
.pagehead { display: flex; align-items: flex-end; gap: 14px; margin-bottom: 4px; flex-wrap: wrap; }
h1 { font-size: 1.32rem; font-weight: 650; letter-spacing: -0.2px; }
/* A section heading needs room above it or it reads as a caption on the table it
   follows rather than a title for the one below. "Squad" was sitting right on the
   bottom border of the season-by-season table. */
h2 { font-size: 0.98rem; font-weight: 600; margin: 30px 0 10px; }
h2:first-of-type { margin-top: 22px; }
.sub { color: var(--muted); font-size: 0.82rem; }
/* How many rows the controls above just produced. It used to sit in the navbar, which
   put page state in the one bar that is identical on every page. */
.pagehead .count {
  margin-left: auto; color: var(--muted); font-size: 0.78rem;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.note {
  color: var(--muted); font-size: 0.79rem; line-height: 1.5;
  border-left: 2px solid var(--border); padding: 2px 0 2px 11px; margin: 12px 0 18px;
  max-width: 78ch;
}
.note strong { color: var(--text); font-weight: 600; }
/* Amber for the same reason the flag is amber. It means read this differently, not
   something is broken, and the numbers under it are correct. */
.note.warn { border-left-color: var(--amber); }

/* Controls */
.bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 16px 0 12px; }
/* The scope bar LEADS the page rather than following the heading, so it drops the top
   margin it needed when something sat above it. The rule under it is what stops the
   heading below from reading as the last item in the bar. */
.bar.controls {
  margin: 0 0 16px; padding-bottom: 14px; border-bottom: 1px solid var(--border);
}
/* SENDS AN ITEM TO THE FAR END OF ITS ROW, and it is a class rather than an inline
   style because it is only true while the bar is one row. It used to be written inline
   on the flagged toggle, where it kept meaning something after the bar wrapped and
   quietly meant something else: the switches became their own line and the auto margin
   pushed that whole line hard right, under three lines of controls sitting flush left.
   Nothing was broken and the page just looked wrong, which is the harder kind to spot.
   An auto margin is a statement about a row, so it is dropped wherever rows wrap. */
.push { margin-left: auto; }
/* Keeps "per page" welded to the sizes it names, so a wrapping bar breaks around the
   pair rather than between them. */
.perpage { display: flex; align-items: center; gap: 8px; }
.input, .select {
  background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); font: inherit; font-size: 0.84rem; padding: 7px 11px; outline: none;
}
.input:focus, .select:focus { border-color: var(--accent); }
.input::placeholder { color: var(--muted); }
.btn {
  background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--muted); font: inherit; font-size: 0.8rem; padding: 7px 12px;
  cursor: pointer; text-decoration: none; display: inline-block;
  transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
.btn:hover { color: var(--text); border-color: var(--muted); text-decoration: none; }
.btn.on { border-color: var(--accent); color: var(--text); }
/* Divides the controls that apply on change from the one pair that needs the button.
   Without it Apply reads as though the dropdowns to its left were waiting on it too. */
.sep { width: 1px; height: 22px; background: var(--muted); opacity: 0.4;
       margin: 0 4px; flex: none; }

/* Page size, styled as a segmented control because it is one choice among a few
   rather than a free number. */
.seg { display: inline-flex; gap: 2px; background: var(--surface2);
       border: 1px solid var(--border); border-radius: var(--radius); padding: 2px; }
.seg a {
  font-size: 0.78rem; padding: 4px 10px; border-radius: 5px; color: var(--muted);
  text-decoration: none; transition: color 0.12s ease, background 0.12s ease;
}
.seg a:hover { color: var(--text); text-decoration: none; }
.seg a.on { background: var(--accent); color: #06121f; }
/* A disabled step and the gap marker keep their space so the control does not shuffle
   sideways as you move between pages. */
.seg .off { font-size: 0.78rem; padding: 4px 10px; color: var(--border); }
.pager { margin-top: 14px; }
.pages a, .pages .off { min-width: 22px; text-align: center; }

/* THE LADDER, drawn on one axis on purpose. Three numbers would be more machinery than
   content as a table, and the claim being made is that the divisions sit at different
   heights on a SINGLE scale, which is a picture rather than a list. Bar length is the
   conversion factor itself, so D1 fills the track by definition and the others are
   literally the fraction of a D1 involvement they are worth. */
.ladder { margin: 16px 0 4px; display: flex; flex-direction: column; gap: 6px;
          max-width: 78ch; }
.rung { display: flex; align-items: center; gap: 10px; font-size: 0.8rem; }
.rung.me .rname, .rung.me .rval { color: var(--text); }
.rname { font-weight: 650; letter-spacing: 0.4px; color: var(--muted);
         width: 26px; flex: none; }
.rbar { flex: 0 0 190px; height: 7px; background: var(--surface2);
        border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
.rbar i { display: block; height: 100%; background: var(--accent); }
.rval { font-family: var(--mono); font-size: 0.76rem; color: var(--muted);
        width: 34px; flex: none; text-align: right; }
.rnote { color: var(--muted); font-size: 0.76rem; }

/* Division tags. Colour is REDUNDANT here, never the only channel, because the tag
   already reads D1/D2/D3 in text. It exists so the mix of a pooled table is scannable
   without reading every row, which is the one thing this page has to make obvious. */
.dtag {
  font-family: var(--mono); font-size: 0.68rem; font-weight: 600; letter-spacing: 0.3px;
  padding: 2px 6px; border-radius: 4px; border: 1px solid;
}
.d-d1 { color: var(--accent); border-color: #1f4f7d; background: #10243a; }
.d-d2 { color: var(--purple); border-color: #4b3a70; background: #241c38; }
.d-d3 { color: var(--green); border-color: #24603a; background: #12271b; }

/* The converted rate is the column the page exists for, so it gets the emphasis the
   adjusted rate gets on the single-division leaderboard. */
td.lvl strong { color: var(--text); }
th.lvl a.on { color: var(--text); }

/* Three lists side by side, never one interleaved list. The layout is the argument:
   separate columns say these rankings do not share an axis, where one table would say
   the opposite no matter what the note underneath it claimed. */
.cols3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin: 4px 0 0; }
.col { border: 1px solid var(--border); border-radius: var(--radius);
       background: var(--surface); padding: 11px 13px; }
.colhead { display: flex; align-items: center; justify-content: space-between;
           gap: 8px; margin-bottom: 8px; font-size: 0.78rem; }
.ranklist { list-style: decimal; margin: 0; padding-left: 18px; }
.ranklist li { font-size: 0.84rem; padding: 3px 0; }
.ranklist li::marker { color: var(--muted); font-size: 0.74rem; }
.ranklist .num { float: right; font-size: 0.78rem; }
@media (max-width: 720px) { .cols3 { grid-template-columns: 1fr; } }

/* The slash-to-search hint. Shown only once the field is idle, so it never sits on
   top of what you are typing. */
.kbd {
  font-family: var(--mono); font-size: 0.68rem; color: var(--muted);
  border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px;
  background: var(--surface2);
}
.hint { position: absolute; right: 9px; top: 50%; transform: translateY(-50%);
        pointer-events: none; transition: opacity 0.12s ease; }
.results input:focus ~ .hint, .results.typing .hint { opacity: 0; }

/* Cards */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
         gap: 10px; margin: 16px 0; }
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 12px 14px;
  transition: border-color 0.14s ease, transform 0.14s ease;
}
.card:hover { border-color: var(--muted); transform: translateY(-1px); }
/* Card labels are written lowercase in the templates rather than forced lowercase
   here, because the rule was also flattening the division in "rank in D1 2025". */
.card .k { font-size: 0.72rem; color: var(--muted); letter-spacing: 0.3px; }
.card .v { font-size: 1.28rem; font-weight: 650; font-variant-numeric: tabular-nums; margin-top: 3px; }
.card .x { font-size: 0.73rem; color: var(--muted); margin-top: 2px; }

/* Tables

   THE STICKY HEADER WAS PARKING ITSELF ON TOP OF THE FIRST TWO ROWS, and the cause
   is one property. An ancestor whose overflow is anything other than visible becomes
   the containing block for a sticky descendant, so `overflow-x: auto` here meant
   `top: 52px` was measured from the top of .tablewrap instead of from the viewport,
   and the header sat 52px down inside its own table. The wrapper only exists to let a
   wide table scroll sideways, which is a narrow-screen problem, so the overflow is
   scoped to that case at the bottom of this file and the header now sticks under the
   navbar the way it was always meant to. */
.tablewrap { border: 1px solid var(--border); border-radius: var(--radius); }
table { width: 100%; border-collapse: collapse; font-size: 0.83rem; }
thead th {
  position: sticky; top: 52px; z-index: 5; background: var(--surface);
  text-align: left; font-weight: 600; color: var(--muted);
  padding: 9px 10px; border-bottom: 1px solid var(--border); white-space: nowrap;
  font-size: 0.75rem; letter-spacing: 0.2px;
}
/* The header paints over the wrapper's rounded corners, so it has to carry them. */
thead th:first-child { border-top-left-radius: var(--radius); }
thead th:last-child  { border-top-right-radius: var(--radius); }
/* The bottom corners belong to whichever section is actually last, so a table with a
   totals row hands them to the tfoot. Rounding the tbody as well leaves a notch where
   the two backgrounds meet. */
tbody:not(:has(+ tfoot)) tr:last-child td:first-child { border-bottom-left-radius: var(--radius); }
tbody:not(:has(+ tfoot)) tr:last-child td:last-child  { border-bottom-right-radius: var(--radius); }
tfoot tr:last-child td:first-child { border-bottom-left-radius: var(--radius); }
tfoot tr:last-child td:last-child  { border-bottom-right-radius: var(--radius); }

thead th a {
  color: var(--muted); display: inline-block; transition: color 0.12s ease;
}
thead th a:hover { color: var(--text); text-decoration: none; }
thead th a.on { color: var(--accent); }
/* The arrow is always in the markup and only inked on the active column, so a header
   never changes width when you sort by it and the row stops twitching. */
.arrow { opacity: 0; margin-left: 2px; font-size: 0.7em; }
thead th a.on .arrow { opacity: 1; }
thead th a:hover .arrow { opacity: 0.45; }

/* PADDING AND WRAPPING ARE SHARED WITH tfoot. Scoped to tbody alone, a totals row got
   no padding at all and its numbers ran straight into each other. */
tbody td, tfoot td { padding: 8px 10px; white-space: nowrap; }
tbody td { border-bottom: 1px solid var(--border); }
tbody tr:last-child td { border-bottom: none; }
tbody tr { transition: background 0.1s ease; }
tbody tr:hover { background: var(--surface); }
tbody tr:hover td:first-child { box-shadow: inset 2px 0 0 var(--accent); }
tbody tr.sel { background: var(--surface2); }
tbody tr.sel td:first-child { box-shadow: inset 2px 0 0 var(--accent); }
/* A whole row is a click target where the row stands for one thing. The anchor inside
   still works, so middle-click and copy-link keep behaving normally. */
tr.rowlink { cursor: pointer; }
.num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--mono); font-size: 0.79rem; }
.dim { color: var(--muted); }
.up { color: var(--green); }
.down { color: var(--red); }
.empty { color: var(--muted); font-size: 0.86rem; padding: 26px 4px; }

/* Not ranked. Deliberately quiet and deliberately WORDS rather than a dash, because a
   dash in a numeric column reads as data that failed to arrive. This is the opposite,
   a number withheld on purpose, and the row beside it is complete. */
.unranked {
  font-family: var(--sans); font-size: 0.7rem; letter-spacing: 0.2px;
  color: var(--muted); font-style: italic; white-space: nowrap;
}
.card .v.sm { font-size: 1rem; font-weight: 600; color: var(--muted); }

/* Flag, deliberately amber and never red. It means look harder, not bad player. */
.flag {
  display: inline-block; font-size: 0.68rem; font-weight: 700; letter-spacing: 0.4px;
  color: var(--amber); border: 1px solid var(--amber); border-radius: 4px;
  padding: 1px 5px; vertical-align: middle;
}
.tag {
  display: inline-block; font-size: 0.68rem; letter-spacing: 0.3px; color: var(--muted);
  border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; vertical-align: middle;
}
.tag.warn { color: var(--purple); border-color: var(--purple); }

/* School mark. The box is a fixed square whether or not there is a file, because
   coverage is partial by construction and a missing logo must not reflow a row or
   read as a broken image.

   THE TILE IS LIGHT ON PURPOSE and it is not a style choice. These marks are drawn
   by their schools for white backgrounds. Measured over a 400 file sample, 115 of
   them carry no colour lighter than 0.35 luminance, so on this app's own surface
   roughly a third would render as an invisible smudge. A pale tile is what makes
   them legible at all.

   Both states use the SAME tile, so a school with a mark does not read as more real
   than one without. The initials are muted rather than black for the same reason. */
.crest {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--crest, 22px); height: var(--crest, 22px);
  flex: 0 0 var(--crest, 22px); border-radius: 5px;
  background: #eceff3; border: 1px solid var(--border);
  vertical-align: middle; overflow: hidden;
}
.crest img { width: 78%; height: 78%; object-fit: contain; display: block; }
.crest-ini {
  font-size: calc(var(--crest, 22px) * 0.4); font-weight: 650; color: #7d8793;
  letter-spacing: 0.3px; line-height: 1; user-select: none;
}
/* In a table cell the mark sits beside the name and must not push the text around,
   so the pair is a flex row with the name free to truncate. */
.crestcell { display: flex; align-items: center; gap: 8px; min-width: 0; }
.crestcell > a, .crestcell > span { min-width: 0; }

/* Player photo. Round, so it cannot be confused with the square school mark sitting
   a few pixels away, and so a portrait crop reads as a person rather than a card.

   MOST PLAYERS HAVE NO PHOTO and that is by design, the resolver drops anything it
   cannot place cleanly. So the initials box is the ordinary case and is styled as a
   real element rather than an error state. It is the same size as the photo, so a
   page does not change shape depending on whether a face happened to resolve, and a
   player with a photo must not read as better documented than one without. */
.face {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--face, 56px); height: var(--face, 56px);
  flex: 0 0 var(--face, 56px); border-radius: 50%;
  background: var(--surface2); border: 1px solid var(--border);
  overflow: hidden; vertical-align: middle;
}
.face img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* In the photo column the initials would be 26px of lettering repeated down a table
   whose job is numbers, so the empty state drops to a plain ring. What is being read
   there is presence and absence, and a ring says absent more quietly than two more
   characters do. */
.facecell { width: 34px; padding-right: 0; }
.facecell .face.none { background: transparent; }
.facecell .face-ini { display: none; }
.face-ini {
  font-size: calc(var(--face, 56px) * 0.34); font-weight: 600; color: var(--muted);
  letter-spacing: 0.5px; line-height: 1; user-select: none;
}

/* Percentile bar. Banded by value rather than one flat blue, so a column of these
   reads as a shape you can scan instead of 250 identical stripes. */
.mini { display: inline-block; width: 54px; height: 5px; background: var(--surface2);
        border-radius: 3px; overflow: hidden; vertical-align: middle; margin-left: 7px; }
.mini-fill { display: block; height: 100%; background: var(--accent);
             transition: width 0.25s ease; }
.mini-fill.t4 { background: var(--green); }
.mini-fill.t3 { background: var(--accent); }
.mini-fill.t2 { background: var(--muted); }
.mini-fill.t1 { background: #4a5560; }

/* Team rating bar. Grows out of a CENTRE line, not from the left edge, because a
   rating is a signed deviation from a division mean pinned at zero. Filling from the
   left would draw an average team as half full and the division's worst as merely
   short, when the honest reading is nothing at all for average and length the other
   way for bad. Read down the column it gives the division's shape in one glance. */
.dvg { position: relative; display: inline-block; width: 96px; height: 9px;
       vertical-align: middle; margin-left: 9px; }
th.rat, td.rat { width: 168px; }
.dvg-zero { position: absolute; left: 50%; top: -1px; bottom: -1px; width: 1px;
            background: var(--border); }
.dvg-fill { position: absolute; top: 2px; height: 5px; border-radius: 2px;
            transition: width 0.25s ease; }
.dvg-fill.pos { background: var(--green); }
.dvg-fill.neg { background: #5c4148; }

/* Conference spread. A mean alone cannot tell a conference of eleven similar teams
   from one holding both the best and the worst side in the division, and those are
   not the same conference. The span is drawn weakest to strongest with the mean
   ticked on it, over a shared axis whose centre is the division average. */
.rng { position: relative; display: inline-block; width: 100%; min-width: 120px;
       height: 11px; vertical-align: middle; }
.rng::before { content: ""; position: absolute; left: 0; right: 0; top: 5px; height: 1px;
               background: var(--surface2); }
.rng-zero { position: absolute; left: 50%; top: 0; bottom: 0; width: 1px;
            background: var(--border); }
.rng-span { position: absolute; top: 3px; height: 5px; border-radius: 3px;
            background: var(--accent); opacity: 0.4; }
.rng-mean { position: absolute; top: 0; width: 2px; height: 11px; margin-left: -1px;
            border-radius: 1px; background: var(--accent); }

/* The one column allowed to stretch. Everything else in a table is nowrap and sized
   by its content, so a bar column would otherwise collapse to its min-width. */
th.wide, td.wide { width: 132px; min-width: 132px; }

/* Chart */
.chartwrap { background: var(--surface); border: 1px solid var(--border);
             border-radius: var(--radius); padding: 12px 14px 8px; margin: 4px 0 6px; }
.chart { width: 100%; height: auto; display: block; }
.chart .grid { stroke: var(--border); stroke-width: 1; }
.chart .median { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 3 3; }
.chart .axis { fill: var(--muted); font-size: 10px; font-family: var(--font); }
.chart .band { font-weight: 600; letter-spacing: 0.2px; }
/* The weaker half is shaded, not just ruled off, because which side the goals fell on
   is the entire question the picture is answering. */
.chart .weakband { fill: var(--accent); opacity: 0.05; }
.chart .pt { transition: r 0.12s ease, opacity 0.12s ease; cursor: crosshair; }
.chart .pt.on { fill: var(--accent); opacity: 0.9; }
.chart .pt.off { fill: none; stroke: var(--muted); stroke-width: 1.2; opacity: 0.5; }
.chart .pt:hover { opacity: 1; }
.chart .pt.on:hover { r: 8; }
.chart .pt.off:hover { r: 6; stroke: var(--text); }

/* SVG TEXT DOES NOT REFLOW, IT SCALES WITH THE PICTURE. A 680 unit chart is drawn into
   334px on a phone, so a 10px label comes out under 5px and cannot be read at all. The
   sizes here are user units chosen to land near 11px once that scale is applied, which
   is why the two widths need different numbers rather than one override. The class is
   emitted from the width the chart was generated at, so a new size cannot silently
   inherit the wrong one. */
@media (max-width: 720px) {
  .chart.w680 .axis { font-size: 20px; }
  .chart.w430 .axis { font-size: 14px; }
  .chart.w680 .wfval, .chart.w680 .wflab { font-size: 20px; }
}

/* Search results */
.results { position: relative; }
.hits { position: absolute; z-index: 60; left: 0; right: 0; max-width: 460px;
        background: var(--surface); border: 1px solid var(--border);
        border-radius: var(--radius); margin-top: 4px; overflow: hidden; }
.hits a { display: block; padding: 8px 12px; font-size: 0.84rem; color: var(--text); }
.hits a:hover, .hits a.sel { background: var(--surface2); text-decoration: none; }
.hits a.sel { box-shadow: inset 2px 0 0 var(--accent); }
.hits .m { color: var(--muted); font-size: 0.76rem; margin-left: 6px; }
.hits .none { padding: 8px 12px; font-size: 0.82rem; color: var(--muted); }
/* AN EMPTY RESULTS BOX IS STILL A BORDERED BOX, so with nothing typed it rendered as a
   2px line hanging under the navbar. It only had somewhere to show once the search
   moved into the nav and became fixed. */
.hits:empty { display: none; }

/* THE NAV DROPDOWN IS FIXED RATHER THAN ABSOLUTE so it can be wider than the input it
   hangs off. It used to be load bearing for a second reason, that the navbar took
   `overflow-x: auto` at the narrow breakpoint and an overflow that is not visible
   clips absolutely positioned descendants. That reason is gone, because the narrow
   navbar is two rows now instead of one row that scrolls, and the narrow rule at the
   bottom of this file puts the list back on absolute where it can anchor itself to a
   search box whose vertical position is no longer a constant. */
.nav-search .hits {
  position: fixed; top: 50px; left: auto; right: 24px;
  width: min(460px, calc(100vw - 48px)); max-width: none;
  max-height: calc(100vh - 80px); overflow-y: auto;
}

.split { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }

/* A TABLE THAT DOES NOT FIT MUST SCROLL INSIDE ITS BORDER, NEVER SPILL OUT OF IT.
   The scroll container was scoped to narrow screens only, so on a wide screen the
   flagged view, which adds four evidence columns and is eighteen columns wide, ran
   its last cells and its own header straight past the rounded border. It read as a
   broken page rather than a wide one.

   THE COST IS THE STICKY HEADER AND IT IS WHY THIS IS OPT IN RATHER THAN GLOBAL. An
   overflow container becomes the containing block for position:sticky, so a header
   inside one stops sticking to the viewport and the 52px navbar offset has to go back
   to zero. Applying that everywhere would trade a bug that fires on one view for a
   regression on every long table, so `wide` is set by the template on the views that
   genuinely need it and the ordinary table keeps its viewport-sticky header. */
.tablewrap.wide { overflow-x: auto; }
.tablewrap.wide thead th { top: 0; }
/* AND THE SCROLL IS THE SAFETY NET RATHER THAN THE FIX. Eighteen columns overflow a
   1500px screen by a hair, so the last cell was half visible and the table read as
   broken rather than as scrollable. Padding is 20px per cell horizontally, which is
   360px of the width spent on gaps, so tightening it on this view alone buys back more
   than the overflow costs and the table simply fits. */
.tablewrap.wide td, .tablewrap.wide th { padding-left: 7px; padding-right: 7px; }
.tablewrap.wide table { font-size: 0.8rem; }
@media (max-width: 1080px) {
  .tablewrap { overflow-x: auto; }
  thead th { top: 0; }

  /* A NARROW SCREEN CANNOT SHOW A FOURTEEN COLUMN TABLE AND MUST NOT PRETEND OTHERWISE.
     At 390px the leaderboard rendered 2 of its 14 columns and hid 702px of itself, so
     the reader got a rank, a name and not one of the numbers the page exists for, and
     iOS draws no scrollbar at rest to say the thing slides. Two rules answer that
     together and neither of them hides a column.

     THE IDENTITY COLUMN PARKS AT THE LEFT EDGE. Which column that is depends on the
     table, so it is named rather than guessed. On a ranked list it is the second cell,
     because a rank is only worth reading while the row it belongs to is still legible,
     so the name is the thing that has to survive a swipe. On a career or a squad the
     first cell already is the identity, and with photos on there is a face between the
     rank and the name, which is why there are three of these rather than one. Sticky
     needs no measured offset at `left: 0`, so none of this depends on knowing how wide
     the columns to its left happen to be. The background has to be opaque or the cells
     slide visibly underneath, and the tfoot keeps its own surface so a totals row does
     not gain one mismatched cell. */
  .tablewrap.pin1 td:nth-child(1), .tablewrap.pin1 th:nth-child(1),
  .tablewrap.pin  td:nth-child(2), .tablewrap.pin  th:nth-child(2),
  .tablewrap.pin3 td:nth-child(3), .tablewrap.pin3 th:nth-child(3) {
    position: sticky; left: 0; z-index: 4;
    background: var(--bg); box-shadow: inset -1px 0 0 var(--border);
  }
  .tablewrap.pin1 thead th:nth-child(1),
  .tablewrap.pin  thead th:nth-child(2),
  .tablewrap.pin3 thead th:nth-child(3) { z-index: 7; background: var(--surface); }
  .tablewrap.pin1 tfoot td:nth-child(1),
  .tablewrap.pin  tfoot td:nth-child(2),
  .tablewrap.pin3 tfoot td:nth-child(3) { background: var(--surface); }
  /* An opaque cell does not let the row highlight through, so the pinned one has to
     repeat it or a hovered row on a laptop narrow enough to reach this breakpoint
     lights up everywhere except the column holding the name. The accent stripe is
     restated alongside the divider for the same reason, since one box-shadow property
     cannot be inherited from two rules. */
  .tablewrap.pin1 tbody tr:hover td:nth-child(1),
  .tablewrap.pin  tbody tr:hover td:nth-child(2),
  .tablewrap.pin3 tbody tr:hover td:nth-child(3) { background: var(--surface); }
  .tablewrap.pin1 tbody tr.sel td:nth-child(1),
  .tablewrap.pin  tbody tr.sel td:nth-child(2),
  .tablewrap.pin3 tbody tr.sel td:nth-child(3) { background: var(--surface2); }
  .tablewrap.pin1 tbody tr:hover td:nth-child(1),
  .tablewrap.pin1 tbody tr.sel td:nth-child(1) {
    box-shadow: inset 2px 0 0 var(--accent), inset -1px 0 0 var(--border);
  }

  /* AND THE RIGHT EDGE SAYS THERE IS MORE. The cue is painted as a background rather
     than an element because a scroll container cannot hold an overlay that stays put,
     and pairing a `local` cover with a `scroll` haze is what makes it hide itself at
     the end of the travel with no script and no scroll listener. Only the right side
     is drawn, since the left one would sit behind the pinned cell.

     IT IS A PALE HAZE AND NOT THE USUAL SHADOW, because the usual shadow is black and
     this app's surface is #0d1117. A black edge on a near black table is invisible,
     which is the whole reason the first attempt at this did nothing. */
  .tablewrap {
    background-image:
      linear-gradient(to left, var(--bg), rgba(13, 17, 23, 0)),
      radial-gradient(farthest-side at 100% 50%,
                      rgba(139, 148, 158, 0.28), rgba(139, 148, 158, 0));
    background-position: right center, right center;
    background-size: 34px 100%, 18px 100%;
    background-repeat: no-repeat, no-repeat;
    background-attachment: local, scroll;
  }
  /* Where a scrollbar is drawn at all it should be visible rather than discovered. */
  .tablewrap { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
  .tablewrap::-webkit-scrollbar { height: 9px; }
  .tablewrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; }
  .tablewrap::-webkit-scrollbar-track { background: transparent; }
}

/* THE NAVBAR GROWS TO FIT ITS CONTENTS RATHER THAN CLIPPING THEM. As a single 52px bar
   it was 671px of content inside a 390px viewport, so 281px of it lived behind a
   horizontal scroll with nothing on screen to say so. About sat half cut and the global
   player search, which is the way into the whole app, was entirely past the edge.

   THE FIRST PASS MADE IT TWO ROWS AND KEPT THE TAB STRIP SCROLLING AS A SAFETY NET, AND
   THAT SAFETY NET WAS THE REMAINING BUG. A scroll container with its scrollbar
   suppressed cannot say it has more inside it, so the sixth tab did not read as
   offscreen, it read as a broken bar. Measured at the widths people actually hold: at
   390px, the most common phone viewport there is, About rendered 18 of its 48px. At
   360px About was gone entirely and Wyscout was sliced. The strip WRAPS now, so the bar
   takes another line when it needs one and no tab is ever cut at any width. Adding a
   seventh tab costs height instead of costing a reader the last tab.

   ROWS ARE ORDERED RATHER THAN LEFT TO THE SOURCE, because the tabs want the full width
   to centre inside and the brand and the stamp are both small. Row one is the brand and
   the build stamp, row two is the tabs, row three is search. Order is set here and not
   in the template so the desktop bar, which is one row and reads left to right in
   source order, is untouched.

   THE WORDMARK COMES BACK, and it was only ever hidden to buy 91px for a strip that
   could not fit. Once the bar grows the thing that rule was paying for does not exist,
   so the app gets to say its own name on a phone.

   THE STAMP ALSO COMES BACK HERE, for the same reason and on the same reasoning. */
@media (max-width: 860px) {
  .split { grid-template-columns: 1fr; }
  .main { padding: 18px 14px 48px; }
  .navbar {
    height: auto; flex-wrap: wrap; overflow: visible;
    gap: 7px 10px; padding: 7px 14px;
  }
  .nav-brand { order: 1; }
  /* A full-width basis is what puts the tabs on a line of their own, and wrapping is
     what lets that line become two on a narrow phone instead of hiding a tab behind a
     scroll nobody can see. Centred because a wrapped last line left-aligned under a
     full one reads as an unfinished row rather than a deliberate second line. The row
     gap is larger than the column gap so two lines of tabs do not touch. */
  .nav-tabs {
    order: 3; flex: 1 1 100%; min-width: 0;
    flex-wrap: wrap; justify-content: center; gap: 5px 2px;
  }
  .nav-tab { padding: 7px 9px; }
  .nav-search { order: 4; flex: 1 1 100%; margin-left: 0; min-width: 0; }
  /* The slash shortcut needs a keyboard, so on a touch device the badge is a hint at
     something the reader cannot do, sitting on top of a placeholder that is already
     being truncated for width. */
  .nav-search .hint { display: none; }
  /* Shares row one with the brand and is pushed to the far end of it. Both are small
     and neither wants a line to itself, so the row that would otherwise be half empty
     carries the pair. */
  .nav-asof { order: 2; flex: 0 0 auto; margin-left: auto; border-left: 0; padding-left: 0; }
  /* Anchored to the input now that the bar has no overflow to clip it, which also
     means it follows the search box instead of trusting a hardcoded 50px that a two
     row navbar makes wrong. */
  .nav-search .hits {
    position: absolute; top: calc(100% + 4px); left: 0; right: 0;
    width: auto; max-height: 60vh;
  }
  /* 16px IS NOT A TYPE CHOICE, IT IS THE iOS ZOOM THRESHOLD. Safari zooms the whole
     page in when a field smaller than that takes focus and never zooms back out, so
     tapping the filter box left the reader stranded at 1.3x on a page that is already
     short of width. Every field in the app was 12.6px. */
  .input, .select { font-size: 16px; }
  /* The separator divides the controls that apply on change from the pair that waits
     for Apply. Once the bar wraps into three rows the break does that job and the rule
     is left dangling at the end of whichever line it landed on. */
  .sep { display: none; }
  /* Same reasoning one step further. A wrapped bar has no far end to send anything to,
     so every line starts at the left edge like the heading and the table under them. */
  .push { margin-left: 0; }
  /* Pinning the strip as well as a two row navbar would spend a fifth of a phone
     screen on furniture before any content. */
  .wybar { position: static; padding: 7px 14px; }
}

/* THIS BUYS THE TABS ANOTHER LINE'S WORTH OF WIDTH BEFORE THEY TAKE ONE. It was written
   when the strip could not wrap, to make five tabs fit whole at 360px where they were
   6px over. The strip wraps now, so nothing here is load bearing for correctness any
   more and a tab can no longer be sliced whatever this rule says. What it still does is
   hold the tabs on a single line down to a narrower phone than they would otherwise
   manage, which is worth a fifth of a rem.

   IT MOVED FROM 400px TO 480px BECAUSE THE OLD EDGE LEFT A DEAD ZONE. Six tabs at full
   size need 425px of viewport and at reduced size 386px, so between 401 and 428 the bar
   took a second line while a NARROWER phone fitted on one. A 414px handset drew a taller
   bar than a 390px one, which is not wrong but is visibly silly. The two thresholds have
   to meet rather than overlap. */
@media (max-width: 480px) {
  .navbar { gap: 7px 8px; }
  .nav-tab { font-size: 0.8rem; padding: 7px 7px; }
}

/* Nothing here animates for anyone who has asked their machine to stop animating. */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* Career totals row. Separated from the seasons above it by weight and a rule rather
   than by colour, so it reads as a summary of the table instead of another row in it. */
tfoot tr.total td {
  border-top: 2px solid var(--border); font-weight: 600;
  background: var(--surface); color: var(--text);
}
tfoot tr.total td.dim { font-weight: 400; }

/* COMPARE. Two players, two colours, everywhere on the page. A takes the accent and B
   takes pink, which is the one hue not already carrying a meaning here: green is a good
   value, red a bad one, amber is the weak-opposition flag, and blue, purple and green
   are already spoken for by the three division tags. */
.cslots { display: grid; grid-template-columns: 1fr auto 1fr; gap: 12px;
          align-items: stretch; margin: 14px 0 4px; }
.cswap { display: flex; align-items: center; justify-content: center; font-size: 0.8rem; }
.cslot {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); padding: 11px 13px; min-width: 0;
}
/* The side stripe is what makes A and B legible without reading the letter, and it is
   the same pair of colours the bars and the chart marks use. */
.cslot.a { box-shadow: inset 3px 0 0 var(--accent); }
.cslot.b { box-shadow: inset 3px 0 0 var(--pink); }
.cslot.empty { border-style: dashed; }
.cwho { display: flex; align-items: center; gap: 8px; font-size: 0.95rem;
        font-weight: 600; flex-wrap: wrap; }
.cmeta { display: flex; align-items: center; gap: 8px; margin-top: 8px;
         flex-wrap: wrap; font-size: 0.8rem; }
.cmeta .select { max-width: 260px; }
.ctag {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 4px; font-size: 0.68rem; font-weight: 700;
  color: #06121f; flex: none;
}
.ctag.a { background: var(--accent); }
.ctag.b { background: var(--pink); }
.key { font-weight: 600; }
.key.a { color: var(--accent); }
.key.b { color: var(--pink); }
.cpick { margin-top: 8px; }
.cpick .input { width: 100%; }
.cpick .hits { max-width: none; }

/* THE VERDICT IS A PANEL AND NOT A PARAGRAPH, because it is the answer to the question
   the page was opened to ask and everything below it is only the working. A comparison
   the ladder cannot settle gets a muted panel rather than a green one, so refusing to
   answer looks like a result and not like a page that failed to load. */
.verdict {
  border: 1px solid var(--border); border-left: 3px solid var(--green);
  border-radius: var(--radius); background: var(--surface);
  padding: 13px 15px; margin: 16px 0 6px;
}
.verdict.tied { border-left-color: var(--muted); }
.vhead { display: flex; align-items: center; gap: 8px; font-size: 1.02rem;
         font-weight: 650; flex-wrap: wrap; }
.vbody { color: var(--muted); font-size: 0.84rem; line-height: 1.55; margin-top: 6px; }
.vbody strong { color: var(--text); font-weight: 600; }

/* The comparison table reads down the metrics rather than across the players, so the
   two numbers being weighed are always adjacent on the same line. */
table.cmp td:first-child, table.cmp th:first-child { width: 34%; }
table.cmp th.a { color: var(--accent); }
table.cmp th.b { color: var(--pink); }
table.cmp td.num { width: 15%; }
table.cmp .note-cell { font-size: 0.76rem; }
td.win-a, td.win-b { color: var(--text); font-weight: 650; }
td.win-a { background: rgba(88, 166, 255, 0.09); }
td.win-b { background: rgba(247, 120, 186, 0.09); }
table.cmp tr.lvlrow td { border-top: 1px solid var(--border); background: var(--surface); }

/* Waterfall. Bars are coloured by WHICH PLAYER the step favours rather than by good and
   bad, so a run of pink in a blue comparison reads immediately as the corrections eating
   somebody's lead. The two total columns are outlined rather than solid, which separates
   a running position from a step. */
.wf .zero { stroke: var(--muted); stroke-width: 1; }
.wf .link { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 3; }
.wf .wfbar.a { fill: var(--accent); }
.wf .wfbar.b { fill: var(--pink); }
.wf .wfbar.flat { fill: var(--muted); }
/* A step is translucent and a running total is solid, so the two columns you can read a
   position off are the two that look solid. */
.wf .wfbar { opacity: 0.55; }
.wf .wfbar.total { opacity: 1; }
.wf .wfval { font-size: 11px; font-family: var(--mono); font-weight: 600; }
.wf .wfval.a { fill: var(--accent); }
.wf .wfval.b { fill: var(--pink); }
.wf .wfval.flat { fill: var(--muted); }
.wf .wflab { fill: var(--text); font-size: 11px; font-family: var(--font);
             font-weight: 600; letter-spacing: 0.2px; }
.wf .wfband { stroke: var(--text); stroke-width: 1.4; opacity: 0.75; }

/* Paired scatter. Shape carries the player as well as colour, so the two series stay
   apart where marks land on top of each other. */
.chart.pair .pt.sa.on { fill: var(--accent); stroke: none; opacity: 0.9; }
.chart.pair .pt.sa.off { fill: none; stroke: var(--accent); stroke-width: 1.2; opacity: 0.45; }
.chart.pair .pt.sb.on { fill: var(--pink); stroke: none; opacity: 0.9; }
.chart.pair .pt.sb.off { fill: none; stroke: var(--pink); stroke-width: 1.2; opacity: 0.45; }
.chart.pair .pt:hover { opacity: 1; }

@media (max-width: 860px) {
  .cslots { grid-template-columns: 1fr; }
  .cswap { justify-content: flex-start; }
}

/* THE WYSCOUT SESSION STRIP. It sits outside the navbar deliberately. The navbar is
   identical for every reader and this bar is the one thing on the page that is not, so
   folding it in there would put session state in the one element whose job is to be
   global. Same reasoning that removed the page-meta slot from the nav. */
.wybar {
  position: sticky; top: 52px; z-index: 99;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 7px 24px;
  background: var(--surface2); border-bottom: 1px solid var(--border);
  font-size: 0.78rem;
}
.wybar-tag {
  color: var(--purple); border: 1px solid var(--purple); border-radius: 5px;
  padding: 1px 7px; font-size: 0.68rem; letter-spacing: 0.4px; text-transform: uppercase;
}
.wybar-file { color: var(--text); display: inline-flex; align-items: baseline; gap: 7px; }
.wybar-file .m { color: var(--muted); font-size: 0.72rem; }
.wybar-note { color: var(--muted); margin-left: auto; }
.wybar-clear { display: inline-block; }
.btn-lite {
  background: none; border: 1px solid var(--border); border-radius: 5px;
  color: var(--muted); font: inherit; font-size: 0.75rem; padding: 3px 10px;
  cursor: pointer;
}
.btn-lite:hover { color: var(--text); border-color: var(--muted); }

/* A message about the file the reader just handed over, so it has to be readable
   before anything else on the page and must not look like a stat. */
.notice {
  border: 1px solid var(--amber); border-left-width: 3px; border-radius: var(--radius);
  background: var(--surface); color: var(--text);
  padding: 12px 15px; margin: 16px 0; font-size: 0.86rem; line-height: 1.55;
}
.upload {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  margin: 14px 0 4px;
}
.upload .input { flex: 1 1 320px; padding: 8px 10px; }
.mono { font-family: var(--mono); font-size: 0.85em; color: var(--muted); }

/* Numbered instructions. A list of actions in order needs to read as one, so it gets the
   note's line height and colour rather than a bare <ol> tight against the margin. */
.steps {
  margin: 14px 0 4px 20px; padding: 0;
  color: var(--muted); font-size: 0.86rem; line-height: 1.7; max-width: 74ch;
}
.steps li { margin-bottom: 5px; padding-left: 4px; }
.steps strong { color: var(--text); font-weight: 600; }

/* THE ANSWER TO "I UPLOADED A FILE, NOW WHAT". It sits directly under the counts because
   that is where the reader is looking, and it is a panel rather than a sentence so the
   action is findable without reading the page. */
.cta {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  border: 1px solid var(--accent); border-radius: var(--radius);
  background: var(--surface); padding: 14px 16px; margin: 18px 0;
}
.cta-body { flex: 1 1 340px; font-size: 0.86rem; line-height: 1.6; color: var(--muted); }
.cta-body strong { color: var(--text); font-weight: 600; }
.cta .btn { white-space: nowrap; }

/* The metric picker on the ranked view. Each group is its own panel and the segments
   inside wrap, because a Wyscout export offers thirteen of them and they were running
   into each other on one line. */
.mpick { display: grid; gap: 12px; margin: 16px 0 6px; }
.mpick .cslot { display: flex; flex-direction: column; gap: 9px; }
.mpick .k { font-size: 0.72rem; color: var(--muted); letter-spacing: 0.3px; }
.mpick .seg { flex-wrap: wrap; row-gap: 3px; }
.mpick .seg a { padding: 5px 11px; }

/* Footer. Sits below every page, so it takes the top border rather than a margin, which
   keeps it attached on a short page and out of the way on a long one. */
.foot {
  border-top: 1px solid var(--border); margin-top: 44px;
  padding: 20px 24px 30px;
  display: flex; align-items: baseline; justify-content: space-between;
  flex-wrap: wrap; gap: 8px;
  font-size: 0.78rem; color: var(--muted);
}
.foot-links { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }

/* METRIC ROWS, not tiles. A tile grid puts the value directly under the label, so a
   label that wraps to two lines pushes its number down and the numbers stop lining up
   across a row. With thirteen metrics that reads as noise. Here the label and the value
   sit on one baseline with the value hard right, so every number in a column aligns no
   matter how long its label is, and tabular figures keep the digits themselves aligned. */
.metrics {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(258px, 1fr));
  gap: 0 26px; margin: 14px 0 4px;
}
.metric {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 9px 2px; border-bottom: 1px solid var(--border);
}
.metric dt { color: var(--muted); font-size: 0.82rem; min-width: 0; }
.metric dd {
  color: var(--text); font-size: 0.95rem; font-weight: 600;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* AFTER COLLEGE. The one block on a player page whose evidence comes from outside the
   spine, so it is given a surface of its own rather than another note. Green because
   this app uses green for a real signal everywhere else and a professional career is
   the strongest one it has. The lead line is a sentence, not a stat row, because the
   fact is categorical and a tile would invite comparing it with the rates below it. */
.prooutcome {
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--green);
  border-radius: var(--radius); padding: 13px 16px; margin: 0 0 20px;
}
.prooutcome > p:first-child { font-size: 0.95rem; line-height: 1.55; }
.prooutcome .note { margin: 10px 0 0; border-left-color: var(--border); }
.prooutcome .note:last-child { margin-bottom: 0; }

/* THE PRO BADGE. Coloured by tier because MLS and the third tier are a long way apart
   and one flat colour would say only "made it", which is the least interesting half of
   the fact. The label names the league rather than the tier, so the two third-tier
   colours are the same while the words are not. Deliberately quiet: this is a record
   rather than a warning and it does not want the eye the way the FLAG does. */
.protag {
  display: inline-block; font-size: 0.62rem; font-weight: 700; letter-spacing: 0.5px;
  padding: 2px 5px; border-radius: 4px; vertical-align: middle;
  border: 1px solid transparent; white-space: nowrap;
}
.protag.t1 { color: #a5d6ff; background: rgba(88,166,255,0.14); border-color: rgba(88,166,255,0.35); }
.protag.t2 { color: #7ee2a8; background: rgba(63,185,80,0.13);  border-color: rgba(63,185,80,0.32); }
.protag.t3 { color: #d8c07a; background: rgba(210,153,34,0.12); border-color: rgba(210,153,34,0.30); }
.protag.t0 { color: var(--muted); background: var(--surface2); border-color: var(--border); }
/* Abroad is a DIFFERENT HUE rather than a fourth step in the tier ramp, and that is the
   whole point. The blue to green to gold ramp encodes a measured ordering, so putting a
   career in Denmark anywhere on it would state a comparison this app has never fitted.
   Violet sits outside the ramp and reads as another kind of thing rather than another
   rung. */
.protag.abroad { color: #cbb2f0; background: rgba(163,113,247,0.13); border-color: rgba(163,113,247,0.32); }
.tags { white-space: nowrap; }
.tags > span + span { margin-left: 4px; }

/* Source links under the after-college block. They are the audit trail for a claim
   this app makes about a real person, so they are visible rather than tucked away. */
.prolinks { display: flex; gap: 14px; margin-top: 11px; font-size: 0.78rem; }

/* THE LEVEL PYRAMID on About. A four column grid rather than a table, because the
   middle column is a drawing and the two on the right are numbers, and a table cell
   cannot hold a bar that has to grow toward one edge while its neighbours stay put.

   THE BAR WIDTH IS THE SHAPE OF THE PYRAMID AND NOT A COUNT, which is why it is set
   from the row's position rather than from any data. Reading a length here as a
   population would be reading a number the app never measured. The population is in
   its own column beside it. */
.pyramid { margin: 14px 0 8px; }
.pyr-row {
  display: grid;
  grid-template-columns: minmax(150px, 1.4fr) minmax(90px, 2fr) 92px 62px;
  align-items: center; gap: 12px;
  padding: 5px 0; border-bottom: 1px solid var(--border);
}
.pyr-row:last-child { border-bottom: 0; }
.pyr-name { font-size: 0.86rem; }
.pyr-name .dim { font-size: 0.74rem; }
.pyr-bar-wrap { display: flex; justify-content: center; }
.pyr-bar {
  height: 16px; border-radius: 3px;
  background: linear-gradient(90deg, rgba(88,166,255,0.22), rgba(88,166,255,0.42));
  border: 1px solid rgba(88,166,255,0.38);
}
/* A rung with a fitted gap is drawn in the ladder's own colour, so the six rungs
   without one are visibly a different kind of row rather than a paler version of the
   same one. Colour carries the same distinction the blank gap column does. */
.pyr-row.measured .pyr-bar {
  background: linear-gradient(90deg, rgba(63,185,80,0.20), rgba(63,185,80,0.40));
  border-color: rgba(63,185,80,0.40);
}
/* Never checked, which is not the same as empty. Hatched rather than faded, because a
   faded bar reads as a small number and this is the absence of one. */
.pyr-row.unchecked .pyr-bar {
  background: repeating-linear-gradient(135deg,
    var(--surface2) 0 5px, transparent 5px 10px);
  border-color: var(--border);
}
.pyr-n, .pyr-off {
  text-align: right; font-family: var(--mono); font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}
.pyr-n .dim { font-family: var(--font); font-size: 0.72rem; }
.pyr-row.measured .pyr-off { color: var(--green); }
.pyr-row.head {
  border-bottom: 1px solid var(--border); padding-bottom: 4px;
  font-size: 0.72rem; letter-spacing: 0.4px; color: var(--muted);
  text-transform: uppercase;
}
.pyr-row.head .pyr-n, .pyr-row.head .pyr-off { font-family: var(--font); font-size: 0.72rem; }
@media (max-width: 720px) {
  .pyr-row { grid-template-columns: 1fr 70px 56px; }
  .pyr-bar-wrap { display: none; }
  /* The count belongs to the heading above it. Pushed right it wrapped onto a line of
     its own and read as a stray figure floating clear of everything it describes. */
  .pagehead .count { margin-left: 0; }
}

/* THE WATCHLIST STAR. It ships in the off state on every page and is painted from
   localStorage after load, so a filled star is the only thing in this app that differs
   between two people looking at the same URL. That is why it is deliberately quiet
   until it is on: an unfilled star must not read as a data column, and it sits at the
   end of the row where the pro badge already established that trailing cells are
   annotations rather than measurements. */
.wl-cell { width: 26px; text-align: center; }
.wl-star {
  background: none; border: 0; cursor: pointer; padding: 2px 4px; line-height: 1;
  font-size: 0.95rem; color: var(--border);
  transition: color 0.12s ease, transform 0.12s ease;
}
.wl-star:hover { color: var(--muted); transform: scale(1.15); }
.wl-star.on { color: var(--amber); }
.wl-star.on:hover { color: var(--amber); }
.wl-star.big { font-size: 1.5rem; padding: 0 10px; }

/* THE PITCH. Lines are derived from the formation rather than laid out by hand, so the
   two can never disagree. It is a grid and not a drawn pitch on purpose: the vertical
   order carries the only spatial claim being made, which is which line a player is in,
   and painting turf and penalty boxes behind it would imply positional detail down to
   the yard that a roster position label cannot support. */
.pitch {
  display: flex; flex-direction: column-reverse; gap: 10px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px;
}
.pitch-line { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.slot {
  flex: 1 1 120px; max-width: 190px; min-width: 120px;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 6px; padding: 8px;
}
.slot-pos {
  display: flex; align-items: center; gap: 6px;
  font-size: 0.66rem; letter-spacing: 0.6px; color: var(--muted); margin-bottom: 5px;
}
.slot select { width: 100%; font-size: 0.78rem; padding: 4px 6px; }
/* The value is the player's best percentile and it is a number the app already stands
   behind. It sits under the picker rather than beside the name so a long name cannot
   push it out of alignment with the slot beside it, which is the same tile-versus-row
   lesson the Wyscout metric grid taught. */
.slot-val {
  margin-top: 5px; font-family: var(--mono); font-size: 0.78rem;
  color: var(--text); text-align: right; min-height: 1.05rem;
}
/* Named for this bar specifically rather than reusing .cpick, which belongs to the
   compare page's player picker and sets its inputs to full width. */
.xi-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.xi-bar label { font-size: 0.78rem; color: var(--muted); }
.xi-bar select { width: auto; min-width: 120px; }
