/* ==========================================================================
   Badminton seasons — unofficial BWF fan tool

   The palette is the predecessor's BWF skin in dark, carried over so the two
   tools look like the same family: Roboto, BWF red, near-black rather than
   blue-black.

   Deliberately single-mode. The result ramp is a green-to-red gauge judged
   against this ground; a light flip changes the very thing that was tested, and
   the last pass shipped a first-round label in white on a near-white box
   because of it. Every colour is painted explicitly so the page holds its own
   ground whatever is behind it.
   ========================================================================== */

:root {
  color-scheme: dark;

  --font: Roboto, "Segoe UI", system-ui, -apple-system, sans-serif;
  --mono: "Roboto Mono", ui-monospace, Consolas, monospace;

  --bg:            #1a1a1a;
  --surface:       #262626;
  --surface-2:     #2f2f2f;
  --surface-sunk:  #212121;

  --text:          #f2f2f2;
  --text-dim:      #c9c9c9;
  --text-muted:    #9a9a9a;

  --border:        #3d3d3d;
  --border-soft:   #303030;

  --accent:        #df2027;   /* BWF red */
  --accent-hover:  #ca3732;
  --accent-fg:     #ffffff;
  --accent-text:   #ff5f64;   /* accent used AS text on dark */

  --bad:           #ff6b6b;
  --live:          #12ffc4;   /* a match being played, and only that */
  --warn:          #ffbc20;   /* scheduled, not yet under way */

  /* The result ramp, green for a title through to red for a first-round exit.
     Named here rather than inline because two views paint with it: the season
     strip fills a gauge to a percentage, the grid floods a whole cell. They
     have to agree, and one set of hexes is how that stays true. */
  --res-w:    #1a7f37;
  --res-f:    #3fa34d;
  --res-sf:   #7cb342;
  --res-qf:   #c9a227;
  --res-r16:  #e07b39;
  --res-r1:   #cf4b3f;
  --res-q:    #8f5a55;
  --res-unk:  #6e6e6e;   /* there, but the placing is not one we recognise */
  --res-na:   #4a4a4a;   /* there, with no individual placing at all */
  --res-off:  #292929;   /* did not play — the grid's ground */

  --radius-card:   4px;
  --radius-btn:    3px;
  --shadow:        0 2px 10px rgba(0, 0, 0, .55);
}

* { box-sizing: border-box; }

/* `hidden` has to actually hide. The attribute is only a UA `display: none`, so
   any rule that sets `display` at all outranks it — and this sheet sets it on
   nearly everything, which meant `el.hidden = true` on a flex legend or an
   inline-flex button group did precisely nothing. Asserted once, here, rather
   than remembered at every call site. */
[hidden] { display: none !important; }

body {
  margin: 0;
  padding: 16px 20px 36px;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 var(--font);
  -webkit-font-smoothing: antialiased;
}

h1 {
  font-size: 13px;
  font-weight: 500;
  letter-spacing: .02em;
  color: var(--text-muted);
  margin: 0 0 10px;
}

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

.controls {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 12px;
  align-items: center;
  margin-bottom: 10px;
}

.ctl { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-dim); }

input[type=text], input[type=search] {
  background: var(--surface-sunk);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  padding: 7px 10px;
  font: 400 14px var(--font);
}
input::placeholder { color: var(--text-muted); }

.search { position: relative; }
.search input { width: 280px; }

/* ---------- popovers: suggestions, top ranked, more levels ---------- */

.menu { position: relative; }

.panel, .suggest {
  position: absolute;
  z-index: 30;
  top: calc(100% + 4px);
  left: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow);
}
.panel[hidden], .suggest[hidden] { display: none; }

.suggest {
  right: 0;
  margin: 0;
  padding: 4px 0;
  list-style: none;
  max-height: 300px;
  overflow-y: auto;
}
.suggest li {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 11px;
  cursor: pointer;
  font-size: 14px;
}
.suggest li[aria-selected="true"], .suggest li:hover { background: var(--surface-2); }
.suggest .cc { font: 400 10.5px var(--mono); color: var(--text-muted); margin-left: auto; }
.suggest .none { color: var(--text-muted); cursor: default; font-style: italic; }
.suggest .none:hover { background: none; }

.panel { padding: 8px; min-width: 268px; }
.caret { font-size: 9px; opacity: .8; }

.tabs { display: flex; gap: 3px; margin-bottom: 7px; }
.tabs button {
  flex: 1;
  background: var(--surface-sunk);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-btn);
  color: var(--text-muted);
  font: 500 11px var(--mono);
  padding: 4px 0;
  cursor: pointer;
}
.tabs button:hover { color: var(--text-dim); }
.tabs button.on { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }

.toplist { margin: 0; padding: 0; list-style: none; max-height: 320px; overflow-y: auto; }
.toplist li { display: flex; align-items: center; gap: 8px; padding: 4px 5px; font-size: 13.5px; }
.toplist .rk {
  flex: none; width: 20px; text-align: right;
  font: 500 11px var(--mono); color: var(--text-muted); font-variant-numeric: tabular-nums;
}
.toplist .pl {
  background: none; border: 0; padding: 2px 4px; cursor: pointer;
  color: var(--text); font: 400 13.5px var(--font); text-align: left;
  border-radius: var(--radius-btn);
}
.toplist .pl:hover { background: var(--surface-2); color: var(--accent-text); }
.toplist .sep { color: var(--text-muted); font-size: 11px; }
.toplist .loading { color: var(--text-muted); font-style: italic; padding: 6px; }

.panel.checks { max-height: 320px; overflow-y: auto; }
.panel.checks label {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 6px; cursor: pointer; font-size: 13px; border-radius: var(--radius-btn);
}
.panel.checks label:hover { background: var(--surface-2); }
.panel.checks input { accent-color: var(--accent); }
.panel.checks .n { margin-left: auto; font: 400 10.5px var(--mono); color: var(--text-muted); }

/* ---------- the player ---------- */

.hero {
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 9px 13px;
  margin-bottom: 11px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-card);
}
.hero[hidden] { display: none; }

.avatar {
  flex: none;
  width: 46px; height: 46px;
  border-radius: 50%;
  object-fit: cover;
  object-position: top center;
  background: var(--surface-2);
  border: 1px solid var(--border);
}

.ident { min-width: 0; }
.who {
  margin: 0;
  font-size: 23px;
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.1;
  overflow-wrap: anywhere;
}
.meta {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 2px 0 0;
  color: var(--text-muted);
  font-size: 12.5px;
}
.flag { width: 19px; height: 19px; border-radius: 50%; object-fit: cover; flex: none; }

.heroctl { margin-left: auto; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }

/* ---------- toggles ---------- */

.seg {
  background: var(--surface-sunk);
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  padding: 6px 12px;
  font: 400 13px var(--font);
  cursor: pointer;
  text-transform: capitalize;
}
.seg + .seg { margin-left: -1px; }
.seg:hover { color: var(--text-dim); }
.seg.on { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.seg .n, .chip .n { font: 400 10.5px var(--mono); opacity: .75; margin-left: 4px; }

.only { color: var(--text-muted); font-size: 13px; text-transform: capitalize; }
.toggle { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  font-size: 13px; color: var(--text-dim); }
.toggle input { accent-color: var(--accent); }

/* ---------- filter chips ---------- */

.chiprow { display: flex; align-items: baseline; gap: 9px; margin-bottom: 5px; }
.chiprow > .lbl {
  flex: none;
  width: 58px;
  font: 400 10.5px var(--mono);
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--text-muted);
}

.chips { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }

.chip {
  font: 400 11.5px var(--font);
  padding: 2px 9px;
  border-radius: 999px;
  cursor: pointer;
  user-select: none;
  border: 1px solid var(--border);
  background: var(--surface-sunk);
  color: var(--text-muted);
  transition: background .12s, color .12s, border-color .12s;
}
.chip:hover { border-color: var(--text-muted); color: var(--text-dim); }
.chip.on { background: var(--surface-2); color: var(--text); border-color: var(--border); }
/* Team events are a different sort of thing, not just another level: no
   individual position, so they are dashed and off by default. */
.chip.team { border-style: dashed; }
.chip.team.on { border-color: var(--accent); color: var(--accent-text); }
.chip.more { border-style: dotted; }

/* **A chosen chip is filled**, on every bar that has one.
   The neutral "on" was a slightly lighter pill: it said "this one" but gave no
   hint the choice *was* a choice, so a fold or a filter read as a caption rather
   than a control. One rule for the pickers in the bracket — exactly one draw,
   exactly one round — and for the filters on Seasons and Compare, where many are
   on at once.

   ⚠️ These two really are different kinds of control, and the filters default
   to **all on**, so this opens those pages on a full row of accent. That was the
   argument for leaving them neutral and it lost to the bigger one: the same
   affordance has to look the same everywhere, or a reader learns the red pill on
   one page and has to learn the grey one again on the next.

   ⚠️ A **team** chip is excluded on purpose. It is not just another level — no
   individual position, off by default — and it stays a dashed outline so that
   "on, but a different sort of thing" survives being surrounded by filled pills.
   Filling it would have hidden the dashed border inside its own colour. */
