/* Draftout-matched dark theme.
 *
 * Chrome tokens (surfaces, borders, heading treatment, square corners) are
 * lifted from the site's own stylesheet. Data-mark colors are NOT: the brand
 * cyan #22d3ee sits at OKLCH L 0.80, well above the 0.48-0.67 band that keeps
 * marks legible, so the series colors are the brightest brand-hue steps that
 * pass validation -- see the palette note in charts.js.
 */

@font-face {
  font-family: "DM Sans Variable"; font-style: normal; font-display: swap;
  font-weight: 100 1000; src: url("/cdn/fonts/dm-sans.woff2") format("woff2-variations");
}
@font-face {
  font-family: "Inter Variable"; font-style: normal; font-display: swap;
  font-weight: 100 900; src: url("/cdn/fonts/inter.woff2") format("woff2-variations");
}
@font-face {
  font-family: "JetBrains Mono Variable"; font-style: normal; font-display: swap;
  font-weight: 100 800; src: url("/cdn/fonts/jetbrains-mono.woff2") format("woff2-variations");
}
@font-face {
  font-family: "Minecraft"; src: url("/cdn/fonts/MinecraftRegular.otf") format("opentype");
  font-weight: 400; font-display: swap;
}

:root {
  --bg: #0a0a0a;
  --panel: #101010;
  --panel-2: #161616;
  --border: rgba(103, 232, 249, 0.20);
  --border-soft: rgba(255, 255, 255, 0.07);

  --ink: #e5e5e5;
  --ink-dim: #a3a3a3;
  --ink-mute: #737373;
  --accent: #67e8f9;          /* cyan-300, chrome only -- never a data mark */
  --accent-dim: rgba(103, 232, 249, 0.55);

  /* Validated data series (dark, surface #0a0a0a, all-pairs): all six checks pass. */
  --series-me: #00a8b5;
  --series-opp: #ea4f68;
  --series-none: #6f7275;     /* absence, not an identity -- always direct-labeled */

  /* Emphasis for a hovered mark, not a series of its own: the same hue as
     --series-me a couple of steps lighter (it is --ramp-5). Identity has to
     survive the highlight, so this may never be a different hue. */
  --series-me-hi: #56d2df;

  /* Sequential ramp, single hue, validated with --ordinal. */
  --ramp-1: #004b58; --ramp-2: #006b78; --ramp-3: #008c99;
  --ramp-4: #27afbc; --ramp-5: #56d2df;

  /* Heat ramp for the scoragami grid: six steps, multi-hue on purpose.
     A grid asks a question the bar lists never do -- "is this cell more than
     that one", for two cells that are nowhere near each other and have no
     length to compare -- and lightness alone answers it too quietly. Hue
     travelling with lightness gives every step a second thing to be told
     apart by, which is why the perceptual heat maps are built this way.

     Stops picked out of inferno by bottleneck search (maximise the WORST
     adjacent separation) rather than sampled evenly, then validated: OKLab
     adjacent dE >= 13.7 normal / 11.0 under protan+deutan, against 10.3/10.1
     for the single-hue ramp above. Monotone lightness, adjacent dL >= 0.108,
     darkest step 2.06:1 on the surface -- so it still passes the ordinal ramp
     checks, single hue excepted. */
  --heat-1: #781c6c; --heat-2: #b0325a; --heat-3: #df5437;
  --heat-4: #f88e12; --heat-5: #f8cb37; --heat-6: #fcffa4;

  /* Diverging scale for a rate, centred on 50%. Deliberately the two series
     hues rather than a new pair: every bar on this site already says cyan is
     this player and red is what happened to them, so a strip that reversed
     that to introduce itself would be teaching a second language for one
     chart. The ends are quotations; only the middle is new.

     The middle is a neutral gray because a diverging scale has to have nothing
     at its centre -- a hue there would make "even" its own claim. Chosen dark
     rather than at --series-none: against #ea4f68 that gray measures dE 2.1
     under protanopia, so a protanopic reader could not tell a slightly losing
     bucket from an even one. This one measures 13.9 there and 25.3 against the
     cyan. It is also clear of --cell-empty below, so "came out even" is never
     read as "never happened". 2.27:1 on the surface, under the 3:1 mark and
     accepted for the same reason --heat-1 is: a midpoint that means "no
     signal" should recede, and every cell carries a hover card with the record
     on it. (It sits dE 13.8 from --series-none, under the 15 floor -- they
     never appear in one chart, and must not be put in one.) */
  --rate-low: var(--series-opp);
  --rate-mid: #464c50;
  --rate-high: var(--series-me);

  /* The slot behind a strip cell. --cell-muted's 2.5% is right for a matrix,
     where the muted half is furniture the eye should skip; a strip's empty
     buckets are a reading and have to be visible as slots. */
  --cell-empty: rgba(255, 255, 255, 0.045);

  --grid: rgba(255, 255, 255, 0.09);
  /* Chrome, not data: a neutral gray so a scrollbar is never mistaken
     for a series. --grid alone is too faint to aim at. */
  --scrollbar: rgba(255, 255, 255, 0.18);
  --scrollbar-hover: rgba(255, 255, 255, 0.30);

  /* Skeleton fill, and the highlight that sweeps across it. Chrome for the
     same reason --scrollbar above is: a placeholder that borrowed a series
     color would be read as a mark that has a value. */
  --sk-base: rgba(255, 255, 255, 0.055);
  --sk-lift: rgba(86, 210, 223, 0.16);

  /* The height every ranked list stops growing at: 25 bar rows of 20px content
     plus 2px padding either side, with a 3px gap between. Shared so a bar list
     and the pairs list beside it stay the same height. */
  --list-cap: calc(25 * 24px + 24 * 3px);
  --font-head: "DM Sans Variable", system-ui, sans-serif;
  --font-body: "Inter Variable", system-ui, sans-serif;
  --font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
}

* { box-sizing: border-box; }

/* Goal icons, from one sprite sheet. Both custom properties are set at boot by
   goalicon.js -- the sheet's filename carries a content hash so /cdn can serve
   it immutable for a year, which means only the generated manifest knows it.
   Declared here so the cascade has something before that runs.

   Every rule that sizes an icon does it on .gicon, which both branches of the
   helper carry, so a sprite <span> and a fallback <img> style identically. */
:root {
  --goal-sprite: none;
  --goal-sprite-size: 100% 100%;
}

.gicon { display: block; image-rendering: pixelated; }

/* background-origin/clip: content-box is load-bearing, not tidiness. With
   `box-sizing: border-box` above, .goal-head .goal-icon is 48px wide including
   1px of border and 4px of padding -- so an <img> there paints into a 38px
   content box. A background defaults to padding-box and would paint at 46px,
   8px larger than the site has ever shown it. Anchoring to the content box also
   keeps the cell arithmetic in goalicon.js exact, since the positioning area is
   then the same square the percentages were computed against. */
span.gicon, .gicon-bg {
  background-image: var(--goal-sprite);
  background-size: var(--goal-sprite-size);
  background-repeat: no-repeat;
  background-origin: content-box;
  background-clip: content-box;
}

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

h1, h2, h3, .head {
  font-family: var(--font-head);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 700;
  margin: 0;
}

a { color: var(--accent); }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------ shell */