/* ⚠️ **Every chip, rather than a list of the bars that have one.** This was
   written as five ids — `#tmtDrawPick`, `#tmtRounds`, `#years`, `#levels`,
   `#gridGroups` — so every new chip bar had to remember to join the list, and
   the ranking's sort did not: it was grey for no reason anybody had decided. A
   rule whose own argument is "the same affordance has to look the same
   everywhere" should not be a list to keep up to date. `.seg.on` has always been
   the accent; this makes the pills agree with the segmented buttons.

   ⚠️ **Two exceptions, both deliberate.** A `.team` chip stays a dashed
   outline: it is not just another level — no individual position, off by
   default — and filling it would hide the dashed border inside its own colour.
   And `#tmtDraws`, the day's draw *filter*, stays neutral because it sits
   directly above `#tmtDrawPick`, which is red: a bar of red above a bar of red
   is two controls that look like one. That one was pinned by a check before this
   rule was generalised, which is the only reason generalising it did not quietly
   undo the decision. */
.chip.on:not(.team):not(#tmtDraws .chip) {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-fg);
}
.chip.on:not(.team):not(#tmtDraws .chip):hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
}

button:focus-visible, input:focus-visible, a:focus-visible, li:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- the seasons ---------- */

#status { min-height: 0; margin: 0; color: var(--text-muted); font-size: 12.5px; }
#status.error { color: var(--bad); }

.seasons {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-card);
  padding: 2px 12px;
}
.seasons:empty { display: none; }

/* One row per season, most recent at the top. */
.srow {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 5px 0;
  border-bottom: 1px solid var(--border-soft);
}
.srow:last-child { border-bottom: 0; }

.srow .yr {
  flex: none;
  width: 58px;
  padding-top: 20px;      /* sits level with the boxes, not the name above them */
  font: 500 13px var(--mono);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.srow .yr .cnt { display: block; font-size: 10px; color: var(--text-muted); font-weight: 400; }

.season { display: flex; gap: 4px; overflow-x: auto; padding-bottom: 2px; scrollbar-width: thin; }

.sq { flex: none; width: 52px; text-align: center; text-decoration: none; color: inherit; }

/* Two lines, clamped from the START — clipping the tail of a name is
   recoverable, clipping the head reads as a different tournament. */
.sq .tn {
  display: -webkit-box;
  font-size: 8.5px;
  line-height: 1.15;
  color: var(--text-muted);
  height: 19px;
  margin-bottom: 1px;
  overflow: hidden;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  word-break: break-word;
}
.sq:hover .tn, .sq:hover .lv { color: var(--text); }

/* Fixed slot: every tournament keeps an identical footprint, the box just fills
   less of it, so a lighter event reads as air rather than as a shifted column. */
.slot { display: flex; justify-content: center; align-items: center; }

.box {
  border-radius: var(--radius-btn);
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-weight: 700;
  letter-spacing: -.02em;
  color: var(--text);
  border: 1px solid var(--border-soft);
  overflow: hidden;
  background: linear-gradient(to top,
    var(--fill) 0 var(--pct, 0%), var(--surface-2) var(--pct, 0%) 100%);
}

.sq .lv {
  display: block;
  font: 400 8.5px/1.2 var(--mono);
  color: var(--text-muted);
  margin-top: 2px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* The result ramp: green for a title through to red for a first-round exit.
   The label inside the box says the same thing in words, so nothing here is
   carried by colour alone. Unchanged from the predecessor. */
.box.r-w   { --fill: var(--res-w); }
.box.r-f   { --fill: var(--res-f); }
.box.r-sf  { --fill: var(--res-sf); }
.box.r-qf  { --fill: var(--res-qf); }
.box.r-r16 { --fill: var(--res-r16); }
.box.r-r1  { --fill: var(--res-r1); }
.box.r-q   { --fill: var(--res-q); }
.box.r-na  { --fill: transparent; color: var(--text-muted); }
/* A placing we do not recognise: they were there, so it shows the same minimum
   sliver every other result does, in a colour that claims nothing about how far
   they got. */
.box.r-unk { --fill: var(--res-unk); }
/* White only where the fill reaches the middle of the box, which is where the
   label sits. A first-round exit fills 13%, so its label is on the *unfilled*
   ground and must stay in the ordinary text colour. */
.box.r-w, .box.r-f { color: #fff; }
/* Entered no draw of this discipline: an outline, not a result. */
.box.r-none { --fill: transparent; border-style: dashed; border-color: var(--border); }

.empty { margin: 4px 0 10px; color: var(--text-muted); font-size: 12.5px; max-width: 62ch; }

/* ---------- foot ---------- */

.foot {
  margin-top: 16px;
  padding-top: 11px;
  border-top: 1px solid var(--border-soft);
  color: var(--text-muted);
  font-size: 12px;
  max-width: 74ch;
}
.foot a { color: var(--accent-text); }
.foot p { line-height: 1.6; margin: 0 0 8px; }

details { margin-top: 10px; }
summary { cursor: pointer; font-size: 11.5px; }
pre {
  overflow-x: auto;
  font: 11px/1.45 var(--mono);
  color: var(--text-dim);
  background: var(--surface-sunk);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-btn);
  padding: 10px 12px;
  max-height: 340px;
}

/* ==========================================================================
   The career grid

   The same seasons read the other way round: a row per year, a column per
   tournament, every cell identical and flooded with one colour. It gives up
   everything the strip encodes in size and fill so that the only thing left is
   position and hue — which is what makes a career readable as a block rather
   than as a sequence.

   Cells butt against each other on purpose. Runs of the same colour are meant
   to merge into one shape; the only separators are the hairlines between tiers.
   ========================================================================== */

/* The second page, not a layer over the first. No width cap and no shadow: it
   is the page now, and it sits in the same column as everything else. */
.page { display: block; }

.pagehead {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.pagehead h2 {
  margin: 0;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: .02em;
  color: var(--text-muted);
}
.pagehead .cmp { margin-left: auto; }
.pagehead .cmp input { width: 220px; }
.seg.close { padding: 6px 11px; font-size: 12px; }
a.seg { text-decoration: none; }
.kindwrap { display: inline-flex; }

/* Both grids live in one scroller so they scroll together — two independently
   scrolling grids side by side stop being a comparison the moment either one
   moves. */
.gridbody {
  display: flex;
  gap: 30px;
  align-items: flex-start;
  overflow-x: auto;
  padding-bottom: 8px;
  scrollbar-width: thin;

  /* Set from JS, and the same in both grids: the position in the row is what
     encodes difficulty, never the size. The zoom slider moves this and nothing
     else, so every cell stays identical to every other at any setting. */
  --cell: 20px;
  --gy: 36px;        /* the year gutter, which the tier band has to clear too */
}
.gcard {
  flex: none;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-card);
  padding: 9px 12px 11px;
}

.gprofile { display: flex; align-items: center; gap: 11px; margin-bottom: 9px; }
.gprofile .avatar { width: 40px; height: 40px; }
.gprofile .who { font-size: 18px; }
.gprofile .meta { font-size: 12px; }
.gprofile .close { margin-left: 14px; }

/* Which stretch of columns is which tier. The grid itself carries no text, so
   this band and the tooltips are the whole of the orientation. */
.gtiers { display: flex; margin-left: var(--gy); }
.gt {
  width: calc(var(--cell) * var(--n));
  /* Three characters have to fit in one column — see gridGroupCode. Clipped
     rather than wrapped, with the full name and the slot count on a title;
     below about 14px of zoom the one-slot blocks do clip. */
  font: 400 8px/1.5 var(--mono);
  color: var(--text-muted);
  overflow: hidden;
  white-space: nowrap;
  padding-left: 1px;
  border-left: 1px solid var(--border-soft);
}
.gt:first-child { border-left: 0; padding-left: 0; }

.grow { display: flex; align-items: center; }
.gy {
  flex: none;
  width: var(--gy);
  font: 500 10px var(--mono);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.cell {
  flex: none;
  display: block;
  width: var(--cell);
  height: var(--cell);
  background: var(--res-off);
}
/* The tier boundary, drawn into the cell rather than as a gap so the run of
   colour on either side of it stays unbroken. */
.cell.edge { box-shadow: inset 1px 0 0 var(--bg); }
.cell:hover { position: relative; z-index: 1; outline: 1px solid var(--text); outline-offset: -1px; }

.cell.r-w   { background: var(--res-w); }
.cell.r-f   { background: var(--res-f); }
.cell.r-sf  { background: var(--res-sf); }
.cell.r-qf  { background: var(--res-qf); }
.cell.r-r16 { background: var(--res-r16); }
.cell.r-r1  { background: var(--res-r1); }
.cell.r-q   { background: var(--res-q); }
.cell.r-unk { background: var(--res-unk); }
.cell.r-na  { background: var(--res-na); }
.cell.r-off { background: var(--res-off); }

/* A win says so, and not only in green.
   ⚠️ --res-w (#1a7f37) and --res-f (#3fa34d) are one step apart on the ramp,
   and a won square next to a lost final is two shades of the same colour. For
   a reader with red-green colour vision deficiency the ramp carries almost no
   information at all. The mark is redundant coding — the same fact said twice,
   once in colour and once in text — so the board still reads with the colour
   ignored entirely. */
.cell {
  /* Whichever number is this view's square size. The grid sets --cell on the
     body; an honours row sets --hs per row, because every row has its own. */
  --sq: var(--cell, 20px);
  container-type: inline-size;
}
.honours .cell { --sq: var(--hs); }

/* ⚠️ Gated on the square being big enough to read. Below this the glyphs stop
   being a word and become a smudge, which reads as dirt on the square rather
   than as a result — worse than the colour alone. The zoom slider is the way
   up, and the honours board's small rows are exactly where it is needed. */
@container (min-width: 16px) {
  .cell.r-w::after {
    content: "#1";
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    font: 700 calc(var(--sq) * .44)/1 var(--mono);
    letter-spacing: -.04em;
    color: #000;
  }
}

/* A result from before the World Tour, drawn on the tier it maps to.
   A notch cut out of the top-right corner rather than a mark laid on top: it
   costs no colour, survives every tier in the ramp, and works at 8px as well as
   at 60. Placed after the `.cell.r-*` rules on purpose — those set `background`
   as a shorthand, which resets `background-image`, so this longhand has to come
   later to survive. */
.cell.mapped {
  /* `clip-path`, not a gradient. The grid has a standing rule that a cell is
     never partially filled — how full it is has to mean nothing, or the eye
     starts reading it as a gauge the way the strip's squares are — and the
     suite enforces that by checking no cell carries a gradient. So the corner is
     genuinely cut away rather than painted over. */
  clip-path: polygon(0 0, calc(100% - var(--notch, 3px)) 0,
                     100% var(--notch, 3px), 100% 100%, 0 100%);
}
.honours .cell.mapped { --notch: max(2px, calc(var(--hs) * .17)); }
.key .cell.mapped { --notch: 4px; }

.gnote { margin: 8px 0 0; color: var(--text-muted); font-size: 12.5px; }
.gnote.error { color: var(--bad); }

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 15px;
  align-items: center;
  margin: 11px 0 0;
  color: var(--text-muted);
  font-size: 11.5px;
}
.legend.note { display: block; max-width: 88ch; line-height: 1.55; margin-top: 6px; }

.zoom {
  display: inline-flex; align-items: center; gap: 7px;
  font: 400 10.5px var(--mono); text-transform: uppercase; letter-spacing: .09em;
  color: var(--text-muted);
}
.zoom input[type=range] { width: 96px; accent-color: var(--accent); cursor: pointer; }
.key { display: inline-flex; align-items: center; gap: 5px; }
.key .cell { width: 11px; height: 11px; border-radius: 2px; }

/* ========================== the honours board ==========================

   One row per level, hardest at the top, and only the results that cleared the
   bar. Nothing is on a shared grid: each row has its own pitch, because the
   size of a square is what says how much the result was worth.

   `--k` comes from the model as a bare multiplier — φ per row, applied to area,
   so the sides go up by √φ ≈ 1.272 and the top row is 8.7 times the bottom one.
   The zoom slider moves `--hbase` and every row follows without a re-render.
   ========================================================================== */

.honours {
  --hbase: 8px;         /* the smallest row; every other row multiplies it */
  --hlvlw: 84px;        /* the label gutter, and the spine runs down its middle */
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-card);
  padding: 11px 13px 15px;
  overflow-x: auto;
  scrollbar-width: thin;
}

/* The board proper, inside the scroller.
   `--halfw` is set from JS as the widest half any row needs, in units of
   `--hbase` — so it survives the zoom slider without a re-render, and both
   halves are the same width whichever player is longer. `minmax(halfw, 1fr)`
   is the whole trick: never narrower than the results need, so nothing is ever
   silently cut off, and wider when there is room, so a short board still
   centres. */
.hboard {
  position: relative;
  /* Stated rather than `max-content`: the profiles are much wider than a row of
     squares, and letting them size the board would stretch every row to fit a
     name. `min-width: 100%` still lets it fill the page when there is room. */
  width: calc(var(--halfw) + var(--hlvlw));
  min-width: 100%;
}
.honours.two .hboard { width: calc(var(--halfw) * 2 + var(--hlvlw)); }

/* The centre line, drawn once for the whole board rather than per row: it is an
   axis, and an axis that restarts at every row is a row of ticks. It hangs off
   the board and not the scroller, so it stays true when the board is wider than
   the modal and has to scroll. */
.honours.two .hboard::before {
  content: '';
  position: absolute;
  top: 9px;
  bottom: 9px;
  left: 50%;
  width: 1px;
  background: var(--border);
}

/* Always three columns — player, gutter, player-or-empty-seat — even with one
   career on screen, because the seat is the point of the page. The rows below
   stay centred until somebody is actually sitting in it. */
.hheads {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--hlvlw) minmax(0, 1fr);
  margin-bottom: 13px;
}
.hrow {
  display: grid;
  grid-template-columns: var(--hlvlw) minmax(var(--halfw), 1fr);
  align-items: center;
  margin-bottom: 5px;
  /* Every size in the row derives from this one number. */
  --hs: calc(var(--hbase) * var(--k));
}
.honours.two .hrow {
  grid-template-columns: minmax(var(--halfw), 1fr) var(--hlvlw) minmax(var(--halfw), 1fr);
}