.topbar {
  position: sticky; top: 0; z-index: 50;
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  padding: 10px 20px;
  background: rgba(10, 10, 10, 0.96);
  border-bottom: 1px solid var(--border);
  backdrop-filter: blur(8px);
}
.brand {
  font-family: "Minecraft", var(--font-head);
  font-size: 18px; color: #fff; text-shadow: 2px 2px 0 rgba(0,0,0,0.9);
  letter-spacing: 0.04em;
  background: none; border: 0; padding: 0; cursor: pointer;
}
.brand:hover span { color: #fff; }
.brand:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.brand span { color: var(--accent); }

/* The Draftle button and the support link, far right of the topbar as a pair.
   Both sit at the accent, with a faint cyan fill, so they read as things you
   can click without shouting -- the same treatment .rank-chip gets. Hover
   deepens the fill and brings the border most of the way up rather than
   changing color, so the step is felt more than seen. Square, like everything
   else. The dots in the support icon are zero-length strokes; round linecaps
   render them as the indicator LEDs, which is why stroke-linecap below is
   load-bearing. */
.support, .draftle {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 10px;
  border: 1px solid var(--border);
  background: rgba(103, 232, 249, 0.07);
  color: var(--accent); text-decoration: none;
  font-family: var(--font-head); font-size: 11px;
  letter-spacing: 0.07em; text-transform: uppercase; white-space: nowrap;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease, background-color 120ms ease;
}
/* On the first of the pair, so the auto margin pushes both across together
   and Draftle lands immediately left of Support. */
.draftle { margin-left: auto; }
.support svg {
  width: 14px; height: 14px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.3;
  stroke-linecap: round; stroke-linejoin: round;
}
.support:hover, .draftle:hover, .draftle[aria-pressed="true"] {
  border-color: var(--accent-dim);
  background: rgba(103, 232, 249, 0.16);
}
.draftle[aria-pressed="true"] { color: #e8feff; }
/* The same slow pulse the window bar's Apply uses, for the same reason: a
   thing worth noticing on a page of charts, without pulling the eye off them.
   It rests while the game is open -- found, so it stops asking -- and on
   hover. The keyframes are shared; see .window-bar .apply. */
.draftle:not([aria-pressed="true"]) { animation: apply-pulse 2400ms ease-in-out infinite; }
.draftle:hover { animation: none; }
.support:focus-visible, .draftle:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* The topbar wraps on narrow screens. Once it does, margin-left:auto would
   strand the pair alone on its own line, so let them sit inline with the rest. */
@media (max-width: 720px) {
  .draftle { margin-left: 0; }
  .support span { display: none; }   /* icon only, to keep the bar one line */
}

/* The match count and date range. Was --ink-mute, which is 4.2:1 on this
   surface and so actually below WCAG AA -- the complaint that it was hard to
   read was measurably correct, not a preference. The accent is 13.7:1 and
   matches the support link at the other end of the bar. */
#dataset-note { color: var(--accent); font-size: 11px; }

/* The accent here too, matching the rest of the bar. The usual objection --
   that a placeholder colored like real input reads as already filled -- does
   not apply: typed text is --ink, near-white, so the two stay obviously
   distinct. `opacity: 1` because Firefox dims placeholders on top of whatever
   color is set, which would undo this. */
input[type="search"]::placeholder { color: var(--accent); opacity: 1; }

.picker { position: relative; display: flex; align-items: center; gap: 8px; }
input[type="search"], select, input[type="number"] {
  background: rgba(0,0,0,0.55);
  border: 1px solid var(--border);
  color: var(--ink);
  font-family: var(--font-head);
  font-size: 13px;
  padding: 6px 10px;
  border-radius: 0;
  min-height: 32px;
}
input:focus, select:focus, button:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);
}

.suggest {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 60;
  min-width: 260px; max-height: 320px; overflow-y: auto;
  background: rgba(8,8,8,0.98); border: 1px solid var(--border);
}
.suggest button {
  display: flex; justify-content: space-between; gap: 12px; width: 100%;
  background: none; border: 0; color: var(--ink); cursor: pointer;
  padding: 7px 10px; font-family: var(--font-head); font-size: 13px; text-align: left;
}
.suggest button:hover, .suggest button[aria-selected="true"] { background: rgba(103,232,249,0.12); }
.suggest .n { color: var(--ink-mute); font-family: var(--font-mono); }

/* --------------------------------------------------- checklist popover */

/* The rank scope picker. A <select multiple> could not carry the per-rank
   counts, and the counts are most of the reason to open it at all. */
.picker-menu { position: relative; display: inline-flex; }
.menu-button {
  background: none; border: 1px solid var(--border); color: var(--ink);
  font-family: var(--font-head); font-size: 12px; letter-spacing: 0.04em;
  padding: 5px 10px; cursor: pointer;
}
.menu-button:hover { border-color: var(--accent); color: #e8feff; }
.menu-panel {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 60;
  min-width: 316px; max-height: 380px;
  display: flex; flex-direction: column;
  background: rgba(8,8,8,0.98); border: 1px solid var(--border);
  padding: 4px;
  /* Never reached once .menu-list exists -- the list shrinks first. Kept
     because /static is served with max-age=300 and unhashed filenames, so for
     five minutes after a deploy a visitor can hold this stylesheet against the
     previous app.js. With it, that pairing degrades to "the whole panel
     scrolls"; without it, the bands below the fold are unreachable. */
  overflow-y: auto;
}
/* An author `display` outranks the UA's `[hidden] { display: none }`, so the
   flex rule above would pin this panel permanently open -- `panel_.hidden` and
   closeMenus() would both silently stop working. Not tidiness; load-bearing. */
.menu-panel[hidden] { display: none; }

/* Only the bands scroll, so Apply above them cannot be pushed out of view.
   min-height:0 is what lets a flex item shrink below its content instead of
   growing the panel past its max-height. */
.menu-list {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
}

/* One template for the header and every row, in fixed widths rather than
   content widths: a per-row `auto` column is sized by that row alone, so the
   numbers would still step in and out as the rank names changed length. */
.menu-row, .menu-head {
  display: grid;
  grid-template-columns: 13px 8px minmax(88px, 1fr) 46px 44px;
  align-items: center; gap: 8px; padding: 6px 8px;
}
/* Sticky rather than lifted into a non-scrolling header: the grid template
   above only lines the numbers up while the head and the rows share a width
   context, and outside the scroller the head would be a scrollbar wider than
   the rows it labels. The margin below it becomes padding for the same
   reason a sticky element needs an opaque background -- a margin is a
   see-through gap that scrolling rows flash through. */
.menu-head {
  font-family: var(--font-head); font-size: 10px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-mute);
  padding-bottom: 4px; border-bottom: 1px solid var(--border); margin-bottom: 0;
  position: sticky; top: 0; z-index: 1; background: #080808;
}
.menu-head span { text-align: right; }
.menu-head-gap { grid-column: 1 / 4; }   /* the checkbox, swatch and name cells */
.menu-row {
  font-size: 12px; color: var(--ink); cursor: pointer;
  /* Not .controls label: that rule uppercases and mutes, which is right for a
     field name and wrong for the values being chosen between. */
  font-family: var(--font-head); letter-spacing: 0;
}
.menu-row:hover { background: rgba(103,232,249,0.10); }
.menu-row input { accent-color: var(--accent); width: 13px; height: 13px; margin: 0; }
/* "All ranks" is the reset, not a sibling of the bands -- ruled off so the list
   below reads as one group rather than fourteen peers. */
.menu-all { border-bottom: 1px solid var(--border); margin-bottom: 2px; }
.menu-dot { width: 8px; height: 8px; }
.menu-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.menu-n {
  color: var(--ink-mute); font-family: var(--font-mono); font-size: 11px;
  text-align: right; font-variant-numeric: tabular-nums;
}
/* First child, not last: with sixteen bands the panel scrolls, and an Apply
   at the bottom of the scroll was invisible on open -- people ticked boxes and
   concluded nothing had happened. flex:0 0 auto keeps it from being squeezed
   when the list is tall. */
.menu-apply {
  flex: 0 0 auto;
  width: 100%; margin: 0 0 6px; padding: 7px; cursor: pointer;
  background: rgba(103,232,249,0.18); color: #e8feff;
  border: 1px solid var(--border); font-family: var(--font-head);
  font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase;
}
.menu-apply:hover { border-color: var(--accent); }

/* What picking several bands costs, said next to the button that spends it.
   Muted and small: it is a caveat on the choice, not a warning against it. */
.menu-note {
  flex: 0 0 auto;
  color: var(--ink-mute); font-size: 11px; line-height: 1.35;
  padding: 0 2px 6px;
}

main { padding: 20px; max-width: 1500px; margin: 0 auto; }

/* -------------------------------------------------------------- identity */

.identity { display: flex; gap: 18px; align-items: stretch; margin-bottom: 18px; flex-wrap: wrap; }
.avatar {
  width: 84px; height: 128px; overflow: hidden; flex: none;
  border: 1px solid var(--border); background: var(--panel);
}
.avatar img { width: 100%; display: block; image-rendering: pixelated; margin-top: -6px; }
.identity-main { flex: 1 1 260px; min-width: 240px; }
.identity h1 { font-size: 30px; color: #fff; }
.identity .sub { color: var(--ink-dim); font-size: 13px; margin-top: 4px; }
.rank-chip {
  display: inline-block; padding: 2px 8px; margin-left: 8px;
  border: 1px solid var(--border); font-family: var(--font-head);
  font-size: 11px; letter-spacing: 0.08em; color: var(--accent);
}

/* Twelve tiles in two even rows of six. Halves to 3-up, then 2-up, so the
   rows stay even at every width rather than leaving an orphan. */
.kpis { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; }
@media (max-width: 1180px) { .kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.kpi { border: 1px solid var(--border-soft); background: var(--panel); padding: 9px 11px; }
.kpi .k { font-family: var(--font-head); font-size: 10px; letter-spacing: 0.09em;
          text-transform: uppercase; color: var(--ink-mute); }
.kpi .v { font-family: var(--font-mono); font-size: 21px; color: #fff; margin-top: 3px;
          font-variant-numeric: tabular-nums; line-height: 1.15; }
.kpi .d { font-size: 11px; color: var(--ink-dim); margin-top: 1px; }

/* ------------------------------------------------------------------ tabs */

.tabs { display: flex; gap: 2px; margin: 18px 0 14px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.tabs button {
  background: none; border: 0; border-bottom: 2px solid transparent;
  color: var(--ink-mute); cursor: pointer; padding: 8px 14px;
  font-family: var(--font-head); font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase;
}
.tabs button:hover { color: var(--ink); }
.tabs button[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); }

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

/* min(420px, 100%) rather than a bare 420px: `auto-fit` decides how many tracks
   fit, but it does not clamp the floor to the container, so a grid narrower
   than the floor still lays out one 420px track and pushes the page sideways.
   A no-op wherever 420px actually fits -- 100% resolves against this grid's
   content box, min(1500px, vw) - 40, which clears 420 at any viewport from
   460px up, and below that the collapse rule further down has already taken
   over. It is the belt for nested and future-narrow containers. */
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr)); }
.grid.wide { grid-template-columns: 1fr; }

/* Panels sharing a row line up inside as well as at their edges: each hands its
   heading, note, controls and body to the parent grid as separate rows, so a
   description that wraps to two lines pushes every panel's controls and bars
   down together rather than only its own. Without it the trio on the Match tab
   starts its bars at three different heights.

   `display: contents` is what lets the header's children become grid items of
   the panel. Where subgrid is unsupported this is skipped and the panels align
   at their edges only, as they did before. */
/* Above the collapse breakpoint only. Lining rows up *across* columns means
   nothing once there is one column, and the `row-gap: 0` that lets a panel's
   four tracks sit tight then reads as "stacked panels touch" -- which is what
   the paired draft lists already do below 900px today. Nesting the query is
   what keeps this a wrap rather than a second block of undo rules fighting
   specificity with .panel.fill. */
@media (min-width: 901px) {
@supports (grid-template-rows: subgrid) {
  .grid.align-rows { grid-template-rows: auto auto auto 1fr; row-gap: 0; }
  .grid.align-rows > .panel {
    display: grid; grid-template-rows: subgrid; grid-row: span 4; row-gap: 0;
  }
  /* Panels whose filters live in a shared bar above rather than in the card:
     three tracks to line up (heading, note, body) instead of four. */
  .grid.align-rows.no-controls { grid-template-rows: auto auto 1fr; }
  .grid.align-rows.no-controls > .panel { grid-row: span 3; }
  .grid.align-rows > .panel > header { display: contents; }
  /* The header's own margin-bottom goes with it under display: contents. */
  .grid.align-rows > .panel > div:last-child { margin-top: 12px; }
}
}

/* Everything is one column below the width at which the outer grid stops
   fitting two tracks: 2*420 + 14 gap + 40 of main padding = 894, so the 900
   this file already used is the natural line and nothing above it moves.
 *
 * Both declarations are !important because both targets are inline styles
 * written by app.js: `repeat(2|3, minmax(0, 1fr))` on the paired and tripled
 * sub-grids, and `grid-column: span 2` on the two chart panels meant to take
 * two of three desktop columns (the scatter on Draft, the reroll histogram).
 *
 * The span override is the bug fix, not tidying. Per CSS Grid 8.5 the implicit
 * grid is widened to fit the largest column span among auto-placed items
 * BEFORE anything is placed, so a single `span 2` turns a one-column grid into
 * a two-column one: the `1 / -1` panels still resolve against the explicit
 * grid and stay in column 1, and every auto-placed panel lands in the phantom
 * column beside them, one word per line. Setting the template alone does not
 * fix it.
 *
 * Done here rather than by editing those two app.js lines because /static is
 * unhashed with max-age=300 -- this stylesheet has to fix the layout while
 * paired with the previous app.js -- and it stays correct if they change. */
@media (max-width: 900px) {
  .grid { grid-template-columns: 1fr !important; }
  .grid > * { grid-column: 1 / -1 !important; }
}

/* ------------------------------------------------------------ goal view */

/* The page-level color toggle, above the first panel of every goal-keyed tab.
   Shaped after .goal-browser-bar because it is the same kind of thing -- a row
   of controls governing what is under it -- but with margin below rather than
   .controls' margin above, since nothing precedes it inside the view. */
.combine-bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: 0 0 16px;
}

/* The time window, above every tab in both views. It shares .combine-bar so
   that a page-level control reads as a page-level control whichever one it is,
   and adds only what a two-handle range needs: the handles sit side by side and
   the reading between them names the applied span rather than the staged one.

   Staged behind Apply, unlike every other control on the page -- a windowed
   all-games request re-aggregates the archive instead of summing precomputed
   buckets, so firing one per drag is not affordable. .apply is therefore a real
   button rather than a segmented switch: it has to look like it does something
   when pressed, and like nothing is pending when it is disabled. */
.window-bar { margin-bottom: 12px; }
.window-label {
  font-size: 11px; color: var(--ink-mute); font-family: var(--font-head);
  letter-spacing: 0.06em; text-transform: uppercase;
}
.window-reading { font-size: 12px; color: var(--ink); min-width: 190px; }
.window-bar input[type="range"] { accent-color: var(--accent); width: 130px; }
.window-bar .apply {
  background: none; border: 1px solid var(--border); color: var(--ink-mute);
  cursor: pointer; padding: 5px 10px; font-family: var(--font-head);
  font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
  transition: color 140ms ease, background-color 140ms ease, border-color 140ms ease;
}

/* Staged but not yet applied.
 *
 * The whole hazard of a staged control is that the handles say one thing and
 * the page below still shows another, with nothing to say which is which. The
 * dragging is what draws the eye, so the button has to come and find it: it
 * takes the accent, and it breathes until it is pressed.
 *
 * Keyed on :not(:disabled) rather than a class, because `sync()` already sets
 * `disabled` from `dirty()` -- "there is an unapplied change" is a state the
 * DOM is already carrying, and giving it a second spelling in JS is how the two
 * drift apart.
 *
 * Slow and shallow on purpose: 2.4s, and the pulse rides border and shadow
 * rather than opacity or transform, so nothing moves and no text dims. This
 * sits above a page of charts and has to be noticeable without pulling the eye
 * off them -- an urgent flash beside real data would be worse than the muted
 * button it replaced.
 */
.window-bar .apply:not(:disabled) {
  color: #e8feff;
  border-color: var(--accent);
  background: rgba(103, 232, 249, 0.10);
  animation: apply-pulse 2400ms ease-in-out infinite;
}
.window-bar .apply:not(:disabled):hover {
  color: #e8feff; background: rgba(103, 232, 249, 0.22);
  animation: none;  /* found -- it has done its job, so it stops asking */
}
@keyframes apply-pulse {
  0%, 100% {
    border-color: var(--accent-dim);
    box-shadow: 0 0 0 0 rgba(103, 232, 249, 0);
  }
  50% {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(103, 232, 249, 0.13);
  }
}

/* Disabled means "nothing staged", which is the resting state -- so it fades
   rather than looking broken. */