/* The left-hand player is mirrored — profile and results both — so that the two
   boards face each other across the line instead of pointing the same way. */
.hhead.mirror .gprofile { flex-direction: row-reverse; }
.hhead.mirror .ident,
.hhead.mirror .who { text-align: right; }
.hhead.mirror .meta { flex-direction: row-reverse; }
.hhead.mirror .gprofile .close { margin: 0 0 0 0; margin-right: 14px; }

.hlvl {
  position: relative;
  z-index: 1;
  font: 400 10px/1.4 var(--mono);
  color: var(--text-muted);
  letter-spacing: .02em;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  /* Opaque, so it masks the spine rather than sitting on top of it. */
  background: var(--surface);
  padding: 3px 0;
}
.honours:not(.two) .hlvl { text-align: left; }
.hlvl.spacer { background: none; }

.hside {
  display: flex;
  align-items: center;
  /* Strictly proportional, with no floor: `--halfw` is computed from it, and a
     max() would make the width of a row a different function at the bottom of
     the board than at the top. */
  gap: calc(var(--hs) * .09);
  min-width: 0;
  /* Only ever fires on a career long enough to overflow half the modal, and
     when it does the mirroring means it is the *worst* results that go — the
     ones nearest the outer edge. */
  overflow: hidden;
}
.hside.mirror { justify-content: flex-end; }
.honours:not(.two) .hside { justify-content: center; }

/* Square, sized by its row rather than by the grid's flat `--cell`. */
.honours .cell,
.hnone {
  flex: none;
  display: block;
  width: var(--hs);
  height: var(--hs);
  border-radius: max(1px, calc(var(--hs) * .08));
}
.hnone {
  border: 1px dashed var(--border);
  background: none;
  opacity: .5;
}

/* On the inside of the row, next to the label: it is the one number worth
   having, and out at the far end it would be the first thing clipped. */
.hn {
  flex: none;
  /* Fixed, because `--halfw` adds it as a constant. */
  width: 26px;
  text-align: center;
  font: 500 10px var(--mono);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.key .hnone { width: 11px; height: 11px; border-radius: 2px; }

/* Controls that only mean something on the strip. "Size by weight" governs a
   square the compare page does not draw, and the discipline has its own toggle
   over there. */
body:not([data-page="seasons"]) .seasonsonly { display: none; }

/* The empty seat.

   A page called Compare should look like one before anybody has been chosen.
   This used to be a search box in the header labelled "Compare with…", which is
   a perfectly clear label that nobody read, because nothing on the page
   suggested there was a second half to fill. */
.addslot {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  width: 100%;
  min-height: 96px;
  padding: 18px 22px;
  background: none;
  border: 1px dashed var(--border);
  border-radius: var(--radius-card);
  color: var(--text-muted);
  font: 400 13px/1.4 var(--font);
  cursor: pointer;
  transition: border-color .12s, color .12s;
}
.addslot:hover { border-color: var(--accent); color: var(--text-dim); }
.addslot .plus { font: 300 22px/1 var(--font); color: var(--text-dim); }
.addslot .say { font-weight: 500; }
.addslot .hint { font-size: 11.5px; opacity: .8; }

.gcard.empty {
  display: flex;
  /* Level with the profile it sits beside, not floating in the middle of a
     fifteen-season card. */
  align-items: flex-start;
  background: none;
  border: 0;
  padding: 9px 0 0;
  min-width: 260px;
  align-self: stretch;
}
.hhead.empty { display: flex; align-items: center; }
.hhead.empty .addslot { min-height: 62px; padding: 10px 18px; }

/* ============================ the page nav ============================

   Two pages, and the bar says so. This started life as a segmented control in
   the top-right corner of the hero, which is where a setting goes — a toggle
   that size reads as "an option about the thing you are looking at", not as
   "the other half of the application". Whatever it said, it was going to be
   read as a switch.
   ========================================================================== */

.pagenav {
  display: flex;
  gap: 9px;
  margin: 0 0 16px;
}

/* Outlined in BWF red, both of them, so the bar reads as the two halves of the
   application rather than as a heading with an underline under one word. The
   one you are on is filled; the one you are not is the same shape waiting. */
.tab {
  flex: none;
  min-width: 210px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 10px 22px;
  background: none;
  border: 1px solid var(--accent);
  border-radius: var(--radius-card);
  color: var(--text-dim);
  cursor: pointer;
  text-align: left;
  transition: color .12s, background .12s;
}
.tab .name {
  font: 500 16px/1.2 var(--font);
  letter-spacing: .01em;
}
.tab .sub {
  font: 400 11.5px/1.2 var(--font);
  opacity: .8;
}

.tab:hover { background: rgba(223, 32, 39, .13); color: var(--text); }
.tab.on {
  background: var(--accent);
  color: var(--accent-fg);
}
.tab.on:hover { background: var(--accent-hover); }
/* Not the accent: a red ring 2px outside a red border is no ring at all. */
.tab:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }

@media (max-width: 640px) {
  .tab { min-width: 0; flex: 1; padding: 9px 12px; }
  .tab .sub { display: none; }
}

/* ========================== the tournament page ==========================

   The predecessor's order-of-play layout, which is the right one.

   A **real grid**: one column per court, one row per position on that court, so
   two cards on the same row are genuinely at the same point in the day. The
   y-axis is the running order and never the clock — badminton matches follow one
   another on a court, BWF says so itself with "Followed by", and the per-match
   times it publishes are flat 50-minute estimates that on some courts run
   backwards (Part 4.7).

   Below the breakpoint the grid is dropped entirely and the cards stack in DOM
   order, which is row-major — i.e. still down the day.
   ========================================================================== */

.tmtwhen {
  font: 400 12px var(--mono);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.badge {
  font: 500 10.5px var(--mono);
  text-transform: uppercase;
  letter-spacing: .09em;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--text-muted);
}
.badge.live { border-color: var(--accent); color: var(--accent-text); }
.badge.upcoming { border-color: var(--border); color: var(--text-dim); }

/* ---- the day bar ---- */

.daybar { display: flex; gap: 6px; flex-wrap: wrap; }
.day {
  appearance: none;
  flex: none;
  text-align: center;
  background: var(--surface);
  color: var(--text-dim);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-btn);
  padding: 5px 13px;
  font: inherit;
  font-size: 12px;
  line-height: 1.25;
  cursor: pointer;
}
.day b { display: block; font-size: 14px; font-weight: 700; color: var(--text); }
.day:hover { border-color: var(--accent); }
.day.is-today { border-color: var(--text-muted); }
.day.is-active { background: var(--accent); border-color: var(--accent); }
.day.is-active, .day.is-active b { color: var(--accent-fg); }

/* ---- the grid ---- */

.oop-grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 4), minmax(0, 1fr));
  gap: 8px;
  align-items: start;
}
.oop-head {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 5px 10px;
  background: var(--surface-sunk);
  border: 1px solid var(--border-soft);
  border-bottom: 2px solid var(--accent);
  border-radius: var(--radius-btn) var(--radius-btn) 0 0;
  font: 700 11px var(--font);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-dim);
}
.oop-list { display: grid; gap: 6px; max-width: 560px; }

/* The column already names the court. */
.oop-grid .match-head .court,
.oop-grid .match-head .sep.court { display: none; }
/* A finished match carries up to five game badges, and at a quarter of the
   width they were squeezing the name onto a second line. Tighter here only. */
.oop-grid .sets { gap: 2px; }
.oop-grid .sets b { min-width: 18px; font-size: 12px; padding: 1px 2px; }

@media (max-width: 1100px) {
  .oop-grid { display: block; }
  .oop-head { display: none; }
  .oop-cell { margin-bottom: 6px; }
  .oop-grid .match-head .court,
  .oop-grid .match-head .sep.court { display: inline; }
}

/* Keyboard hints. Every shortcut drives a control that is already visible, so
   the hint belongs with the note that explains that control rather than in a
   help panel of its own — a shortcut nobody can find is not a feature. */
.keys {
  display: block;
  margin-top: 6px;
  color: var(--text-muted);
  font-size: 11.5px;
}
.keys kbd {
  display: inline-block;
  min-width: 15px;
  padding: 0 4px;
  margin: 0 1px;
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: var(--surface-2);
  color: var(--text-dim);
  font: 400 10.5px var(--mono);
  text-align: center;
}
@media (max-width: 720px) { .keys { display: none; } }

/* ========================== the bracket ==========================

   One absolutely-positioned canvas inside a scroller. The positions come from
   `bracketLayout`, which is arithmetic and lives in the model; nothing here
   knows the spacing law.

   ⚠️ **Native scrolling, deliberately — no custom pan.** The predecessor drove
   this with pointer events and recorded two traps it cost it: `setPointerCapture`
   retargets the follow-up `click` to the capturing element, so cards never
   received it, and `preventDefault()` on `pointerdown` can suppress the
   compatibility click entirely. It also needed a capture-phase click swallower
   so that releasing a drag did not open the card underneath. All of that exists
   to make dragging and clicking coexist. Here the fold does the work custom
   panning was there to do — from the quarter-finals the rest of the tournament
   is 654 x 282 and fits with no scrolling at all — so the scroller is the
   browser's, the click is an ordinary click, and none of those traps can
   return. */

.brwrap {
  overflow: auto;
  max-height: min(72vh, 900px);
  background: var(--surface-sunk);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-card);
  scrollbar-width: thin;
}
.brcanvas { position: relative; }

.bcol {
  position: absolute;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: center;
}

/* Hairlines. A 1px box rather than an SVG path: there are at most 124 of them
   and they are all axis-aligned, so a div is cheaper than a path and lands on
   the same device pixel at every zoom. */
.bline { position: absolute; background: var(--border); }

.bcard {
  position: absolute;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-card);
  cursor: pointer;
}
.bcard.is-upcoming { border-left-color: var(--border); }
.bcard.is-live { border-left-color: var(--live); }
/* A bye is not a fixture, and must not read as one waiting to be played. */
.bcard.is-bye { border-style: dashed; border-left-color: var(--border); cursor: default; }

.bside {
  display: grid;
  grid-template-columns: 15px 22px 1fr auto;
  align-items: center;
  gap: 5px;
  padding: 2px 6px;
  min-width: 0;
}
.bside + .bside { border-top: 1px solid var(--border-soft); }
.bside .flag { width: 15px; height: 15px; border-radius: 50%; object-fit: cover; }
.bside .seed { font-size: 9.5px; color: var(--text-muted); text-align: right; }
.bside .bn {
  min-width: 0;
  font-size: 11.5px;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bside .bn .muted { color: var(--text-muted); }
.bside.is-winner .bn { font-weight: 700; }
.bside.is-loser .bn, .bside.is-loser .bsc { opacity: .5; }
.bside .bsc { display: flex; gap: 2px; }
.bside .bsc b {
  min-width: 14px;
  padding: 0 2px;
  border-radius: 3px;
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: 10px;
  text-align: center;
  font-weight: 400;
}
.bside .bsc b.won { background: var(--accent); color: var(--accent-fg); }
.bside .bsc b.mk { background: transparent; color: var(--bad); font-weight: 700; }

/* Same star, same meaning as the order of play. */
.bcard.is-dim { opacity: .45; border-left-color: var(--border); }
.bcard.is-dim:hover { opacity: .9; }
.bcard.is-starred { box-shadow: 0 0 0 1px var(--accent); }
.bcard.is-starred .bside .bn { color: var(--accent-text); }
.bcard.is-fresh { border-color: var(--live); }

.chiprow .lbl.from { margin-left: 14px; white-space: nowrap; }

/* ---- the card: BWF's reading order — flag, seed, name, per-game scores ---- */

.match {
  background: var(--surface);
  border: 1px solid var(--border-soft);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-card);
  overflow: hidden;
}
.match.is-upcoming { border-left-color: var(--border); }
.match.is-live { border-left-color: var(--live); }