.window-bar .apply:disabled { opacity: 0.4; cursor: default; }
.window-bar .linkish {
  background: none; border: 0; color: var(--accent); cursor: pointer;
  font-family: var(--font-head); font-size: 11px; letter-spacing: 0.06em;
  text-transform: uppercase; padding: 5px 2px;
}
.window-bar .linkish:hover { text-decoration: underline; }
/* The same reset button, offered inside the empty-window message. */
.empty .linkish {
  background: none; border: 0; color: var(--accent); cursor: pointer;
  font-family: var(--font-head); font-size: 11px; letter-spacing: 0.06em;
  text-transform: uppercase; padding: 8px 2px 0;
}
.empty .linkish:hover { text-decoration: underline; }

.goal-browser { margin-bottom: 18px; }
.goal-browser-bar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px;
}
.goal-browser-bar input { min-width: 240px; flex: 0 1 340px; }
.goal-count {
  font-family: var(--font-head); font-size: 11px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-mute);
}

/* Icon mosaic, matching the column steps and 3px cyan gutters of the site's
   own wiki grid: the container color shows through the gaps as the rules. */
.goal-grid {
  display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 3px; padding: 3px;
  background: rgba(103, 232, 249, 0.10);
  box-shadow: inset 0 0 0 1px rgba(34, 211, 238, 0.18);
}
@media (min-width: 640px)  { .goal-grid { grid-template-columns: repeat(8, minmax(0, 1fr)); } }
@media (min-width: 768px)  { .goal-grid { grid-template-columns: repeat(10, minmax(0, 1fr)); } }
@media (min-width: 1024px) { .goal-grid { grid-template-columns: repeat(12, minmax(0, 1fr)); } }
@media (min-width: 1280px) { .goal-grid { grid-template-columns: repeat(14, minmax(0, 1fr)); } }

.goal-grid button {
  position: relative; aspect-ratio: 1 / 1; width: 100%;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(103, 232, 249, 0.08);
  background: rgba(10, 10, 10, 0.85);
  padding: 4px; cursor: pointer; color: var(--ink-mute); font-size: 10px;
  transition: border-color 150ms, background-color 150ms, box-shadow 150ms;
}
.goal-grid button:hover {
  z-index: 10; border-color: rgba(103, 232, 249, 0.45);
  background: rgba(103, 232, 249, 0.08);
  box-shadow: 0 0 18px rgba(34, 211, 238, 0.35);
}
.goal-grid button:focus-visible {
  z-index: 10; border-color: rgba(103, 232, 249, 0.55);
  outline: 2px solid var(--accent); outline-offset: -2px;
}
.goal-grid button[aria-pressed="true"] {
  z-index: 10; border-color: rgba(103, 232, 249, 0.55);
  background: rgba(103, 232, 249, 0.12);
}
.goal-grid .gicon {
  width: 100%; height: 100%; object-fit: contain; image-rendering: pixelated;
}
/* Retired goals, and goals this player has no data for, stay browsable but
   read as inactive. */
.goal-grid button.retired .gicon { opacity: 0.4; }
.goal-grid button.unplayed { background: rgba(10, 10, 10, 0.5); }
.goal-grid button.unplayed .gicon { opacity: 0.3; }
.goal-grid button:hover .gicon { opacity: 1; }
.goal-grid .empty { grid-column: 1 / -1; }

.goal-head { margin-bottom: 16px; }
.goal-head .goal-id { display: flex; align-items: center; gap: 14px; margin-bottom: 12px; }
.goal-head .goal-icon {
  width: 48px; height: 48px; image-rendering: pixelated; flex: none;
  border: 1px solid var(--border-soft); background: var(--panel); padding: 4px;
}
.goal-head h1 { font-size: 22px; color: #fff; }
.goal-head .sub { color: var(--ink-mute); font-size: 12px; margin-top: 3px; }

/* ---------------------------------------------------------------- draftle */

/* The game view. Built from the same pieces as the rest of the page -- .panel,
   .controls, .seg, .suggest, .hit-row -- so it reads as a section of the site
   rather than a widget dropped on it. What is new here is the level ramp. */
.draftle-view { max-width: 1500px; }
/* Title on the left, mode switch on the right, one line. */
.draftle-head {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  flex-wrap: wrap; margin-bottom: 12px;
}
.draftle-head h1 { font-size: 26px; color: #fff; font-family: "Minecraft", var(--font-head); }
.draftle-head h1 .n { color: var(--accent); }
.draftle-head-right { display: flex; align-items: center; gap: 10px; }

/* The search bar is the game: big, centered, first. Its meta line -- the
   guess count and the give-up link -- sits under it, also centered. */
.draftle-play {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  margin: 8px 0 18px;
}
.draftle-picker { width: min(680px, 100%); }
.draftle-picker input {
  width: 100%; font-size: 18px; padding: 12px 18px; min-height: 52px;
}
.draftle-picker .suggest { width: 100%; min-width: 0; }
.draftle-picker .suggest button { padding: 9px 14px; font-size: 14px; }
.draftle-picker .suggest .draftle-goal .gicon { width: 24px; height: 24px; }
.draftle-play-meta { display: flex; align-items: center; gap: 14px; }
/* One goal, icon and label, in a suggestion row or a table cell. */
.draftle-goal { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.draftle-goal .gicon { width: 20px; height: 20px; flex: none; }
/* background-color, never the `background` shorthand: the shorthand resets
   background-image, -size, -origin and -clip, which is the whole sprite. */
.draftle-goal.big .gicon {
  width: 56px; height: 56px; padding: 4px;
  border: 1px solid var(--border-soft); background-color: var(--panel-2);
}
.draftle-goal.big .draftle-goal-label { font-size: 20px; color: #fff; font-family: var(--font-head); }
.draftle-goal-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.draftle-retired {
  padding: 1px 5px; border: 1px solid var(--border-soft); color: var(--ink-mute);
  font-family: var(--font-head); font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase;
}
/* Hint mode's top row: the goal of the day, revealed one cell per guess. */
.draftle-target td { background: rgba(103, 232, 249, 0.06); border-bottom: 1px solid var(--border); }
.draftle-hidden-goal { color: var(--ink-dim); font-style: italic; }
.lvl-target { --lvl: var(--accent); }
.lvl-target .v { color: var(--accent); }
/* Everything in the tables is at full white -- the muted header the rest of
   the site uses was too faint next to the values it labels, and every value
   here is read against the answer's own number above it. */
.draftle-table th, .draftle-table td.lvl .v { color: #fff; opacity: 1; }
.suggest .draftle-goal { flex: 1 1 auto; }
.draftle-count {
  font-family: var(--font-head); font-size: 11px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-mute);
}
.draftle-play .linkish, .draftle-done .linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--accent); font: inherit; font-size: 12px;
}
.draftle-play .linkish:hover, .draftle-done .linkish:hover { text-decoration: underline; }

.draftle-done { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; margin-bottom: 16px; }
.draftle-done h2 { font-size: 14px; color: var(--accent); }
.draftle-done .linkish { display: inline-block; margin-top: 6px; }

/* Eleven scored columns do not fit a phone; the table scrolls inside its own
   box and the page never does. */
.draftle-scroll { overflow-x: auto; border: 1px solid var(--border-soft); background: var(--panel); }
/* Larger than the site's data tables: this is the whole page, read one row at
   a time, not a dense reference the eye scans down. */
.draftle-table { min-width: 900px; width: 100%; font-size: 15px; table-layout: fixed; }
.draftle-table col.goal { width: 280px; }
/* A name too long for its column is clipped with an ellipsis rather than
   allowed to widen it -- the width is shared with the section above. */
.draftle-table td.draftle-guess { overflow: hidden; }
.draftle-table .draftle-goal { min-width: 0; flex: 1 1 auto; }
/* Headers wrap rather than nowrap: under the fixed column layout a header
   wider than its column would overflow the table and put a scrollbar under
   a table that otherwise fits. */
.draftle-table th { white-space: normal; font-size: 12px; padding: 8px 10px; line-height: 1.2; vertical-align: bottom; }
.draftle-table td { padding: 8px 10px; }
.draftle-table .draftle-goal .gicon { width: 30px; height: 30px; }
.draftle-table .draftle-goal-label { font-size: 15px; }
.draftle-table .draftle-n { font-size: 13px; }
.draftle-tables .draftle-section {
  font-size: 12px; color: var(--accent); margin: 14px 0 6px;
}
.draftle-target-box { border-color: var(--border); }
.draftle-note { color: var(--ink-mute); font-size: 12px; margin: -2px 0 8px; }
/* The hover card's explanation line: prose, not a key/value row. */
#tip .r.draftle-tip { display: block; max-width: 260px; white-space: normal; line-height: 1.4; }
.draftle-table td.draftle-guess { white-space: nowrap; }
.draftle-guess-inner { display: flex; align-items: center; gap: 10px; min-width: 0; }
.draftle-n { color: var(--ink-mute); font-size: 11px; width: 1.6em; text-align: right; }
.draftle-table td.lvl { vertical-align: top; padding: 8px 10px; }
.draftle-table td.lvl .v {
  display: block; font-family: var(--font-mono); font-size: 16px;
  font-variant-numeric: tabular-nums;
}
.draftle-table td.lvl .tag {
  display: inline-block; margin-top: 3px; padding: 1px 6px;
  font-family: var(--font-head); font-size: 10px; letter-spacing: 0.06em;
  text-transform: uppercase; white-space: nowrap;
  border: 1px solid var(--lvl, var(--border-soft)); color: var(--lvl, var(--ink-mute));
}
/* The tags. "Similar" and a matching category are the site's own cyan, the
   one reading that says "you are close" -- the color the site already uses
   for the thing being looked for. A miss is amber for a number, where the
   direction is still a clue, and red for a category, where it is not. */
.lvl-same, .lvl-match { --lvl: var(--accent); }
.lvl-more, .lvl-less { --lvl: var(--heat-4); }
.lvl-different { --lvl: var(--heat-2); }
.lvl-unknown, .lvl-hidden { --lvl: var(--ink-mute); }
/* A stat of the goal of the day nobody has matched yet. Muted even where the
   table is otherwise all white: a dash is not a value. */
.draftle-table td.lvl .v.locked { color: var(--ink-mute); }
.draftle-table tr.hit-row td.lvl .tag { border-color: var(--accent); color: #eafdff; }

/* Draftle on a phone. Eleven columns cannot be read sideways, and two tables
   scrolling sideways independently cannot be compared at all, so below the
   topbar's own breakpoint each row becomes a card: the goal's name across the
   top, then its stats two to a line, each printed with its own label from the
   cell's data-label. Same DOM, same order, so a tag is read against the goal
   of the day's card directly above the guesses. */
@media (max-width: 720px) {
  .draftle-head h1 { font-size: 22px; }
  /* The site's narrow-screen rule makes .picker static so the topbar's
     suggestion box can span the whole bar. Down here that would hand this
     picker's box to the initial containing block and drop it a viewport-
     height down, out of sight -- so it stays the positioned parent, and the
     box spans the input, which is already the full width. Two classes to
     outrank the site rule, which comes later in the file. */
  .draftle-play .draftle-picker { position: relative; width: 100%; }
  .draftle-picker input { font-size: 16px; min-height: 46px; padding: 10px 14px; }
  .draftle-scroll { overflow: visible; border: 0; background: none; }
  .draftle-table { display: block; min-width: 0; width: auto; table-layout: auto; }
  .draftle-table colgroup, .draftle-table thead { display: none; }
  /* Newest guess first here, where the search box is at the top and a new
     card would otherwise land below everything already scrolled past. The
     desktop table keeps the order played; the DOM order is the same in both,
     only the flex direction differs. */
  .draftle-table tbody { display: flex; flex-direction: column-reverse; gap: 8px; }
  .draftle-table tr {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px;
    padding: 10px 12px; border: 1px solid var(--border-soft); background: var(--panel);
  }
  .draftle-table tr.draftle-target { border-color: var(--border); background: rgba(103, 232, 249, 0.06); }
  .draftle-table tr.hit-row { border-color: rgba(103, 232, 249, 0.75); }
  .draftle-table td, .draftle-table td.lvl, .draftle-table td.draftle-guess {
    display: block; padding: 0; border: 0; background: none; text-align: left;
  }
  .draftle-table td.draftle-guess {
    grid-column: 1 / -1; padding-bottom: 8px; border-bottom: 1px solid var(--border-soft);
  }
  .draftle-table td.lvl::before {
    content: attr(data-label); display: block;
    font-family: var(--font-head); font-size: 10px; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--ink-mute); margin-bottom: 1px;
  }
  .draftle-table td.lvl .v { display: inline-block; font-size: 15px; margin-right: 6px; }
  .draftle-table td.lvl .tag { vertical-align: 2px; }
  .draftle-done { gap: 12px; }
  .draftle-goal.big .draftle-goal-label { font-size: 17px; white-space: normal; }
}

/* Divider introducing a subsection inside a tab. */
.section-head {
  grid-column: 1 / -1; display: flex; align-items: center; gap: 12px;
  margin: 12px 0 -2px; min-width: 0;
}
.section-head h2 { font-size: 12px; color: var(--accent); white-space: nowrap; }
.section-head .line { flex: 1 1 auto; height: 1px; background: var(--border); min-width: 16px; }
.section-head .sub-note {
  color: var(--ink-mute); font-size: 12px; flex: 0 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.panel { border: 1px solid var(--border-soft); background: var(--panel); padding: 14px 16px 16px; min-width: 0; }
.panel > header { margin-bottom: 12px; }
.panel h2 { font-size: 13px; color: var(--ink); }
.panel .note { color: var(--ink-mute); font-size: 12px; margin-top: 3px; line-height: 1.45; }
/* A note that reports something is wrong, not just context. */
.panel .note.warn {
  color: var(--series-opp); border-left: 2px solid var(--series-opp); padding-left: 8px;
}
/* What the sample floor took out of this panel. Not styled as a warning: a
   filter the reader set doing what they set it to is not a fault, it only has
   to be visible. */
.panel .note.sample-note { font-style: italic; }
/* Last on its own line inside the control row it explains -- `order` puts it
   after the chart search that is appended behind it, and a full basis is what
   makes the row wrap rather than squeeze. See bySample for why it cannot
   simply sit beside the controls as another header child. */
.controls .note.sample-note { flex: 0 0 100%; order: 1; margin-top: 6px; }

.controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
/* .combine-bar rides along: the page-level bar holds the same kinds of control
   as a panel's own row, so they have to read as the same kind of control. */
.controls label, .combine-bar label {
  font-size: 11px; color: var(--ink-mute); font-family: var(--font-head);
  letter-spacing: 0.06em; text-transform: uppercase;
}
.seg { display: flex; border: 1px solid var(--border); }
.seg button {
  background: none; border: 0; color: var(--ink-mute); cursor: pointer;
  padding: 5px 10px; font-family: var(--font-head); font-size: 11px;
  letter-spacing: 0.06em; text-transform: uppercase;
}
.seg button[aria-pressed="true"] { background: rgba(103,232,249,0.18); color: #e8feff; }
.seg button:hover { color: var(--ink); }
.controls input[type="range"],
.combine-bar input[type="range"] { accent-color: var(--accent); width: 110px; }

/* The per-chart goal search. Pushed to the end of the control row -- the
   switches change what the chart measures, this only narrows what is left, so
   it reads last -- and sized down to the height of the segmented buttons it
   sits beside rather than the page-header search it inherits from. */
.controls .chart-filter {
  /* 160px of basis rather than a comfortable 200: a flex row wraps on base
     sizes and only shrinks afterwards, so a wider box drops itself onto a
     second line in the panels that have three switches to sit beside. */
  margin-left: auto; flex: 0 1 160px; min-width: 120px;
  min-height: 26px; padding: 4px 8px; font-size: 12px;
}

/* ------------------------------------------------------------- bar lists */

.barlist { display: flex; flex-direction: column; gap: 3px; }
.barrow {
  /* 96px on the value column, not 88: the availability lists write a rate and
     its fraction together ("33.4% 116/347"), which needs 94px. At 88 it spilled
     invisibly; inside a scrolling list it would be clipped instead. */
  display: grid; grid-template-columns: 20px 1fr 96px; gap: 9px;
  align-items: center; padding: 2px 0; position: relative;
}

/* Past 25 rows a list scrolls rather than stretching the panel (see
   ROWS_BEFORE_SCROLL in charts.js), so raising the row control from 25 to 50
   adds a scrollbar and leaves the page the same length. scrollbar-gutter keeps
   the bar off the value column instead of overlapping the numbers.

   The `.panel.fill` selector has to be repeated here: that rule clears the
   max-height so a panel's list can stretch to its neighbor's height, and it
   would otherwise out-specify the cap and let the list grow unbounded again. */
.barlist.capped, .scroll.capped, .panel.fill .scroll.capped {
  max-height: var(--list-cap);
  overflow-y: auto;
  scrollbar-gutter: stable;
}
.barlist.capped, .scroll { scrollbar-width: thin; scrollbar-color: var(--scrollbar) transparent; }
.barlist.capped::-webkit-scrollbar, .scroll::-webkit-scrollbar { width: 8px; }
.barlist.capped::-webkit-scrollbar-track, .scroll::-webkit-scrollbar-track { background: transparent; }
.barlist.capped::-webkit-scrollbar-thumb, .scroll::-webkit-scrollbar-thumb {
  background: var(--scrollbar); border-radius: 4px;
}
.barlist.capped::-webkit-scrollbar-thumb:hover, .scroll::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-hover);
}
/* Where the goal stands in the whole ranking, ahead of its icon.

   20px is three digits of the mono face at 11px and not a pixel more, because
   the column is right-aligned: at any wider setting a three-digit rank -- the
   widest there is, the list being the ~370-goal catalog -- would sit indented
   from the panel title above it, and the column would read as floating rather
   than as the left edge of the row. Shorter ranks fall to the right of it on
   tabular figures, which is what keeps the icons after them in line. */
.barrow.ranked { grid-template-columns: 20px 20px 1fr 96px; gap: 8px; }
.barrow .rank {
  font-family: var(--font-mono); font-size: 11px; color: var(--ink-mute);
  text-align: right; font-variant-numeric: tabular-nums;
}
.barrow:hover .rank { color: var(--ink-dim); }

.barrow .icon { width: 20px; height: 20px; image-rendering: pixelated; }
.barrow .icon.missing { background: var(--panel-2); }
.barrow .track { position: relative; height: 20px; min-width: 0; }
.barrow .fill {
  position: absolute; inset: 0 auto 0 0; height: 20px;
  border-radius: 0 4px 4px 0;   /* rounded data-end, square at baseline */
  background: var(--series-me);
}
.barrow .lbl {
  position: absolute; left: 7px; top: 0; height: 20px; display: flex; align-items: center;
  font-size: 12px; color: var(--ink); white-space: nowrap; pointer-events: none;
  text-shadow: 0 1px 2px rgba(0,0,0,0.85);
}
.barrow .val {
  font-family: var(--font-mono); font-size: 12px; color: var(--ink-dim);
  text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap;
}
.barrow .val b { color: #fff; font-weight: 500; }

/* Column heads over a two-value bar list. Only the diverging lists have them:
   one value per row is named by the panel title, two are not. Set in the head
   face at panel-note weight so the heads read as a caption on the list rather
   than as a first row of it. */
.barrow.barhead {
  padding: 0 0 3px; border-bottom: 1px solid var(--grid); margin-bottom: 3px;
}
.barrow.barhead .val {
  font-family: var(--font-head); font-size: 10px; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-mute);
}
.barrow:hover .fill { filter: brightness(1.18); }
.barrow.retired .icon { opacity: 0.45; }
.barrow.retired .lbl::after {
  content: "retired"; margin-left: 8px; font-size: 10px; color: var(--ink-mute);
  font-family: var(--font-head); letter-spacing: 0.06em; text-transform: uppercase;
}

/* Stacked variant: 2px surface gaps do the separating, never strokes. */
.barrow .seg-fill { position: absolute; top: 0; height: 20px; }
.barrow .seg-fill:last-child { border-radius: 0 4px 4px 0; }
/* The count printed on a stacked segment, centred in its own segment. Shadowed
   the way .lbl is, because it sits on a data color rather than on the surface,
   and inert so it never takes the hover the row is serving. */
.barrow .seg-lbl {
  position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; font-family: var(--font-mono); font-size: 11px;
  color: #fff; font-variant-numeric: tabular-nums; pointer-events: none;
  white-space: nowrap; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85);
}

/* ------------------------------------------------- pair matrix (sticky) */

/* The grid scrolls inside this box in both directions rather than growing the
   page, which is what lets the axes stay put: `position: sticky` resolves
   against the nearest scrolling ancestor, so the strips can only be held
   against a scroll that happens here. */
.matrix {
  overflow: auto; max-height: 72vh;
  scrollbar-width: thin; scrollbar-color: var(--scrollbar) transparent;
}
.matrix::-webkit-scrollbar { width: 8px; height: 8px; }
.matrix::-webkit-scrollbar-track { background: transparent; }
.matrix::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 4px; }