.match-head {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
  padding: 3px 10px;
  background: var(--surface-sunk);
  border-bottom: 1px solid var(--border-soft);
  font-size: 10.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.match-head .rnd { font-weight: 700; color: var(--accent-text); }
.match-head .sep { opacity: .45; }
.match-head .stat { margin-left: auto; font-weight: 700; }
.stat.finished { color: var(--text-muted); }
.stat.finished.is-note { color: var(--bad); }
.stat.live { color: var(--live); }
.stat.upcoming { color: var(--warn); }

.side {
  display: grid;
  grid-template-columns: 19px 28px 1fr auto;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
}
.side + .side { border-top: 1px solid var(--border-soft); }
.side.is-loser .nm, .side.is-loser .sets { opacity: .55; }
.side.is-winner .nm { font-weight: 700; }
.side .flag { width: 19px; height: 19px; border-radius: 50%; object-fit: cover; }
.side .seed {
  font-size: 11px;
  font-weight: 700;
  color: var(--text-muted);
  text-align: center;
}
/* Deliberately allowed to wrap. In a four-column grid a doubles pair is wider
   than the column, and a second line costs a few pixels where an ellipsis hides
   half the pair. `anywhere` because some single names are themselves wider than
   the column with no space to break at. */
.nm { min-width: 0; font-size: 13px; line-height: 1.25; overflow-wrap: anywhere; }
.nm .sub { font-size: 10.5px; color: var(--text-muted); margin-left: 5px; }
.nm .muted { color: var(--text-muted); }

.sets { display: flex; gap: 3px; }
.sets b {
  min-width: 23px;
  text-align: center;
  font: 700 13px var(--mono);
  padding: 1px 3px;
  border-radius: 2px;
  background: var(--surface-2);
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
.sets b.won { background: var(--accent); color: var(--accent-fg); }
/* Not styled as a score: it is an outcome, not a number, and on a walkover it
   is the only thing in the row. Outlined so it never competes with a game. */
.sets b.mk {
  background: none;
  border: 1px solid var(--bad);
  color: var(--bad);
  font-size: 10px;
  letter-spacing: .05em;
  min-width: 0;
  padding: 0 5px;
}

.match-foot {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  padding: 3px 10px;
  border-top: 1px solid var(--border-soft);
  font: 400 11px var(--mono);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.match-foot .oopt { opacity: .8; }

/* ---- Follow Matches: dim the day, light up what you starred ----

   The whole day is on screen at once, so the default state has to recede far
   enough that a handful of picked-out cards read at a glance from across the
   room. Dimming everything is what makes a star worth anything. */

.tmtbar { align-items: center; }
.tmtbar .stars {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 12px;
}
.starcount { font: 400 11.5px var(--mono); color: var(--text-muted); }
.seg:disabled { opacity: .4; cursor: default; }

.match { cursor: pointer; }
.match.is-dim { opacity: .45; border-left-color: var(--border); }
.match.is-dim:hover { opacity: .85; border-left-color: var(--accent); }
.match.is-starred { box-shadow: 0 0 0 1px var(--accent); }
.match.is-starred .match-head { background: var(--accent); color: var(--accent-fg); }
.match.is-starred .match-head .rnd,
.match.is-starred .match-head .stat,
.match.is-starred .star { color: var(--accent-fg); }
.match.is-starred .match-head .sep { opacity: .6; }

.star { font-size: 12px; color: var(--text-muted); line-height: 1; }
.match.is-dim:hover .star { color: var(--accent-text); }

/* Marked when the score moved under you on a check, so coming back to the tab
   shows what changed rather than just a differently-filled page. */
.match.is-fresh { border-color: var(--live); }
.match.is-fresh .match-head::after {
  content: '';
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--live);
  margin-left: 4px;
}

.match-foot .local { color: var(--accent-text); }
.seg.is-live { border-color: var(--live); color: var(--live); }

/* ---- one heading per day, when every day is on screen ---- */

.daygroup { margin-bottom: 18px; }
.daygroup-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 8px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--border-soft);
}
.daygroup-head h3 { margin: 0; font-size: 14px; font-weight: 500; color: var(--text); }
.daygroup-head span { font: 400 11.5px var(--mono); color: var(--text-muted); }
.daygroup-head b { color: var(--accent-text); }

/* ============================ the winners' pyramid ============================ */

.pyr { overflow-x: auto; overflow-y: hidden; padding-bottom: 6px; }
.pyrscroll { display: flex; align-items: flex-end; gap: 10px; padding: 8px 2px 0; }

/* A faint ground under each season, so twenty pyramids in a row read as twenty
   things rather than one long frieze — the base rows are adjacent and the tiles
   are small, and without this they run together. */
.pyrseason {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 6px 6px 0; border-radius: 4px;
  background: rgba(255, 255, 255, .022);
}
.pyrseason:nth-child(even) { background: rgba(255, 255, 255, .045); }
.pyrstack { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.pyrrow { display: flex; align-items: flex-end; justify-content: center; gap: 3px; }

/* A season that never held this tier keeps its space rather than closing up. */
.pyrrow.is-empty { width: 100%; border-bottom: 1px dashed rgba(255,255,255,.10); }

/* ⚠️ **No inset ring.** There was one on every tile, and one per tier below,
   and not one of them was ever visible: an inset box-shadow paints *behind* the
   element's content, and the content here is a photograph filling the tile. They
   showed for the instant before the images arrived and then vanished — which is
   exactly how it went unnoticed, and how a test that compared the declared
   colour to the export's table passed while the page drew nothing. Rank is said
   by size on this page. The only ring left is the Olympic one, and it is drawn
   *outside*, where a photograph cannot cover it. */
.pyrtile {
  display: block; overflow: hidden; border-radius: 3px;
  background: rgba(255, 255, 255, .06);
}
/* ---- the career view: a result where a photograph goes ----

   The square keeps every bit of its machinery — the gold ring, the fade, the
   displaced tournament's dashed outline — and what fills it is the Compare
   page's own `.cell`, so the two pages cannot drift apart about what green
   means. `--sq` is what the `#1` glyph is sized from. */
.pyrtile.is-cell { background: none; }
.pyrtile .cell { display: block; width: 100%; height: 100%; border-radius: inherit; }

/* ⚠️ **A tournament they did not enter, and it must not read as a defeat.** Not
   `--res-off`, which is the grid's ground and on this page would be a dark square
   indistinguishable from a hole: an outline says "this was on, they were not
   here" without putting a colour from the losing end of the ramp on it. */
.pyrtile .cell.r-none {
  background: none;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .17);
}

.pyrtile .face { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top center; }
.pyrtile .noface {
  display: flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 700; color: var(--dim); letter-spacing: .04em;
}

/* ---- a pair, in one square ----

   ⚠️ The square stays the size the ladder says it is; what changes is that two
   people are in it. A doubles title is won by a partnership, and the board's
   standing rule is that every square means the same thing — so the pair has to
   fit the square rather than the square growing to fit the pair.

   ⚠️ **Each half shows the middle of its photograph.** `object-fit: cover` in a
   box half as wide as it is tall matches the source on height and crops the
   sides, which is the central band; `object-position: top center` then keeps
   the crop off the chin, the same rule the full square follows. `min-width: 0`
   because a flex item will not shrink past its intrinsic width without it, and
   the two photographs would push the square wide. */
.pyrtile .pairface { display: flex; width: 100%; height: 100%; }
.pyrtile .pairface .half {
  width: 50%; height: 100%; min-width: 0;
  object-fit: cover; object-position: top center;
}
/* The seam, so a pair reads as two people rather than as one odd photograph.
   Drawn inside the right-hand half as a shadow, not as a border on either: a
   border would take a pixel out of one face and not the other, and at the
   smallest zoom a Super 750 square is 22px and that is visible. */
.pyrtile .pairface .half + .half { box-shadow: inset 1px 0 0 rgba(0, 0, 0, .55); }
.pyrtile .pairface .noface { font-size: 8px; }

/* The gold ring, and nothing else on the board wears one. The Olympics and the
   Worlds are the same size and share a row, so **the ring is what tells them
   apart** — and it only takes one of them to do it: the Olympic square is the
   marked case and the world champion beside it is the plain one. Ringing both
   made them read as a matched pair rather than a ranking, and the white ring
   was the brighter of the two on a dark ground, which put the wrong one on top.
   Every other tier says its rank by size alone.
   ⚠️ Not `inset`, and not `outline` either: outline is a single property and the
   footnote mark below needs it, so a Tokyo 2020 square — Olympic *and* displaced
   — could only have shown one of the two. */
/* The square that outranks the rest of its line. Every row is drawn at one
   size, so this ring is the ranking: the Olympics over the Worlds, a continental
   games over a continental championships, the Tour Finals over a Super 1000.
   ⚠️ Only ever **one** square in a row wears it. Ringing both made the pair read
   as equals in two liveries, and a white ring on the plain case was the brighter
   of the two against a dark ground, so the world champion came out looking like
   the bigger prize. */
.pyrtile.is-top { box-shadow: 0 0 0 2px #ffd24a; }

.pyryear {
  font-size: 12px; color: var(--dim); font-variant-numeric: tabular-nums;
  border-top: 1px solid rgba(255,255,255,.10); width: 100%; text-align: center; padding-top: 4px;
}

/* The pyramid does not always taper: 2027 holds as many Super 1000s as Super
   750s. Marked rather than hidden — it says the elite tier grew. */
.pyrseason.is-bulging .pyryear { color: var(--warn); }

/* ⚠️ One wrapper around both bands, and the wrapper is what is wider than the
   screen. `.pyr` scrolls, `.pyrwrap` is as wide as the pyramid, and the
   dominance bars are placed against *its* left edge — so the two bands cannot
   drift apart, and one scrollbar moves both. `max-content` rather than a width:
   the columns size themselves off their tiles. */
.pyrwrap { width: max-content; min-width: 100%; }

/* ---- the two badges on the summit row ---- */

/* ⚠️ The badge is balanced by an equal padding on the *right*, so the
   photograph sits in the middle of the column and the mark hangs off its left.
   Without it the badge and the tile were centred as a pair and every summit
   photograph was pushed half a badge to the right of the column it belongs to —
   which is visible the moment you look down the page, because nothing else in
   the column is off-centre. Padding rather than absolute positioning: 2021
   holds two of these side by side, and a mark taken out of the flow would have
   been drawn on top of the Olympic photograph next to it. */
.pyrmajor {
  display: inline-flex; align-items: center; gap: 3px;
  padding-right: calc(var(--badge) + 3px);
}
/* ⚠️ `pyrbadge`, not `badge`: the tournament page's status pill is `.badge`
   and gave both of these a rounded grey box behind them. Found by looking at
   the page, which is the only way a name collision in CSS ever is. */
.pyrmajor .pyrbadge { width: var(--badge); height: auto; flex: none; }
/* ⚠️ The cup is drawn in outline and the rings are five solid strokes, so at
   the same declared width the cup reads as half the mark. It gets the same
   width and a heavier stroke instead of being scaled down to "match". It takes
   the colour the Worlds tile is ringed in, so the badge and the frame around
   the photograph beside it are the same object said twice. */
.pyrmajor .pyrbadge.cup { color: #e8e8e8; stroke-width: 2.1; }

/* ---- a title drawn somewhere other than where it was played ---- */

/* ⚠️ A **dashed** outline, not a gold ring. It was a gold one, and gold is now
   what marks an Olympic champion at the summit — a footnote and a tier cannot
   share a colour. Dashed says "annotated" at any zoom, and it sits outside the
   tile where the solid tier rings sit inside, so the two can never be read as
   the same kind of mark. */
/* Offset clear of the gold ring, so Tokyo 2020 wears both without them touching. */
.pyrtile.is-moved { outline: 1px dashed rgba(255, 255, 255, .8); outline-offset: 2px; }
/* ---- picked out, and everybody else pushed back ----

   A doubles board is two photographs per square and several hundred squares,
   and following one partnership across it by eye is genuinely hard — which is
   what this is for. Click a square and the rest recede.

   ⚠️ **The photograph fades; the square does not.** Dimming the whole tile was
   the obvious way and it deletes the board: `.pyrtile` carries the faint white
   ground that draws the pyramid's silhouette, so at 16% the shape of the season
   — which is the entire point of the view — went with it. Fading only what is
   *inside* leaves twenty grey pyramids standing with the picked faces lit in
   them, which is a far more useful picture than three faces in the dark.

   ⚠️ The tier marks fade too. A full-strength gold ring or a dashed footnote on
   a square whose face has receded reads as the mark being the thing selected. */
.pyrtile { cursor: pointer; }
.pyrtile.faded .face,
.pyrtile.faded .noface { opacity: .16; }
.pyrmajor.faded .pyrbadge { opacity: .16; }
.pyrtile.is-top.faded { box-shadow: 0 0 0 2px rgba(255, 210, 74, .18); }

/* The career board is not clickable: the pick is what chose the subject, so a
   square that re-picked on click would fight the menu above it. */
.pyrtile.is-cell { cursor: default; }

/* The competitor menu, sized to sit in the header beside the chips. */
.whopick {
  background: var(--surface-sunk); color: var(--text-muted);
  border: 1px solid var(--border); border-radius: var(--radius-btn);
  padding: 6px 10px; font: 400 13px var(--font);
  max-width: 240px;
}
.pyrtile.is-moved.faded { outline-color: rgba(255, 255, 255, .16); }
/* Not the whole bar: the run's block of colour is the era band's silhouette in
   the same way the squares are the pyramid's, so it stays and its face, name
   and flag are what recede. */
.erabar { cursor: pointer; }
.erabar.faded .erafill { opacity: .3; }
.erabar.faded .erawho { opacity: .22; }

.pyryear .ast { font-style: normal; color: var(--warn); margin-left: 2px; }
.pyryear.is-moved { color: var(--text-dim); }

/* ---- the export picker ---- */

.exportbar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 8px 0 2px; font-size: 12px; color: var(--text-dim);
}
.exportbar select {
  background: var(--surface-2); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--radius-btn); padding: 3px 6px; font: inherit;
  font-variant-numeric: tabular-nums;
}
.exportbar #expNote { color: var(--text-muted); }
.exportbar #expNote.is-bad { color: var(--bad); }

/* ============================ the dominance band ============================

   Bars under the pyramid, one run of seasons each, placed at pixel offsets
   measured off the columns above them. Absolute rather than a grid: the columns
   are content-sized and every one of them is a different width, so there is no
   track list to write down. */

.eraband { position: relative; margin: 10px 0 4px; }
.eraband .empty { margin: 4px 2px; }

/* ⚠️ **Not `overflow: hidden`.** It was, and it silently broke the sticky name
   inside: an element with a clipped overflow is a scroll container, and a
   `position: sticky` child inside one sticks to a box that never scrolls, which
   is to say it does nothing. `.erafill` does the clipping instead. */
.erabar {
  position: absolute; height: 26px;
  display: flex; align-items: center;
  border-radius: 4px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .13);
  font-size: 11px; color: var(--text);
}

.erafill {
  position: absolute; inset: 0; overflow: hidden; border-radius: 4px;
  background: rgba(255, 255, 255, .04);
}

/* One block per season inside the run, in the player's colour and shaded by how
   many titles that season held — a flat fill would claim LEE Chong Wei's
   three-title 2008 and his seven-title 2013 were the same season.
   ⚠️ Every block but the last runs to where the *next* column starts, not to
   its own right edge: the columns are 10px apart and stopping at the edge left
   a dark stripe at every year boundary, so a decade read as ten bars. */
.erayr { position: absolute; top: 0; bottom: 0; background: var(--era); }

/* ⚠️ Sticky, so the name travels with the scroll and is readable for the whole
   of a ten-season run rather than only while 2007 is on screen. Clamped to its
   own bar, so it never wanders onto somebody else's. The dark pill is what
   keeps it legible as it slides across eight different colours at six different
   strengths. */
.erawho {
  position: sticky; left: 3px; z-index: 1;
  display: flex; align-items: center; gap: 5px;
  /* ⚠️ Clamped to the bar it belongs to. `.erabar` cannot clip — that would
     break the sticky above — so a long name in a one-season bar has to be
     stopped here or it hangs over the neighbouring lane. */
  max-width: calc(100% - 6px); min-width: 0;
  margin: 0 3px; padding: 2px 6px 2px 2px; border-radius: 11px;
  background: rgba(16, 16, 16, .55);
  font-weight: 600; white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .9);
}
.erawho b { overflow: hidden; text-overflow: ellipsis; font-weight: 600; }
.erawho .face {
  width: 18px; height: 18px; border-radius: 50%; object-fit: cover;
  object-position: top center; flex: none;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .5);
}
.erawho .noface { font-size: 8px; }
/* A pair's bar carries the pair, split the way its squares are. */
.erawho .pairface {
  width: 18px; height: 18px; border-radius: 50%; overflow: hidden; flex: none;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .5);
}
/* Smaller than the photograph beside it: the flag says which country, and a
   circle the same size as a face competes with it for the same job. */
.erawho .flag { width: 14px; height: 14px; }

/* ============================ the domination score ============================

   The Winners page's second view. One line per career, a face at every season
   they won something, and the height is how much of that season they took.

   ⚠️ The chart is a `viewBox` with no fixed width, so it fits whatever the page
   gives it and the geometry in `app.js` never has to know about a resize. The
   board beside it scrolls sideways and this deliberately does not — which is the
   whole reason the two are views rather than one page.
   ==================================================================== */

.chartwrap { position: relative; margin-top: 8px; }
#scoreChart {
  display: block; width: 100%; height: auto;
  background: var(--surface-sunk);
  border: 1px solid var(--border-soft); border-radius: 6px;
}
.scoregrid line { stroke: rgba(255, 255, 255, .07); }
.scoreaxis text { fill: var(--text-muted); font: 400 10px var(--mono); }
/* The footnote mark, and everything wearing it. */
.scoreaxis .ast { fill: var(--warn); font-size: 13px; }
.scorehole { fill: rgba(255, 188, 32, .07); }
.scorewhy { fill: var(--warn); font: 400 10px var(--mono); opacity: .85; }

.scorestrip .sz { fill: rgba(255, 255, 255, .14); }
.scorestrip .sz.is-marked { fill: var(--warn); opacity: .75; }
.scorestrip .szn { fill: var(--text-muted); font: 400 9.5px var(--mono); }
.scorestrip .szn.is-marked { fill: var(--warn); }
.scorestrip .szlbl { fill: var(--text-muted); font: 400 9px var(--mono); }

#scoreChart path.series {
  fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round;
}
/* A leg that touches a short season. A solid line across 2020 asserts a trend
   through a year that was barely played. */
#scoreChart path.series.over-hole { stroke-dasharray: 4 4; }
#scoreChart .pt .ring { stroke-width: 2; }
#scoreChart .pt.on-hole .ring { stroke-width: 2.5; }
#scoreChart .pt.on-hole { filter: drop-shadow(0 0 3px rgba(255, 188, 32, .9)); }
#scoreChart .pt image { pointer-events: none; }
/* Pushed back for the length of a hover. */
#scoreChart .dim { opacity: .13; }
/* Pushed back until the pin is let go — see `drawScore` for why these are dimmed
   rather than removed. */
#scoreChart .faded { opacity: .16; }
#scoreChart .faded .ring { stroke-width: 1.5; }
#scoreChart .hit { fill: transparent; cursor: pointer; }

#scoreTip {
  position: absolute; pointer-events: none; z-index: 5;
  min-width: 190px; max-width: 300px;
  background: #101010ee; border: 1px solid var(--border); border-radius: 6px;
  padding: 8px 10px; font-size: 12px; box-shadow: 0 6px 22px #0009;
}
#scoreTip .who { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; }
#scoreTip img { width: 26px; height: 26px; border-radius: 50%; object-fit: cover;
  object-position: top center; }
#scoreTip .big { font: 700 16px var(--mono); }
#scoreTip .k { color: var(--text-muted); display: block; }
#scoreTip .big + .k { display: inline; }
#scoreTip ul { margin: 5px 0 0; padding-left: 15px; color: var(--text-dim); }
#scoreTip li { font-size: 11.5px; }