/* width: max-content so the grid keeps its stated pixel size instead of being
   squeezed to the box -- being wider than the box is the whole point. */
.matrix-grid { display: grid; width: max-content; }
.matrix-corner { grid-area: 1 / 1; }
.matrix-cols   { grid-area: 1 / 2; display: flex; }
.matrix-rows   { grid-area: 2 / 1; display: flex; flex-direction: column; }
.matrix-body   { grid-area: 2 / 2; }

/* Opaque, or the cells scroll visibly underneath the icons. The corner outranks
   both strips because at the top-left all three overlap. */
.matrix-corner, .matrix-cols, .matrix-rows { background: var(--panel); }
.matrix-corner { position: sticky; top: 0; left: 0; z-index: 3; }
.matrix-cols { position: sticky; top: 0; z-index: 2; }
.matrix-rows { position: sticky; left: 0; z-index: 2; }

.matrix-icon { flex: none; background: var(--panel-2); }
.matrix-icon .gicon { width: 100%; height: 100%; display: block; image-rendering: pixelated; }

/* A magnified matrix is wider than its panel by design, so the panel scrolls
   sideways rather than the page doing it. */
.zoomable { overflow-x: auto; overflow-y: hidden; scrollbar-width: thin;
            scrollbar-color: var(--scrollbar) transparent; }
.zoomable::-webkit-scrollbar { height: 8px; }
.zoomable::-webkit-scrollbar-track { background: transparent; }
.zoomable::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 4px; }
.zoomable svg { display: block; }

/* The hovered matrix cell, redrawn a few pixels larger with an outline. The
   stroke is the page's brightest ink so it reads against every step of the
   ramp, including the lightest one. */
.cell-lift {
  stroke: var(--ink);
  stroke-width: 1.5;
  paint-order: stroke;
  pointer-events: none;
}

.legend { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 10px; }
.legend .item { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--ink-dim);
                font-family: var(--font-head); letter-spacing: 0.05em; text-transform: uppercase; }
.legend .sw { width: 10px; height: 10px; flex: none; }
/* Names the scale a key of bare numbers would otherwise leave unlabeled.
   Muted and swatchless, so it reads as the caption on the row rather than as a
   band with a color of its own. */
.legend-title { font-size: 11px; color: var(--ink-mute); font-family: var(--font-head);
                letter-spacing: 0.05em; text-transform: uppercase; }

/* The continuous heat scale: the ramp itself, with counts beneath it. Capped
   at 280px because the ticks are ratios rather than a length -- a wider bar
   spaces the same five numbers further apart without saying anything more. */