/* One chip per name the bar admits — never fewer, whatever is pinned. */
.scorelegend { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px; }
.scorelegend .lg {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  border: 1px solid var(--border); background: var(--surface-sunk);
  border-radius: 999px; padding: 2px 10px 2px 3px;
  font: 400 11.5px var(--font); color: var(--text-dim);
}
.scorelegend .lg:hover { border-color: var(--text-muted); }
.scorelegend .lg img {
  width: 18px; height: 18px; border-radius: 50%;
  object-fit: cover; object-position: top center;
}
.scorelegend .lg i { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
/* The same split as the board's square, at thumbnail size — on a legend chip
   and in the hover, so a pair is recognisably the same competitor in all four
   places it appears. */
.pairpic {
  display: inline-flex; width: 18px; height: 18px; flex: none;
  border-radius: 50%; overflow: hidden;
}
.pairpic img {
  width: 50%; height: 100%; min-width: 0;
  object-fit: cover; object-position: top center;
}
.pairpic img + img, .pairpic .blank + img, .pairpic img + .blank {
  box-shadow: inset 1px 0 0 rgba(0, 0, 0, .55);
}
/* The half of a pair BWF has no photograph of. It holds its place rather than
   letting the other face fill the circle — see `legendFace`. */
.pairpic .blank { width: 50%; height: 100%; background: rgba(255, 255, 255, .1); }
#scoreTip .pairpic { width: 26px; height: 26px; }
.scorelegend .lg.off { opacity: .35; }
.scorelegend .lg .pk { font: 400 10px var(--mono); color: var(--text-muted); }

.scorecols { display: flex; gap: 40px; flex-wrap: wrap; align-items: flex-start;
  margin-top: 18px; }
.scorecols h3 {
  font: 600 11px var(--mono); letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-muted); margin: 0 0 6px;
}
.scorecols table { border-collapse: collapse; font-size: 12px; }
.scorecols th, .scorecols td { padding: 3px 12px 3px 0; text-align: left; white-space: nowrap; }
.scorecols th {
  color: var(--text-muted); font: 600 10.5px var(--font);
  letter-spacing: .06em; text-transform: uppercase;
}
.scorecols td.n { font-family: var(--mono); font-variant-numeric: tabular-nums; }
/* ⚠️ **Dimmed, not coloured.** These rows were amber, which is this palette's
   attention colour — so the one season nobody should read at face value was the
   brightest line in the table, and Anders ANTONSEN's two titles out of three
   looked like the find of the chart. A footnote should recede. The mark stays
   lit, because a mark is a pointer and not a highlight. */
.scorecols tr.thin td { color: var(--text-muted); }
.scorecols tr.thin td.n { color: #7a7a7a; }
.scorecols .ast, #scoreNote .ast { font-style: normal; color: var(--warn); margin-left: 2px; }

/* ---- the dominators, ranked ----

   ⚠️ Both numbers are always drawn and only the *sort* moves. The two orderings
   disagree — KIM / SEO are first on peak and eighth on total — and that
   disagreement is the thing worth seeing, which a table that showed one column
   at a time would have hidden. The sorted column is marked instead. */
.ranks { margin-top: 22px; }
.ranks .chiprow { align-items: baseline; gap: 10px; flex-wrap: wrap; }
/* ⚠️ `.chiprow > .lbl` is a **fixed 58px** with `flex: none` — a tidy column for
   the one-word labels every other chip row has ("Levels", "Day", "Draws"). This
   one is a sentence, so it sizes itself; left at 58 it overflowed its box and
   printed straight over the link beside it, and `white-space: nowrap` on its
   own only made the overlap longer. */
.ranks .chiprow > .lbl { width: auto; flex: 0 1 auto; }
.ranks .chiprow .lbl, .ranks .chiprow .linkish { white-space: nowrap; }
.ranks h3 {
  font: 600 11px var(--mono); letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-muted); margin: 0;
}
.ranks .lbl { text-transform: none; letter-spacing: 0; }
.linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: 400 11px var(--font); color: var(--text-muted);
  text-decoration: underline; text-underline-offset: 2px;
}
.linkish:hover { color: var(--text); }

.ranktable { border-collapse: collapse; font-size: 12px; margin-top: 8px; }
.ranktable th, .ranktable td { padding: 3px 14px 3px 0; text-align: left; white-space: nowrap; }
.ranktable th {
  color: var(--text-muted); font: 600 10.5px var(--font);
  letter-spacing: .06em; text-transform: uppercase;
}
.ranktable td.n, .ranktable th.n { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.ranktable th.n, .ranktable td.n { text-align: right; }
/* The column the table is in the order of. Marked rather than moved, so the
   reader can read the other one off the same row. */
.ranktable th.by { color: var(--text); }
.ranktable td.by { color: var(--text); font-weight: 600; }
.ranktable td:not(.by).n { color: var(--text-muted); }
.ranktable td.rk { color: var(--text-muted); width: 1px; }
/* ⚠️ `rkwho`, not `who`: `.who` is the player card's 23px name heading, and it
   made every row of this table a headline. Found by looking at the page, which
   is the only way a name collision in CSS ever is — the same trap `.pyrbadge`
   was named around. */
/* ⚠️ And the flex box is a **span inside** the cell, not the cell. `display:
   flex` on a `<td>` takes it out of table layout, and the column stopped lining
   up with its own header while the blank-photograph placeholder was squeezed to
   a hairline. */
.ranktable td .rkwho { display: inline-flex; align-items: center; gap: 7px; }
.ranktable td .rkwho .noface {
  width: 18px; height: 18px; border-radius: 50%; flex: none;
  background: rgba(255, 255, 255, .08);
}
/* The year a peak was taken in. 76 in a season nobody remembers is a different
   sentence from 76 in 2024, and the number alone cannot say which. */
.ranktable .yr { font-size: 10px; color: var(--text-muted); margin-left: 5px; }
/* The gold asterisk on a pandemic season, the same one the chart's axis draws
   and for the same reason. It marks the *year*, in whichever column the year is
   shown — never the competitor, who did nothing to deserve it. */
.ranktable .ast {
  font-style: normal; color: var(--warn); margin-left: 3px; font-size: 11px;
}
/* "8 of 12" — the denominator is what makes a peak mean something, and it is
   the quieter half of the fact. */
.ranktable .of, .scorecols .of {
  font-size: 10px; color: var(--text-muted); margin-left: 4px;
}

.ranktable tr.rankrow { cursor: pointer; }
.ranktable tr.rankrow:hover td { background: rgba(255, 255, 255, .04); }
.ranktable tr.rankrow:focus-visible { outline: 1px solid var(--text-muted); }
/* ⚠️ Dimmed like everything else a pick pushes back — one gesture, one meaning,
   wherever a competitor is drawn. Not `display: none`: a ranking with rows
   missing out of the middle of it is not a ranking. */
.ranktable tr.rankrow.faded td { opacity: .32; }

/* The ladder, in the note: five weights nobody has to take on trust. */
#scoreNote .ladder { display: inline-flex; flex-wrap: wrap; gap: 4px 8px; margin: 0 4px; }
#scoreNote .wt {
  display: inline-flex; gap: 5px; align-items: baseline;
  border: 1px solid var(--border-soft); border-radius: 4px; padding: 0 6px;
  background: var(--surface-sunk);
}
#scoreNote .wt b { font: 600 11.5px var(--mono); color: var(--text); }

/* The clutter bar, which needs room for the sentence that says what it is
   hiding — so it is the one header control that may wrap. */
#winFloor { flex-wrap: wrap; }
#winFloor input[type=range] { width: 120px; }
.floortxt {
  font: 400 11px var(--font); text-transform: none; letter-spacing: 0;
  color: var(--text-muted);
}


/* ⚠️ The player card belongs to the pages that are *about* a player. The
   tournament and winners pages are about the sport — whatever is on court this
   week, and who has won what — so a career sitting above them is answering a
   question nobody on that page asked, and pushes the thing they came for down
   the screen. The search box stays: it is how you leave for a player. */
body[data-page="winners"] #hero,
body[data-page="tmt"] #hero,
body[data-page="winners"] .legends { display: none !important; }

/* ============================ the year board ============================

   One season, a column per tournament, three rows. The whole layout rests on
   one decision: **it is laid out by row, not by column.** The champions have to
   form an unbroken band across the page or the view loses the thing it is for,
   and a column-major DOM cannot keep three rows level without every column
   agreeing on its own heights. So each row is its own CSS grid over the same
   `grid-template-columns`, and the columns line up because they are the same
   track list five times over.

   `--cell` is the zoom slider's unit and the size of a semi-finalist's face.
   `--colw` is two cells and a gap, because the semi-final row holds two of
   them. `--gut` is the sticky row-label column. */
.yearboard {
  --gut: 58px;
  overflow-x: auto; overflow-y: hidden;
  padding: 2px 0 10px;
}

.ybline {
  display: grid;
  grid-template-columns: var(--gut) repeat(var(--cols), var(--colw));
  align-items: end;
  width: max-content; min-width: 100%;
}

/* ⚠️ Sticky against the scroll container, with the page ground behind it —
   without the background the faces slide under the labels rather than past
   them. `z-index` over the tiles for the same reason. */
.yblabel {
  position: sticky; left: 0; z-index: 3;
  background: var(--bg);
  font-size: 10px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-muted);
  padding: 0 8px 0 2px; align-self: center;
  white-space: nowrap;
}

/* ⚠️ The result ramp, which the career grid and the season strip already paint
   a champion, a lost final and a lost semi-final with. Same three placements,
   same three colours — so a reader who has learned one page has learned this
   one. Only the label is tinted: the cells hold photographs. */
.ybline[data-row="w"]  .yblabel { color: var(--res-f); }
.ybline[data-row="f"]  .yblabel { color: var(--res-sf); }
.ybline[data-row="sf"] .yblabel { color: var(--res-qf); }

/* Fixed by row, so an empty column does not shorten the band it sits in. */
.ybline[data-row="w"]  { min-height: calc(var(--cell) * 1.75 + 10px); }
.ybline[data-row="f"]  { min-height: calc(var(--cell) * 1.35 + 10px); }
.ybline[data-row="sf"] { min-height: calc(var(--cell) + 10px); }
.ybline[data-row] { align-items: center; }

/* A faint rule under each band, so three rows of faces read as three rows. */
.ybline[data-row="w"],
.ybline[data-row="f"] { border-bottom: 1px solid rgba(255, 255, 255, .055); }

.ybcell { display: flex; align-items: center; justify-content: center; gap: 2px; }

/* ---- the column header ---- */

.ybline.heads { align-items: end; padding-bottom: 3px; }
.ybhead {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  min-width: 0;
}
/* ⚠️ **Vertical, because a column is about 34px wide at the default zoom** and
   twenty-six tournament names will not fit across a screen horizontally. The
   name is `shortTmtName`'s — the sponsor stripped — and the full one with its
   level and date is on the hover. */
.ybname {
  writing-mode: vertical-rl; text-orientation: mixed;
  font-size: 10px; line-height: 1.1; color: var(--text-muted);
  max-height: 112px; overflow: hidden; white-space: nowrap;
  padding-top: 2px;
}
.ybname .ast { color: var(--warn); font-style: normal; }

/* ⚠️ Gold, and the one channel the level has. The placement took the face
   sizes, so the tier is said here and by nothing else — sized by
   `LEVEL[].weight`, which is why a Super 500's numeral is visibly smaller than
   a Super 1000's. */
.ybmark { display: block; flex: none; line-height: 1; }
.ybmark.number,
.ybmark.word {
  color: #ffd24a; font-weight: 700;
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
/* A word has to fit the same column a numeral does, so it is set small and
   upright rather than scaled with the tier. */
.ybmark.word { font-size: 9px; text-transform: uppercase; letter-spacing: .02em; }
.ybmark .pyrbadge { width: var(--mk); height: auto; display: block; }
/* The same correction the pyramid makes: the cup is an outline and the rings
   are five solid strokes, so at one width the cup reads as half the mark. */
.ybmark .pyrbadge.cup { color: #e8e8e8; stroke-width: 2.1; }

/* ---- the month ruler ---- */

.ybline.months { align-items: end; }
.ybmonth {
  font-size: 10px; color: var(--text-muted); letter-spacing: .04em;
  text-transform: uppercase;
  border-left: 1px solid var(--border-soft);
  padding: 0 0 3px 4px; overflow: hidden; white-space: nowrap;
}

/* ---- a competitor ---- */

.ybtile {
  position: relative; display: block; overflow: visible;
  width: var(--side); height: var(--side);
  border-radius: 3px; cursor: pointer;
  background: rgba(255, 255, 255, .06);
}
.ybtile .face {
  display: block; width: 100%; height: 100%; border-radius: 3px;
  object-fit: cover; object-position: top center;
}
.ybtile .noface {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; border-radius: 3px;
  font-size: 9px; font-weight: 700; color: var(--text-muted); letter-spacing: .04em;
}
/* A pair in one cell, exactly as the pyramid draws one: half each, each half
   the middle of its own photograph. See the note over `.pyrtile .pairface`. */
.ybtile .pairface { display: flex; width: 100%; height: 100%; overflow: hidden; border-radius: 3px; }
.ybtile .pairface .half {
  width: 50%; height: 100%; min-width: 0;
  object-fit: cover; object-position: top center;
}
.ybtile .pairface .half + .half { box-shadow: inset 1px 0 0 rgba(0, 0, 0, .55); }
.ybtile .pairface .noface { font-size: 7px; border-radius: 0; }

/* ⚠️ The ordinal is drawn **only** where a bronze play-off decided the order —
   see `podiumSeason`. Where there was no play-off the two share third and a
   number beside either face would be an invention. */
.ybord {
  position: absolute; right: -3px; bottom: -3px;
  min-width: 11px; height: 11px; padding: 0 2px;
  border-radius: 6px; background: var(--surface-2);
  box-shadow: 0 0 0 1px var(--bg);
  font-size: 8px; font-weight: 700; font-style: normal; line-height: 11px;
  text-align: center; color: var(--text-dim);
}

/* A place on the podium that nobody holds, inside a tournament that was
   played — the semi-finalists BWF did not record. Kept, so the row stays a row.  */
.ybgap {
  display: block; width: var(--cell); height: var(--cell);
  border-radius: 3px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .07);
}

/* ---- a tournament with nothing in it ----

   ⚠️ Two different absences and they must not look alike. `soon` has not
   happened: a quiet dotted rule, the same thing the order-of-play grid says
   with an empty cell. `none` is a tournament BWF has no record of, which is a
   fault in the archive rather than in the calendar, so it takes the warn
   colour the page uses everywhere for "this is not what it should be". */
.ybempty {
  width: 100%; height: calc(var(--cell) + 2px);
  border-radius: 2px;
}
/* ⚠️ Faint, but not invisible. At a dotted .16 the columns for the rest of the
   season read as nothing at all rather than as tournaments not yet played, and
   a reader scrolling to the right of October saw blank page. */
.ybempty.soon {
  background: repeating-linear-gradient(135deg,
    rgba(255, 255, 255, .055) 0 2px, transparent 2px 6px);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .055);
}
.ybempty.none {
  background: repeating-linear-gradient(135deg,
    rgba(255, 188, 32, .13) 0 2px, transparent 2px 5px);
}
/* ⚠️ **Distinct from both of its neighbours, because it means something else.**
   `soon` is the rest of the season and is deliberately faint; `none` is a gap in
   BWF's records and is warned about in amber. This one is neither — it is our
   own file being out of date, so it reads as a question rather than a warning:
   a cooler hatch at the opposite angle, with a dotted edge. */
.ybempty.stale {
  background: repeating-linear-gradient(45deg,
    rgba(120, 190, 255, .16) 0 2px, transparent 2px 5px);
  box-shadow: inset 0 0 0 1px rgba(120, 190, 255, .18);
}

/* ---- the fade ---- */

.ybtile.faded .face,
.ybtile.faded .noface,
.ybtile.faded .pairface { opacity: .16; }
.ybtile.faded .ybord { opacity: .2; }

/* ---- the ledger ----

   The answer the board can only imply. Same argument as the dominance band
   under the pyramid: the faces say a name over and over and this is what the
   repetition means. */
.ybledger {
  margin: 16px 0 4px; border-collapse: collapse;
  font-size: 12px; width: 100%; max-width: 620px;
}
.ybledger th {
  text-align: left; font-size: 10px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-muted);
  border-bottom: 1px solid var(--border); padding: 0 6px 5px;
}
.ybledger td { padding: 3px 6px; border-bottom: 1px solid var(--border-soft); }
.ybledger td.who { display: flex; align-items: center; gap: 7px; }
.ybledger td.who b { font-weight: 600; }
.ybledger td.who .muted { color: var(--text-muted); font-size: 11px; }
.ybledger .ybface {
  display: block; width: var(--side); height: var(--side);
  border-radius: 3px; overflow: hidden; flex: none;
  background: rgba(255, 255, 255, .06);
}
.ybledger .ybface .face { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top center; }
.ybledger .ybface .pairface { display: flex; width: 100%; height: 100%; }
.ybledger .ybface .pairface .half {
  width: 50%; height: 100%; min-width: 0;
  object-fit: cover; object-position: top center;
}
.ybledger .ybface .noface {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; font-size: 9px; font-weight: 700; color: var(--text-muted);
}
.ybledger th.n, .ybledger td.n {
  width: 1px; text-align: right; font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ybledger td.n.nil { color: var(--text-muted); }
.ybledger td.tot { color: var(--text-dim); border-left: 1px solid var(--border-soft); }
.ybledger tbody tr { cursor: pointer; }
.ybledger tbody tr:hover { background: rgba(255, 255, 255, .035); }
.ybledger tbody tr.faded { opacity: .34; }

/* The three counts take the same three ramp colours the row labels do. */
.ybledger tbody td.n:nth-of-type(2) { color: var(--res-f); }
.ybledger tbody td.n:nth-of-type(3) { color: var(--res-sf); }
.ybledger tbody td.n:nth-of-type(4) { color: var(--res-qf); }
.ybledger tbody td.n.nil { color: var(--text-muted); }

/* ⚠️ **A flag behind the initials, where BWF has no usable photograph.** Set
   low enough to read two letters over, and only on this board: see `oneFace`
   for why the pyramid does not ask for it. */
.noface.hasflag {
  background-image: var(--flag);
  background-size: 78% auto;
  background-repeat: no-repeat;
  background-position: center 46%;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .85);
  color: #fff;
}
.ybtile .pairface .noface.hasflag { background-size: 118% auto; }