.heat-key { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 12px; }
.heat-key .legend-title { padding-top: 1px; }
.heat-key .scale { position: relative; width: min(280px, 100%); }
.heat-key .bar { height: 10px; }
.heat-key .ticks { position: relative; height: 14px; margin-top: 3px; }
.heat-key .ticks span {
  position: absolute; transform: translateX(-50%);
  font-family: var(--font-mono); font-size: 10px; color: var(--ink-mute);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* A panel whose scroller should fill the height the grid row gives it, so a
   capped list sits flush beside a taller neighbor instead of leaving a gap. */
.panel.fill { display: flex; flex-direction: column; }
.panel.fill > div:last-child { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.panel.fill .scroll { flex: 1 1 auto; max-height: none; min-height: 0; }

/* A chart told to fill its card (see fillHeight in app.js). The SVG is taken
   out of flow so redrawing it at a measured height cannot change the host's
   height and re-trigger the observer doing the measuring. The min-height is
   the floor for when this panel is alone in its grid row and has no taller
   neighbor to take its height from -- without it the box would collapse. */
.panel.fill .chart-fill { position: relative; flex: 1 1 auto; }
/* The floor needs `div` and `:last-child` to land, because the rule three
   lines up hits the very same element: .chart-fill IS the panel's last child
   div, and its `min-height: 0` -- the "let a flex item shrink past its
   content" trick the scrolling lists need -- was quietly outranking the floor
   on (0,3,1) against (0,3,0). Nothing showed for it on a wide screen, where
   the grid row is stretched by the taller panel beside this one and the body
   takes its height from the leftover space. In one column there is no taller
   sibling, so the body collapsed to nothing and the chart, absolutely
   positioned inside it, had no box left to paint in. */
.panel.fill > div.chart-fill:last-child { min-height: 300px; }
.panel.fill .chart-fill > svg { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Reroll pairs: two goals discarded together, one row each. */
.pair-list { display: flex; flex-direction: column; }
.pair-row {
  display: grid; grid-template-columns: 1fr auto 1fr 34px;
  align-items: center; gap: 8px; padding: 3px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04); min-width: 0;
}
/* moved into @media (hover: hover) at the end of this file */
.pair-goal {
  display: flex; align-items: center; gap: 6px; min-width: 0;
  font-size: 12px; color: var(--ink);
}
.pair-goal .gicon { width: 18px; height: 18px; image-rendering: pixelated; flex: none; }
.pair-goal span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pair-plus { color: var(--ink-mute); font-size: 11px; flex: none; }
.pair-row .val { text-align: right; font-size: 11px; color: var(--accent); }

/* ----------------------------------------------------------------- table */

table { width: 100%; border-collapse: collapse; font-size: 12px; }
th {
  text-align: left; font-family: var(--font-head); font-size: 10px; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-mute); font-weight: 600;
  padding: 6px 8px; border-bottom: 1px solid var(--border-soft); position: sticky; top: 0;
  background: var(--panel); user-select: none; white-space: nowrap;
}
/* A header sits over its own column, so numeric headers align right like the
   values under them. */
th.num { text-align: right; }
/* Only a sortable table's headers advertise themselves as clickable. */
table.sortable th { cursor: pointer; }
table.sortable th:hover { color: var(--ink); }
th[aria-sort] { color: var(--accent); }
td { padding: 5px 8px; border-bottom: 1px solid rgba(255,255,255,0.04); }
/* Dates and durations must not wrap mid-value. */
td.num {
  font-family: var(--font-mono); text-align: right;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* moved into @media (hover: hover) at the end of this file */
.scroll { max-height: 460px; overflow-y: auto; }
/* Match logs are the thing you actually read down, so give them the room. */
.scroll.tall { max-height: min(85vh, 1100px); }

/* Rows that open the match on draftoutmc.com. */
table.linked tbody tr { cursor: pointer; }
table.linked tbody tr:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}
table.linked a { color: inherit; text-decoration: none; }
table.linked tr:hover a { color: var(--accent); text-decoration: underline; }

/* Rows picked out by clicking a chart bucket: filled, not just outlined, so
   they are findable while scanning a long table. */
tbody tr.hit-row > td {
  background: rgba(103, 232, 249, 0.22);
  color: #eafdff;
  border-top: 1px solid rgba(103, 232, 249, 0.75);
  border-bottom: 1px solid rgba(103, 232, 249, 0.75);
}
tbody tr.hit-row > td:first-child {
  border-left: 3px solid var(--accent);   /* a spine to catch the eye */
}
tbody tr.hit-row > td:last-child { border-right: 1px solid rgba(103, 232, 249, 0.75); }
tbody tr.hit-row:hover > td { background: rgba(103, 232, 249, 0.30); }
tbody tr.hit-row a { color: #eafdff; }

.link-button {
  background: none; border: 1px solid var(--border); color: var(--accent);
  cursor: pointer; padding: 4px 10px; font-family: var(--font-head);
  font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
}
.link-button:hover { background: rgba(103, 232, 249, 0.12); }

/* --------------------------------------------------------------- tooltip */

#tip {
  position: fixed; z-index: 100; pointer-events: none; opacity: 0;
  transition: opacity 90ms ease; max-width: 300px;
  background: rgba(6,6,6,0.97); border: 1px solid var(--border);
  padding: 8px 10px; font-size: 12px; line-height: 1.45;
  box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
#tip .t { font-family: var(--font-head); font-size: 11px; letter-spacing: 0.05em;
          text-transform: uppercase; color: var(--accent); margin-bottom: 4px; }
#tip .r { display: flex; justify-content: space-between; gap: 16px; color: var(--ink-dim); }
#tip .r b { color: #fff; font-family: var(--font-mono); font-weight: 500; }

/* ------------------------------------------------------------------- svg */

svg { display: block; width: 100%; overflow: visible; }
svg text { font-family: var(--font-mono); font-size: 10px; fill: var(--ink-mute); }
svg .axis-line, svg .grid-line { stroke: var(--grid); stroke-width: 1; }
svg .mark-me { fill: var(--series-me); }
svg .mark-opp { fill: var(--series-opp); }
svg .hit { fill: transparent; }
svg .hit.selectable { cursor: pointer; }
/* moved into @media (hover: hover) at the end of this file */
/* The clicked bucket keeps a visible frame while its rows stay picked out. */
svg .band-selected {
  fill: rgba(103, 232, 249, 0.08); stroke: var(--accent);
  stroke-width: 1; pointer-events: none;
}
svg .bar-selected { filter: brightness(1.25); }
/* The band the tooltip has snapped to. Quieter than the clicked one -- hover is
   a readout, not a choice, and the two can be on screen at once. */
svg .band-hover {
  fill: rgba(103, 232, 249, 0.06); stroke: var(--accent-dim);
  stroke-width: 1; pointer-events: none;
}

/* Matrix: the redundant mirror half and the self-pair diagonal are inert. */
svg .cell-muted { fill: rgba(255, 255, 255, 0.025); }
/* A strip's empty bucket: a slot, not furniture. See rateStrip in charts.js. */
svg .strip-slot { fill: var(--cell-empty); }
svg .cell-diagonal { fill: rgba(255, 255, 255, 0.10); }
/* The axis icons moved out of the SVG so they could be pinned; the hover
   affordance moved with them. */
.matrix-icon { cursor: help; }
/* moved into @media (hover: hover) at the end of this file */
/* The two goals the crosshair is pointing at. Same light as a direct hover,
   since it means the same thing: this is the icon you are being told about. */
.matrix-icon-active { background: rgba(103, 232, 249, 0.22); }
svg .dot-ring { stroke: var(--panel); stroke-width: 2; }

/* Scatter marks. The resting ring is the surface color, so overlapping dots
   read as separate marks rather than one blob. */
svg .dot {
  fill: var(--series-me);
  stroke: var(--panel);
  stroke-width: 2;
  opacity: 0.85;
}

/* The one the crosshair has snapped to: a lighter step of the same hue -- the
   identity must not change, only its emphasis -- plus a pale ink outline.
   Deliberately not the chrome cyan: against a fill this close to it the ring
   disappeared, and the whole job of the ring is to be findable in a dense
   field. Bigger and fully opaque does the rest. r is set in charts.js, since
   it also decides the hit maths. */
svg .dot-active {
  fill: var(--series-me-hi);
  stroke: var(--ink);
  stroke-width: 1.5;
  opacity: 1;
}

svg .crosshair-line {
  stroke: var(--accent-dim);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}

/* Sits on the same baseline as the axis ticks it replaces, in chrome cyan so it
   reads as a readout rather than as another tick. */
svg .crosshair-tag {
  fill: var(--accent);
  font-size: 10px;
  font-weight: 600;
  paint-order: stroke;
  stroke: var(--panel);
  stroke-width: 3;
}

.empty { color: var(--ink-mute); font-size: 12px; padding: 22px 0; text-align: center; }
.hint { color: var(--ink-mute); font-size: 11px; margin-top: 8px; }

/* --------------------------------------------------------------- loading */

/* A player bundle takes the origin a long time to build, and until it arrives
 * the page has nothing true to say. What it used to show was the *previous*
 * view -- ALL GAMES, its twelve tiles and its tab bar -- unchanged and
 * unlabeled, so the only evidence a click had done anything was the address
 * bar and one line of 12px text down in the body.
 *
 * Every skeleton fades in on a delay with `both` fill, which means it is
 * transparent for the first quarter second and a response quicker than that
 * paints nothing at all. That delay is what lets this treatment stay in once
 * the origin gets fast: a placeholder replaced two frames after it appears
 * reads as a fault, not as feedback. It is one constant, expressed once, in
 * CSS -- a JS timer doing the same job would be a second one to keep in step.
 */
@keyframes sk-in    { from { opacity: 0; } to { opacity: 1; } }
@keyframes sk-sweep { from { background-position: 100% 0; } to { background-position: 0 0; } }

.sk { animation: sk-in 160ms ease 250ms both; }

/* One highlight per shape, painted as a background three times the width of
   the box and slid across it. Every skeleton on the page shares the timing, so
   a card pulses as one object rather than as a stack of loose bars. */
.sk-bar, .sk-rows, .sk-plot {
  background-color: var(--sk-base);
  background-image: linear-gradient(90deg,
    var(--sk-base) 0%, var(--sk-base) 38%,
    var(--sk-lift) 50%,
    var(--sk-base) 62%, var(--sk-base) 100%);
  background-size: 300% 100%;
  background-position: 100% 0;
  animation: sk-sweep 1900ms linear infinite;
  border-radius: 3px;
}

.sk-bar { height: 12px; }
.sk-bar.title { height: 13px; width: 42%; }
.sk-bar.note  { height: 11px; width: 76%; margin-top: 7px; }
/* A KPI value and its detail line. These sit *inside* the real .v and .d
   divs, so those keep their own margins and only the line box has to be
   replaced: 21px at line-height 1.15 is 24px, and 11px at the body's 1.5 is
   16px. Matching them is what stops the twelve tiles resizing when the
   numbers land -- this is the one place where the pending and loaded markup
   are the same element, so it is the one place where a mismatch would show.

   Both are present on every pending tile even though only some real tiles
   carry a detail: the row takes its height from the tallest tile either way. */
.sk-bar.kpi-v { height: 24px; width: 68%; }
.sk-bar.kpi-d { height: 16px; width: 46%; }

/* A ranked list, drawn rather than built out of fifty divs. The height is
   --list-cap, so the panel the real list arrives into is the height the
   skeleton already was and nothing below it moves.
 *
 * The mask cuts the gaps out of one painted rectangle. Where mask-image is
 * unsupported this degrades to a solid shimmering block, which is still a
 * skeleton. The taper is not decoration: every list this stands in for is
 * sorted descending, so a wedge is the shape of the thing that is coming. */
.sk-rows {
  height: var(--list-cap);
  margin-top: 12px;
  --sk-pattern: repeating-linear-gradient(to bottom, #000 0 20px, transparent 20px 27px);
  -webkit-mask-image: var(--sk-pattern);
  mask-image: var(--sk-pattern);
  clip-path: polygon(0 0, 100% 0, 36% 100%, 0 100%);
}

/* A chart-shaped body, for the tabs whose cards are plots. */
.sk-plot { height: 300px; margin-top: 12px; }

/* The wait copy: .empty's muted line, given room above the grid it explains. */
.sk-note { color: var(--ink-dim); font-size: 12px; padding: 6px 0 16px; text-align: center; }

/* Motion is the message here -- the whole point of the sweep is to show the
   tab is not wedged -- so with motion turned down the skeleton keeps its shape
   and loses the sweep, and the status line above it, which rewrites itself on
   its own timer, becomes the thing that shows progress. Nothing on the page
   needs the animation to be legible. */
@media (prefers-reduced-motion: reduce) {
  .sk { animation-duration: 1ms; }
  .sk-bar, .sk-rows, .sk-plot { background-image: none; animation: none; }
  /* The pulse goes, the emphasis stays. Dropping the animation alone would
     leave the button at whatever keyframe it was resting on; pinning the lit
     end of the pulse keeps "there is something to apply" legible without
     anything moving. */
  .window-bar .apply:not(:disabled),
  .draftle:not([aria-pressed="true"]) {
    animation: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(103, 232, 249, 0.13);
  }
}

/* ------------------------------------------------------ narrow screens */

/* Last in the file on purpose. Every rule here overrides a base rule declared
   above it at equal specificity -- .picker, .controls, .panel, .barrow, main --
   so it has to come after them to win the cascade rather than lose it by a
   dozen lines. The .support rules stay up with .support because they are the
   exception: their base rule is directly above them. */
@media (max-width: 720px) {
  /* 8px back on each side is 4% of a 390px viewport, and every hardcoded row
     template in this file is sized against what is left. The topbar moves with
     main so the brand still lines up with the content column.

     overflow-x is `clip`, not `auto`: `auto` would make main a scroll
     container, and .barrow .lbl is absolutely positioned and allowed to
     overhang its track, so a scrollbar would appear on rows that are fine.
     Engines without `clip` drop the declaration and get today's behaviour. */
  .topbar { padding: 10px 12px; }
  main { padding: 12px; overflow-x: clip; }
  .panel { padding: 14px 12px 16px; }

  /* Bar rows are the one place a fixed template is written inline: rateBars'
     laddered five columns (56 + 120 + 62 + 62) come to 336px at gap 9, wider
     than the panel holding them. .ranked is restated because its own gap
     out-specifies this one. */
  .barrow, .barrow.ranked { gap: 6px; }

  /* Both popovers are `left: 0` against their own trigger, and a trigger sits
     a couple of hundred px into its row -- a 316px panel opened from there
     ends past the right edge. Handing the containing block to the full-width
     ancestor is the only clamp that needs no measuring: `top: calc(100% + 4px)`
     then reads "below the whole bar" and left/right: 0 spans the content
     column. Scoped by descendant on purpose, so a trigger moved out of
     .controls keeps its current positioned parent instead of escaping to the
     initial containing block and opening a viewport-height down.

     Neither rule may set `display`: both popovers are toggled with the
     `hidden` attribute, and an author display outranks the UA's
     `[hidden] { display: none }` -- see the note on .menu-panel[hidden]. */
  .picker { position: static; }
  .suggest { left: 0; right: 0; min-width: 0; max-height: min(320px, 50vh); }

  .controls { position: relative; }
  .controls .picker-menu { position: static; }
  .controls .menu-panel { left: 0; right: 0; min-width: 0; max-height: min(380px, 60vh); }
}

/* ------------------------------------------------------------ touch input */

/* A card sized for a phone. On a 320px screen a 300px card centred on a finger
   near either edge gets clamped into the corner it was meant to avoid, and the
   12px body copy that reads fine under a cursor is small held at arm's length. */
@media (pointer: coarse) {
  #tip {
    max-width: min(320px, calc(100vw - 16px));
    font-size: 13px; padding: 10px 12px;
  }
}

/* Hover states are a lie on a touch screen: iOS holds the last tapped element
   hovered until something else is tapped, so a tapped band or row stays lit
   with nothing reading it any more. Gathered here rather than left beside the
   rules they belong to so there is one place that answers "what does hover
   do", and each is the only rule for its selector, so moving them changes no
   cascade. */
@media (hover: hover) {
  svg .hit.selectable:hover { fill: rgba(103, 232, 249, 0.07); }
  tbody tr:hover { background: rgba(103,232,249,0.06); }
  .pair-row:hover { background: rgba(103, 232, 249, 0.06); }
  .matrix-icon:hover { background: rgba(103, 232, 249, 0.22); }
}

/* A tap on a chart is a reading, not a text selection or a link press: without
   these, reading a bar flashes a grey box over it and a drag across a list
   starts selecting the labels instead of scrolling. */
svg .hit, .barrow, .pair-row, .matrix-icon {
  -webkit-tap-highlight-color: transparent;
}
.barlist, .pair-list { -webkit-user-select: none; user-select: none; }
