﻿/* Layout and components. Tokens live in tokens.css and are not restated here. */

*, *::before, *::after { box-sizing: border-box; }

/* The `hidden` attribute is only `display: none` in the UA stylesheet, so any
 * rule of ours that sets a display, `.spritepicker { display: flex }`. Wins
 * on specificity and the element stays visible. Several things here are
 * toggled by that attribute, so it is enforced once, here. */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  background: var(--page);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-base);
  line-height: 1.5;
  overflow: hidden;              /* the panes scroll, the page does not */
  -webkit-font-smoothing: antialiased;
}

/* Scrollbars, because the default ones are a Windows control from 1995 and
 * this is a dark UI: a light grey trough with stepper arrows at each end sits
 * on the page like a seam.
 *
 * `scrollbar-width` and `scrollbar-color` are the standard pair and now do the
 * work in both Firefox and Chrome. Measured: with both declared, Chrome honours
 * the standard properties and ignores the ::-webkit-scrollbar widths and
 * colours below. Those are kept for older Safari, where they are still the
 * only thing that applies.
 *
 * Nothing is hidden. A scrollbar that vanishes leaves no clue that there is
 * more below, which is a worse trade than a slightly wide one. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border-strong); border-radius: 999px;
  /* Inset by a transparent border rather than by being narrow, so the hit
     target stays the full 10px while the visible bar is 6. */
  border: 2px solid transparent; background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); background-clip: padding-box; }
::-webkit-scrollbar-corner { background: transparent; }
/* The stepper buttons at each end. Chrome draws them on Windows and they are
   the clunkiest part of the default control. */
::-webkit-scrollbar-button { display: none; }

h1, h2, h3 { margin: 0; font-weight: 600; }
a { color: var(--series-1); }
button { font: inherit; color: inherit; }

:focus-visible {
  outline: 2px solid var(--series-1);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• device frame â•â• */

/* Two layouts, one markup.
 *
 * DEVICE layout (Kanto) hides the bands entirely and the page is ordinary:
 * chrome around a 3D PokÃ©dex sitting on a stage.
 *
 * IMMERSIVE layout (Kalos) turns the viewport itself into the PokÃ© Ball. The
 * bands become its top and bottom, and everything between them is the screen.
 * The site stops wrapping around a PokÃ©dex and becomes one.
 *
 * The bands are deliberately shallow â€” 46px and 78px. They are there to frame
 * and to say "this is a device", and every pixel they take is a pixel the data
 * does not get. An earlier pass gave them a quarter of the viewport each with
 * full PokÃ© Ball arcs, and the result was a lot of red with the dex squeezed
 * into a letterbox. */

/* 100dvh, with a 100vh fallback for browsers that lack it.
 *
 * The device is the page, so any gap between its height and the viewport shows
 * as a strip of bare body colour under it. `vh` on mobile is the viewport with
 * the URL bar *hidden*, so it overshoots while the bar is visible and the
 * bottom band ends up under it; `dvh` tracks the real height as the bar comes
 * and goes. */
.device { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
.device-screen { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.device-band { display: none; }

/* The frame is deliberately quieter than the device it depicts.
 *
 * A saturated PokÃ© Ball red across the top and bottom of every screen competes
 * with the data for attention all day, and the data is the point. These are
 * the same hues pulled down in lightness and chroma so the frame reads as
 * casing rather than as a highlight â€” recognisable at a glance, invisible
 * after ten seconds. */
body.is-immersive { background: #5c0a16; }
body.is-immersive .device-band {
  display: block; position: relative; flex: none; overflow: hidden;
  background: linear-gradient(180deg, #b8283a 0%, #99202f 52%, #741624 100%);
}
body.is-immersive .device-band-bottom {
  background: linear-gradient(0deg, #b8283a 0%, #99202f 52%, #741624 100%);
}
/* Both bands are the same height. The bottom one was 70px to house a 54px
 * ball with clearance; with the controls flattened into the band there is
 * nothing left to clear, and two frames of equal weight read as a frame
 * rather than as a screen sitting on a plinth.
 *
 * The control sits 2px clear of each edge rather than filling the band. The
 * gap is what stops the block from reading as a hole cut through the casing.
 * Both bands carry the same height so the frame stays symmetrical. */
body.is-immersive .device-band-top,
body.is-immersive .device-band-bottom { height: 40px; }

/* The bands meet the screen directly. There used to be a 10px black lip along
 * the inner edge of each, a leftover from when the centre button straddled
 * that edge and needed something to sit against. With the button inside the
 * band it separated nothing, and read as two black bars stranded between the
 * frame and the screen. The screen's own inset shadow is the edge. */

/* One segmented control, not three floating circles.
 *
 * The circles were centred individually with fixed pixel offsets, so the gaps
 * between them were a constant that had nothing to do with the band; the group
 * read as three objects that happened to be near each other. Butting the three
 * segments together and filling the band's full inner height makes it one
 * control, and the band's height becomes the button's height instead of
 * clearance around it. */
.band-pad {
  position: absolute; left: 50%; transform: translateX(-50%);
  top: 2px; bottom: 2px;           /* a hairline of band showing at each end */
  display: flex; align-items: stretch; z-index: 2;
  border-radius: 5px; overflow: hidden;
  /* Opaque, and the same black as the lip, so the control reads as part of the
   * casing rather than as a tinted window onto the red behind it. */
  background: #141416;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
}
.band-key {
  display: grid; place-items: center; min-width: 50px; padding: 0 var(--sp-2);
  background: none; border: 0; color: #fff; line-height: 1; cursor: pointer;
  /* A hairline between segments rather than a gap, so they stay one block. */
  box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.16);
  font-size: var(--text-sm);
}
.band-pad > :last-child { box-shadow: none; }
.band-key:hover { background: rgba(255, 255, 255, 0.12); }
.band-key:active { background: rgba(0, 0, 0, 0.35); }
/* The readout is not a control, so it does not take a pointer or a hover.
 *
 * White on black is the one place on the device that inverts, which is the
 * point: it is the only segment showing a value rather than offering an
 * action. The ring is the same black as the housing, so the white sits in a
 * gap in the block rather than on top of it. */
.band-key-dex {
  cursor: default; min-width: 78px;
  font-family: var(--font-mono); font-weight: 700; font-size: var(--text-3xs);
  letter-spacing: 0.04em;
  background: #f4f4f2; color: #141416;
  box-shadow: inset 0 0 0 2px #141416;
}
.band-key-dex:hover { background: #f4f4f2; }

/* The screen is a pane of glass spanning the full width between the two
 * frames â€” edge to edge, with no red down the sides.
 *
 * It previously carried a horizontal margin, which left a red strip on each
 * edge and made the device read as a bezel wrapped all the way around a small
 * screen rather than as a top and bottom frame holding a full-width panel. */
body.is-immersive .device-screen {
  margin: 0;
  border: 0;
  border-radius: 0;
  overflow: hidden;
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.55), inset 0 -2px 6px rgba(0, 0, 0, 0.55);
  background: var(--page);
}
body.is-immersive #main { height: auto; flex: 1 1 auto; min-height: 0; }

/* In immersive layout the page IS the PokÃ©dex, so the stage must not contain a
 * second one. It shows the creature instead â€” which is what the big screen on
 * the real device is for. */
body.is-immersive body.is-immersive .stage .flatcard { display: flex; }

/* ---- the wide Kalos layout -------------------------------------------
 *
 * Two columns, not three.
 *
 * The three-column arrangement gave the middle third of a 1400px screen to a
 * single sprite with a caption under it, and squeezed forty numbers, a radar,
 * two ability cards and a similarity list into a 340px gutter on the right.
 * The dead space was the largest element on the page and the data was the
 * smallest.
 *
 * So the creature moves to the top of the left column, directly above the list
 * it belongs to, and the data takes everything else â€” flowing into as many
 * columns as the width allows rather than one narrow ribbon. */
@media (min-width: 1025px) {
  body.is-immersive .view-dex {
    grid-template-columns: 320px minmax(0, 1fr);
    grid-template-rows: minmax(200px, 0.8fr) minmax(0, 1fr);
  }
  body.is-immersive .stage {
    grid-column: 1; grid-row: 1;
    border-bottom: 1px solid var(--border);
  }
  body.is-immersive .rail {
    grid-column: 1; grid-row: 2;
    border-right: 1px solid var(--border);
  }
  body.is-immersive .detail {
    grid-column: 2; grid-row: 1 / 3;
    border-left: 1px solid var(--border);
    padding: var(--sp-4) var(--sp-5);
  }

  /* The head stays one column â€” a name is not a thing to columnise â€” and the
   * tab content flows into as many tracks as fit. A grid rather than CSS
   * multi-column: multicol inside a vertically scrolling box overflows
   * sideways instead of growing downwards, which is the opposite of what a
   * scrolling panel wants. */
  /* Capped, not unbounded.
   *
   * `auto-fill` alone made six 230px columns on a 2000px monitor â€” narrower
   * and harder to read than the three it makes on a laptop, which is the wrong
   * way round. The cap keeps columns at a readable width and lets the panel
   * stop rather than stretching a stat table across a metre of glass. */
  body.is-immersive .tabbody {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: var(--sp-3) var(--sp-5);
    /* stretch, not start: every card in a row is the height of the tallest,
     * so the row reads as one band rather than as three cards that happen to
     * begin at the same y. Measured raggedness was 91px on a ~300px card,
     * which is small enough that the content can absorb it without looking
     * marooned. See the flex rules below, which hand the slack to the rows
     * rather than leaving it in a pool at the bottom. */
    align-items: stretch;
    max-width: 1500px;
  }
  /* Each section becomes a card, so the columns read as distinct modules
   * rather than as text that happens to have wrapped into three streams. */
  body.is-immersive .tabbody > .detail-section {
    margin-top: 0; padding: var(--sp-3) var(--sp-4) var(--sp-4);
    background: var(--surface-raised); border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    display: flex; flex-direction: column;
  }

  /* Where the slack goes.
   *
   * A card stretched to its neighbour's height has to give that height to
   * something, or it is just a taller box with the same content pinned to the
   * top, which is the thing that looks unfinished. So the content rows take
   * it: seven stat rows each grow a little, eighteen matchup cells each grow
   * a little, and the card reads as filled rather than padded.
   *
   * Distributing per row rather than with one space-between gap also fails
   * gracefully. If a neighbour is ever wildly taller, every row grows a bit
   * instead of two items being flung to opposite ends of the card. */
  body.is-immersive .tabbody > .detail-section > .statrow,
  body.is-immersive .tabbody > .detail-section > .abilitycard,
  body.is-immersive .tabbody > .detail-section > .matchgrid,
  body.is-immersive .tabbody > .detail-section > .simlist,
  body.is-immersive .tabbody > .detail-section > .kv,
  body.is-immersive .tabbody > .detail-section > .formstats,
  body.is-immersive .tabbody > .detail-section > .minitable {
    flex: 1 1 auto; min-height: 0;
  }
  /* The radar is a fixed-aspect SVG; growing its box would only add empty
   * space around it. Its legend goes to the floor instead, which lines the
   * legends up across the row. */
  body.is-immersive .tabbody > .detail-section > .legend { margin-top: auto; }
  /* Trailing captions sit on the floor of the card, so the little grey lines
   * align across a row instead of floating at six different heights. */
  body.is-immersive .tabbody > .detail-section > .panel-foot:last-child { margin-top: auto; margin-bottom: 0; }
  body.is-immersive .tabbody > .detail-section > h3 { margin-bottom: var(--sp-3); }
  /* Tabs that are one big section â€” Moves, Locations, Sprites, Evolution â€”
   * span every column instead of being trapped in a 310px track while the
   * rest of the panel sits empty. Their own internal grids then use the width. */
  body.is-immersive .tabbody > .detail-section.is-wide { grid-column: 1 / -1; }
  /* The role card is already a card; inside another it just doubles the box. */
  body.is-immersive .rolecard { background: none; border: 0; border-left-width: 3px; padding: 0 0 0 var(--sp-3); }

  /* Within a tab that is one big section. Moves, Locations, the groups
   * inside it get the same treatment, so a learnset uses the width too.
   *
   * 400px, not 300px, because that is what the table actually needs: seven
   * columns whose min-content adds to 393px, of which the nine generation
   * dots are 99px on their own. A table cannot render narrower than its
   * min-content, so a 300px track did not squeeze it, it let the table
   * overflow 90px into the next track, which is why the level-up dots sat on
   * top of the neighbouring list. Sizing the track to the content also lands
   * on two columns at a typical panel width, which is the arrangement that
   * reads best: level-up down the left, egg moves and TMs down the right. */
  body.is-immersive .movegroups {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
    gap: var(--sp-2) var(--sp-5);
    align-items: start;
  }
  body.is-immersive .detail-head,
  body.is-immersive .typebadges,
  body.is-immersive .flavor,
  body.is-immersive .tabs,
  body.is-immersive .teamadd { max-width: 900px; }
}

/* The device reads as one object, so its outer corners are rounded and the
 * whole thing carries a single shadow rather than the bands each having one. */
body.is-immersive .device {
  border-radius: 18px; overflow: hidden;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 18px 60px rgba(0, 0, 0, 0.55);
}
body.is-immersive .device-band-top { border-radius: 17px 17px 0 0; }
body.is-immersive .device-band-bottom { border-radius: 0 0 17px 17px; }

/* On a phone the bands are mostly cost. The top one shrinks to a thin lip
 * rather than vanishing, so the device still reads as a device; the bottom one
 * keeps its height because it is also the primary navigation and the segments
 * have to stay big enough to hit. */
@media (max-width: 720px) {
  /* The top band was a 10px lip on a phone because it held nothing. Giving it
     the nameplate costs real height: the brand had been sharing the tabs' row,
     not occupying one of its own, so moving it out freed width and no vertical
     space at all. Measured, the header goes from 119px to 137px.

     18px for the name being on the shell rather than beside a button that says
     nearly the same word. The plate is tightened here to keep it to that. */
  body.is-immersive .device-band-top { height: 28px; }
  body.is-immersive .device-band-bottom { height: 42px; }
  /* The plate itself is tightened further down the file, after the rule it
     has to override. This block sits above it. */
  body.is-immersive .band-key { min-width: 62px; }
  body.is-immersive .band-key-dex { min-width: 78px; }
  body.is-immersive .device { border-radius: 0; }
  body.is-immersive .device-screen {
    margin: 0; border-radius: 0; box-shadow: inset 0 0 0 2px #141416;
  }
}

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• topbar â•â• */

/* min-height, not height, and the bar is allowed to wrap.
 *
 * It used to be a fixed 56px with `flex-wrap` only past a breakpoint, and
 * #main was sized as `calc(100vh - 56px)`. On a phone the bar really renders
 * at 143px, so main overshot the window by 87px and the layout hung off the
 * bottom. Nothing now assumes a header height. Main simply takes what is
 * left (see #main below). */
.topbar {
  min-height: var(--header-h);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-2) var(--sp-4);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: relative;
  z-index: 20;
  flex: none;
}

/* Stamped into the casing, not printed on it.
 *
 * A dark fill with the shadow falling INWARD from the top edge and a hairline
 * of light along the bottom is what reads as engraved. The reverse of those
 * two, light from above and shadow below, would read as a raised button, which
 * is what the controls in the lower band are and what this must not be
 * mistaken for. */
.brand {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: flex; align-items: center; gap: 7px;
  height: 26px; padding: 0 13px; border-radius: 5px;
  background: rgba(0, 0, 0, 0.30);
  box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.45),
              inset 0 -1px 0 rgba(255, 255, 255, 0.10),
              0 1px 0 rgba(255, 255, 255, 0.09);
}

/* A Poke Ball drawn in CSS rather than shipped as an image, it is two
 * gradients and a dot, and an <img> for it would be a network request that can
 * fail in front of the site's name. */
.brand-ball {
  width: 16px; height: 16px; border-radius: 50%;
  background:
    linear-gradient(to bottom, var(--brand) 0 46%, #111 46% 54%, #f2f2f2 54% 100%);
  border: 2px solid #111;
  position: relative;
}
.brand-ball::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 5px; height: 5px; border-radius: 50%;
  background: #f2f2f2; border: 1.5px solid #111;
}

.brand-text {
  font-size: var(--text-sm); letter-spacing: 0.02em; white-space: nowrap;
  color: #f4e6e8;
  /* A shadow cast down and inward, the same direction as the plate's. */
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.55);
}
.brand-text b { font-weight: 700; }

.viewswitch {
  display: flex; gap: 2px; padding: 3px;
  background: var(--surface-sunken); border-radius: var(--radius); flex: none;
}
.viewswitch-btn {
  padding: 5px 14px; border: 0; border-radius: 6px; background: transparent;
  color: var(--ink-secondary); cursor: pointer; font-size: var(--text-sm);
  font-weight: 500;
}
.viewswitch-btn:hover { color: var(--ink); background: var(--surface-hover); }
.viewswitch-btn.is-active { background: var(--brand); color: #fff; }

.search {
  flex: 1 1 auto; max-width: 420px; position: relative;
  display: flex; align-items: center;
}
.search-icon {
  position: absolute; left: 10px; width: 15px; height: 15px;
  color: var(--ink-muted); pointer-events: none;
}
#search {
  width: 100%; padding: 7px 34px 7px 32px;
  background: var(--surface-sunken); color: var(--ink);
  border: 1px solid var(--border); border-radius: var(--radius);
  font-size: var(--text-sm);
}
#search::placeholder { color: var(--ink-faint); }
#search:focus { border-color: var(--series-1); outline: none; }
.search-kbd {
  position: absolute; right: 8px; font-family: var(--font-mono);
  font-size: var(--text-2xs); color: var(--ink-muted);
  border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px;
  pointer-events: none;
}
.search:focus-within .search-kbd { display: none; }

/* Must be allowed to shrink. With `flex: none` this group stayed at its
 * natural 641px inside a 361px bar, which put the sprite selector and the
 * shiny toggle past the right edge of a phone screen â€” clipped, and so
 * completely unusable rather than merely awkward. */
/* Two icon buttons, so this no longer needs to grow. It used to hold a label
   and a select wide enough for "Black / White, 1025 of 1025", which took the
   width the search box wanted. */
.spritepicker {
  display: flex; align-items: center; gap: var(--sp-2);
  flex: none; margin-left: auto;
}

.spritemenu { position: relative; flex: none; }
/* Small and centred in a 30px button, matching the cog beside it.
   16px, not 20: the art is a 16x16 grid and only stays crisp at whole
   multiples of 16. At 20px each source pixel covered 1.25 device pixels, so
   some columns rendered one wide and some two. 16 is 1:1. */
.spritemenu-btn {
  display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: var(--radius); cursor: pointer;
  background: var(--surface-sunken); border: 1px solid var(--border);
  color: var(--ink-muted);
}
.spritemenu-btn svg { width: 16px; height: 16px; display: block; }
.gridtoggle svg { fill: currentColor; }
/* Pressed reads as the view you are in, so it carries the brand fill the nav
   tabs use rather than the quieter hover state. */
.gridtoggle[aria-pressed="true"] {
  background: var(--brand); border-color: var(--brand); color: #fff;
}
.spritemenu-btn:hover,
.spritemenu-btn[aria-expanded="true"] { color: var(--ink); border-color: var(--border-strong); }
.spritemenu-panel {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
  width: 264px; padding: var(--sp-3);
  background: var(--surface-raised); border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
}
.spritemenu-panel select { width: 100%; margin-bottom: var(--sp-3); }
.spritemenu-panel .settings-note { margin-bottom: 0; }

select, .field select {
  background: var(--surface-sunken); color: var(--ink);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 6px 8px; font-size: var(--text-sm); cursor: pointer;
  /* A <select> has a wide intrinsic minimum from its longest option
   * ("HeartGold / SoulSilver â€” 508 of 1025"), and will refuse to shrink
   * below it without this. */
  min-width: 0; max-width: 100%; text-overflow: ellipsis;
}
#sprite-set { flex: 0 1 auto; min-width: 7ch; }
select:focus { border-color: var(--series-1); outline: none; }

.chip {
  padding: 5px 11px; border-radius: 999px; cursor: pointer;
  background: var(--surface-sunken); color: var(--ink-secondary);
  border: 1px solid var(--border); font-size: var(--text-sm); white-space: nowrap;
}
.chip:hover { background: var(--surface-hover); color: var(--ink); }
/* Deeper than --series-1 so white clears 4.5:1 on it (the palette blue gives
 * 3.6, fine behind a chart glyph, not behind an 11px label). */
.chip.is-on, .chip[aria-pressed="true"] {
  background: #2a6fc4; border-color: #3d81d6; color: #fff;
}
.chip-toggle .shiny-star { color: var(--warning); }
.chip[aria-pressed="true"] .shiny-star { color: #fff; }

.btn {
  padding: 6px 12px; border-radius: var(--radius); cursor: pointer;
  border: 1px solid var(--border); background: var(--surface-raised);
  font-size: var(--text-sm);
}
.btn:hover { background: var(--surface-hover); }
.btn-ghost { background: transparent; color: var(--ink-secondary); }
.btn-ghost:hover { color: var(--ink); background: var(--surface-hover); }
.link { color: var(--series-1); text-decoration: none; }
.link:hover { text-decoration: underline; }

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• views â•â• */

/* Takes whatever the header leaves, rather than subtracting an assumed header
 * height. The bar wraps to two or three rows on a phone, and any hard-coded
 * figure is wrong the moment it does. */
#main { flex: 1 1 auto; min-height: 0; position: relative; }
.view { position: absolute; inset: 0; }
.view[hidden] { display: none; }

.view-dex {
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr) var(--detail-w);
}

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• rail â•â• */

.rail {
  display: flex; flex-direction: column; min-height: 0;
  background: var(--surface-sunken); border-right: 1px solid var(--border);
}
.rail-head {
  display: flex; align-items: baseline; gap: 6px;
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border);
}
.rail-count { font-family: var(--font-mono); font-size: var(--text-md); font-weight: 600; }
.rail-count-label { font-size: var(--text-xs); color: var(--ink-muted); }
.rail-clear {
  margin-left: auto; background: none; border: 0; cursor: pointer;
  color: var(--series-1); font-size: var(--text-xs);
}

/* Generation jumps. Nine taps reach anywhere in the dex. */
/* Five to a row, fixed, rather than however many happen to fit.
   Nine chips wrapping left the ninth alone on its own line, and the break
   moved with every width. Two rows of five sits still, and Generation X drops
   into the empty tenth slot without any of this changing. */
.railjumps {
  display: grid; grid-template-columns: repeat(5, 19px); gap: 2px;
  margin-left: auto; flex: none;
}
.railjump {
  width: 19px; height: 19px; padding: 0; border-radius: 4px; cursor: pointer;
  background: var(--surface-hover); border: 0; color: var(--ink-muted);
  font-family: var(--font-mono); font-size: var(--text-2xs); line-height: 1;
}
.railjump:hover:not(:disabled) { background: var(--surface-raised); color: var(--ink); }
.railjump.is-on { background: var(--brand); color: #fff; }
.railjump:disabled { opacity: 0.25; cursor: default; }

/* Fast travel. One drag spans the whole list; the bubble names where you are.
 * The native range input carries keyboard support and a proper thumb for
 * free, which a hand-built scrubber would have to reimplement badly. */
.railscrub { position: relative; flex: none; padding: 2px var(--sp-3) 5px; background: var(--surface-sunken); }
.railscrub input {
  width: 100%; height: 14px; margin: 0; cursor: grab;
  accent-color: var(--brand); background: transparent;
}
.railscrub input:active { cursor: grabbing; }
.railscrub-bubble {
  position: absolute; bottom: 22px; transform: translateX(-50%);
  padding: 3px 8px; border-radius: var(--radius-sm); white-space: nowrap;
  background: var(--surface-raised); border: 1px solid var(--border-strong);
  font-size: var(--text-xs); box-shadow: var(--shadow); pointer-events: none;
  z-index: 5;
}

/* Virtualised list: a tall empty sizer sets the scroll height and only the
 * visible rows are in the DOM. 1025 rows each with a sprite is otherwise a
 * thousand image requests and a visibly janky scroll. */
/* overflow-x hidden, not visible. The virtualiser positions rows absolutely
   and a row that momentarily measures a pixel wide gave the rail a horizontal
   scrollbar with stepper arrows along its bottom edge. There is nothing to
   scroll to sideways. */
.rail-scroll {
  flex: 1; overflow-y: auto; overflow-x: hidden; position: relative; outline: none;
}
.rail-sizer { position: absolute; top: 0; left: 0; width: 1px; }
.rail-items { position: absolute; top: 0; left: 0; right: 0; }

.rail-row {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: 0 var(--sp-3); height: 48px; cursor: pointer;
}
.rail-row:hover { background: var(--surface-hover); }
/* Same rule as the analytics rail: the selected row is filled, not edged. */
.rail-row.is-selected { background: var(--brand-wash); }
.rail-sprite {
  width: 34px; height: 34px; flex: none; object-fit: contain;
  image-rendering: pixelated;    /* retro sprites are 64px; smoothing them is a crime */
}
.rail-meta { min-width: 0; flex: 1; }
.rail-name { font-size: var(--text-sm); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rail-no { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--ink-muted); }
.rail-types { display: flex; gap: 3px; }
.rail-pip { width: 8px; height: 8px; border-radius: 2px; }

.rail-empty { padding: var(--sp-6) var(--sp-4); text-align: center; color: var(--ink-muted); font-size: var(--text-sm); }

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• stage â•â• */

.stage {
  position: relative; min-width: 0;
  background:
    radial-gradient(ellipse 70% 55% at 50% 42%, #24242a 0%, var(--surface-sunken) 55%, #08080a 100%);
  overflow: hidden;
}

@keyframes wobble {
  0%, 100% { transform: rotate(-16deg); }
  50%      { transform: rotate(16deg); }
}

/* Above the sprite card.
 *
 * .flatcard is position:absolute inset:0 and covers the whole stage, and
 * neither it nor these controls set a z-index, so paint order fell back to
 * DOM order and the card, coming last, sat on top of Shiny, Add to team and
 * the save star. Every one of them still fired on element.click(), because
 * that skips hit-testing entirely; only a real pointer landed on the card.
 * These are chrome over the stage, so they say so. */
.stage-tools { position: absolute; z-index: 2; top: var(--sp-3); right: var(--sp-3); display: flex; gap: 6px; }
.stage-toggle {
  padding: 5px 10px; border-radius: var(--radius);
  background: rgba(12, 12, 12, 0.7); border: 1px solid var(--border);
  color: var(--ink-secondary); cursor: pointer; font-size: var(--text-xs);
  font-weight: 600; letter-spacing: 0.04em;
}
.stage-toggle:hover:not(:disabled) { color: var(--ink); }
.stage-toggle.is-off { color: var(--ink-faint); }
.stage-toggle:disabled { opacity: 0.35; cursor: not-allowed; }

.stage-shiny .shiny-star { color: var(--warning); }
.stage-shiny.is-on {
  background: var(--warning); border-color: var(--warning); color: #241c00;
}
.stage-shiny.is-on .shiny-star { color: #241c00; }

/* Acting on a PokÃ©mon happens where the PokÃ©mon is. */
/* One positioned stack holding both bottom bars. Only this element is placed;
 * the rows inside it just flow, so neither has to know the other's height. */
.stage-bottom {
  position: absolute; z-index: 2; left: 0; right: 0; bottom: 0;
  display: flex; flex-direction: column; gap: 6px;
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
}

/* The captured dex control.
 *
 * Sits under the action row rather than in it: catching and team-building are
 * things you do to play with the app, and this is a record you keep. Mixing
 * them in one row made all four read as equally weighted verbs. */
.dexmark { display: flex; align-items: center; gap: var(--sp-2); }
.dexmark-set {
  display: flex; gap: 2px; flex: 1;
  background: rgba(12, 12, 12, 0.72); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 2px;
}
.dexmark-btn {
  flex: 1; padding: 4px 6px; border: 0; border-radius: calc(var(--radius) - 3px);
  background: transparent; color: var(--ink-muted); cursor: pointer;
  font-size: var(--text-2xs); font-weight: 600; white-space: nowrap;
}
.dexmark-btn:hover { background: var(--surface-hover); color: var(--ink); }
/* Each rung gets its own colour so the state is readable without reading:
 * green caught, gold shiny, and both lit at once when you have a caught shiny.
 * State 0 and state 1 have no button, so no rules here.
 *
 * The gold was #9a6b12, which put white on it at 4.68:1. Over the line by a
 * rounding error, on 13px text. Deeper amber holds the same meaning at 5.6:1
 * and stays clearly apart from the green beside it. */
.dexmark-btn.is-on { color: #fff; }
.dexmark-btn[data-state="0"].is-on { background: var(--surface-hover); color: var(--ink-secondary); }
.dexmark-btn[data-state="2"].is-on { background: #1f7a34; }
.dexmark-btn[data-state="3"].is-on { background: #8a5f0f; }

/* The catch button lived here. Team building moved to the Collection tab,
 * where you pick from Pokemon you have actually caught rather than from all
 * 1025, so a permanent "Add to team" on the creature had nothing left to
 * mean that the bench does not say better.
 */

.rail-team {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: linear-gradient(to bottom, var(--brand) 0 45%, #111 45% 55%, #f2f2f2 55% 100%);
  border: 1px solid #111;
}
.gridchip-team {
  position: absolute; top: 4px; left: 4px; z-index: 1;
  width: 10px; height: 10px; border-radius: 50%;
  background: linear-gradient(to bottom, var(--brand) 0 45%, #111 45% 55%, #f2f2f2 55% 100%);
  border: 1px solid #111;
}

/* ---- where to find it -------------------------------------------------- */

.bestspot {
  display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-3); margin-bottom: var(--sp-3);
  background: var(--surface-raised); border: 1px solid var(--border);
  border-radius: var(--radius);
}
.bestspot-tag {
  font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-muted); font-weight: 700;
}
.bestspot-where { font-size: var(--text-md); font-weight: 700; }
.bestspot-meta { font-size: var(--text-xs); color: var(--ink-muted); }
.bestspot-meta b { color: var(--ink-secondary); font-family: var(--font-mono); }

/* ---- the places table -------------------------------------------------- */

/* Capped, and left-aligned. Five short columns stretched across 1100px put
 * "Route 2" and "100%" a foot apart with nothing in between, which is a long
 * way for the eye to travel to join two facts. */
.spottable { max-width: 760px; }
.spottable td, .spottable th { padding: 5px var(--sp-3); }
.spot-how { color: var(--ink-secondary); }
/* The region as a band across the single table, rather than a heading above
 * a table of its own. */
.spotregion th {
  position: static; background: var(--surface-sunken);
  padding: 6px var(--sp-3) 5px; border-bottom: 1px solid var(--border);
  font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-secondary); font-weight: 700; text-align: left; cursor: default;
}
.spotregion th .tab-n { background: var(--surface-hover); color: var(--ink-muted); }
.spotregion-more {
  margin-left: 6px; font-size: var(--text-3xs); font-weight: 400; letter-spacing: 0.02em;
  text-transform: none; color: var(--ink-faint);
}

/* Where in the world, as proportions â€” the closest thing to a map the data
 * actually supports. */
.regionspread {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 5px var(--sp-3); margin-bottom: var(--sp-4);
}
.regionchip { display: grid; grid-template-columns: 62px 1fr 22px; align-items: center; gap: var(--sp-2); }
.regionchip-name { font-size: var(--text-xs); color: var(--ink-secondary); }
.regionchip-bar { height: 5px; background: var(--surface-sunken); border-radius: 3px; overflow: hidden; }
.regionchip-bar > span { display: block; height: 100%; background: var(--series-1); border-radius: 3px; }
.regionchip-n { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--ink-muted); text-align: right; }
.regiongroup { margin-bottom: var(--sp-4); }
/* The caption has to clear the bottom bar. One bar now, not two, the
 * action row went with Add to team, so this is roughly its height plus a
 * gap, rather than the 96px the pair used to need. */
.stage-bottom ~ .flatcard, .stage.is-flat .flatcard { padding-bottom: 58px; }

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• boot â•â• */

/* The landing state. The 3D stage is already live behind this, the device
 * you are choosing between is the real one, shut, spinning slowly. So the
 * overlay has to leave the middle of the screen empty and put its own content
 * top and bottom, which is what boot-spacer is for. */






/* While booting, the app chrome is out of the way and the stage owns the
 * screen, the device is the only thing to look at. */

.topbar, .rail, .detail, .stage-tools {
  transition: opacity 0.55s ease;
}
/* The grid collapse itself is defined at the end of this file, after the
 * responsive block, see the note there. */
.view-dex {
  transition: grid-template-columns 0.55s cubic-bezier(0.4, 0, 0.2, 1),
              grid-template-rows 0.55s cubic-bezier(0.4, 0, 0.2, 1);
}

/* The flat fallback card, shown when WebGL is missing or 3D is toggled off.
 * Not an error state. Some people just want the sprite bigger. */
.flatcard {
  position: absolute; inset: 0; display: none;
  flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-2); padding: var(--sp-4) var(--sp-4) var(--sp-3);
}
.stage.is-flat .flatcard { display: flex; }
/* Grows into whatever the panel gives it instead of sitting at a fixed size,
 * so the creature fills its half of the clamshell.
 *
 * `min-height: 0` is load-bearing: a flex child's default min-height is auto,
 * which refuses to shrink below the image's intrinsic size, and the caption
 * underneath gets pushed out of the panel on short windows. */
.flatcard-sprite {
  flex: 1 1 auto; min-height: 0; width: 100%; max-width: 560px;
  object-fit: contain; image-rendering: pixelated;
  filter: drop-shadow(0 18px 30px rgba(0, 0, 0, 0.6));
}
.flatcard-caption { text-align: center; }
.flatcard-name { font-size: var(--text-xl); font-weight: 700; }
.flatcard-no, .flatcard-set { font-family: var(--font-mono); color: var(--ink-muted); font-size: var(--text-xs); }

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• detail â•â• */

.detail {
  overflow-y: auto; min-height: 0;
  background: var(--surface); border-left: 1px solid var(--border);
  padding: var(--sp-4);
}
.detail-head { margin-bottom: var(--sp-4); }
.detail-no { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--ink-muted); }
.detail-name { font-size: var(--text-xl); line-height: 1.15; margin: 2px 0 2px; }
.detail-genus { color: var(--ink-secondary); font-size: var(--text-sm); font-style: italic; }

.typebadges { display: flex; gap: 6px; margin: var(--sp-3) 0; flex-wrap: wrap; }
/* The colour is recognition; the text is the information. Never one without
 * the other, see the note in tokens.css. */
.typebadge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px 3px 7px; border-radius: 999px;
  font-size: var(--text-xs); font-weight: 600; letter-spacing: 0.03em;
  background: var(--surface-raised); border: 1px solid var(--border);
  text-transform: capitalize; cursor: pointer;
}
.typebadge-dot { width: 9px; height: 9px; border-radius: 3px; flex: none; }
.typebadge:hover { border-color: var(--border-strong); }

.flavor {
  font-size: var(--text-sm); color: var(--ink-secondary);
  border-left: 2px solid var(--axis); padding-left: var(--sp-3); margin: var(--sp-4) 0;
}

.detail-section { margin-top: var(--sp-5); }
.detail-section > h3 {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-muted); margin-bottom: var(--sp-3); font-weight: 600;
}

.statrow { display: grid; grid-template-columns: 62px 34px 1fr 40px; align-items: center; gap: var(--sp-2); margin-bottom: 5px; }
.statrow-label { font-size: var(--text-xs); color: var(--ink-secondary); }
.statrow-value { font-family: var(--font-mono); font-size: var(--text-xs); text-align: right; }
.statrow-track { position: relative; height: 7px; background: var(--surface-sunken); border-radius: 4px; overflow: hidden; }
/* The dex median for this stat. Without it a bar is a length with nothing to
 * be long or short compared to. */
.statrow-median {
  position: absolute; top: -2px; bottom: -2px; width: 2px;
  background: var(--ink-muted); opacity: 0.85; border-radius: 1px;
}
.statrow-pct {
  font-family: var(--font-mono); font-size: var(--text-2xs); text-align: right;
  color: var(--ink-muted);
}
/* The % sign was 8px at 70% opacity, which multiplies down to well under the
 * text minimum. It is a unit marker, so it can be small, but it has to be
 * the same ink as the number it belongs to. */
.statrow-pct small { font-size: var(--text-3xs); opacity: 1; margin-left: 1px; }
.statrow-pct.is-high { color: var(--ink-green); }
.statrow-pct.is-low { color: var(--ink-red); }
.statrow-total { margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--grid); }
.statrow-total .statrow-label { font-weight: 600; color: var(--ink); }
.statrow-total .statrow-value { font-weight: 700; }
.statrow-rank { font-size: var(--text-2xs); color: var(--ink-muted); font-family: var(--font-mono); }

/* The derived archetype, sitting with the type badges. All that survives of
 * the card it used to be; the four meters it carried duplicated the stat bars
 * below them. */
.rolechip {
  display: inline-flex; align-items: center; padding: 3px 10px;
  border-radius: 999px; font-size: var(--text-2xs); font-weight: 600;
  background: var(--surface-raised); border: 1px dashed var(--border-strong);
  color: var(--ink-secondary); cursor: help;
}

/* ---- display settings -------------------------------------------------- */

.settings { position: relative; flex: none; }
.settings-btn {
  width: 30px; height: 30px; border-radius: var(--radius); cursor: pointer;
  background: var(--surface-sunken); border: 1px solid var(--border);
  color: var(--ink-muted); font-size: var(--text-base); line-height: 1;
}
.settings-btn:hover, .settings-btn[aria-expanded="true"] { color: var(--ink); border-color: var(--border-strong); }
.settings-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 40;
  width: 264px; padding: var(--sp-3);
  background: var(--surface-raised); border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
}
.settings-title {
  font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-muted); margin-bottom: var(--sp-2);
}
.settings-row { display: flex; justify-content: space-between; align-items: baseline; font-size: var(--text-xs); }
.settings-value { font-family: var(--font-mono); color: var(--ink-secondary); }
.settings-menu .slider { width: 100%; margin: var(--sp-2) 0; }
.settings-note { margin: 0 0 var(--sp-3); font-size: var(--text-3xs); color: var(--ink-muted); line-height: 1.5; }
.settings-reset { width: 100%; }

/* The menu grew a second section, so it needs a scroll bound: a popover taller
 * than the window cannot be closed. */
.settings-menu { max-height: min(78vh, 620px); overflow-y: auto; }
.settings-title-sep {
  margin-top: var(--sp-4); padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
}
.settings-note a { color: var(--ink-blue); text-decoration: underline; text-underline-offset: 2px; }
.settings-note a:hover { color: var(--ink); }
.settings-status {
  margin: 0 0 var(--sp-2); font-size: var(--text-2xs);
  color: var(--ink-secondary); line-height: 1.5;
}
.settings-status b { font-family: var(--font-mono); color: var(--ink); }
/* Off, and looking off. A sign-in button that appears live but does nothing
 * is worse than no button at all. */
#sign-in:disabled { opacity: 0.5; cursor: not-allowed; }
/* The part a lawyer would care about, so it does not read as a footnote. */
.settings-sig {
  margin-top: var(--sp-4); padding-top: var(--sp-3);
  border-top: 1px solid var(--grid);
  text-align: center; font-size: var(--text-2xs); color: var(--ink-muted);
}
/* No `color` here. The heart carries VS16, so it renders as a colour emoji
 * glyph and paints itself, a colour declaration on it would be dead CSS that
 * a later contrast sweep would flag as a 3:1 failure on a value nothing uses. */
.sig-heart { font-style: normal; }
.settings-legal {
  margin-bottom: 0; padding-top: var(--sp-3);
  border-top: 1px solid var(--border); color: var(--ink-faint);
}
.settings-legal b { color: var(--ink-secondary); }

/* ---- the role card ----------------------------------------------------- */

.rolecard {
  background: var(--surface-raised); border: 1px solid var(--border);
  border-left: 3px solid var(--brand);
  border-radius: var(--radius); padding: var(--sp-3);
}
.rolecard-label { font-size: var(--text-md); font-weight: 700; line-height: 1.2; }
.rolecard-why { margin: 4px 0 var(--sp-3); font-size: var(--text-xs); color: var(--ink-secondary); line-height: 1.5; }
.rolecard-marks { display: flex; flex-direction: column; gap: 4px; }
.rolemark { display: grid; grid-template-columns: 52px 1fr 34px; align-items: center; gap: var(--sp-2); }
.rolemark-name { font-size: var(--text-2xs); color: var(--ink-muted); }
.rolemark-meter { height: 5px; background: var(--surface-sunken); border-radius: 3px; overflow: hidden; }
.rolemark-meter > span { display: block; height: 100%; background: var(--series-1); border-radius: 3px; }
.rolemark-pct { font-family: var(--font-mono); font-size: var(--text-2xs); text-align: right; color: var(--ink-secondary); }
.rolemark-pct small { font-size: 8px; opacity: 0.7; }
.rolecard-foot { margin: var(--sp-2) 0 0; font-size: var(--text-3xs); color: var(--ink-faint); }
/* 4px rounded data-end, anchored to the baseline.
 *
 * display:block is load-bearing, not tidiness. This is a <span>, and `width`
 * has no effect on a non-replaced inline element, so the fill sat at 0px
 * wide while carrying a perfectly correct inline `width: 17.6%`, and every
 * stat bar in the app rendered as an empty track. */
.statrow-fill {
  display: block; height: 100%; border-radius: 0 4px 4px 0;
  transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}

.kv { display: grid; grid-template-columns: 1fr auto; gap: 3px var(--sp-3); font-size: var(--text-sm); }
.kv dt { color: var(--ink-muted); }
.kv dd { margin: 0; font-family: var(--font-mono); font-size: var(--text-xs); text-align: right; }

/* Column width follows the text size, not a fixed pixel count.
 *
 * "0.25×" is five monospaced characters, and it is now set at --text-md, so
 * the column has to be about four ems of it. Deriving the minimum from the
 * same token the number uses means the grid gives up a column at 140% instead
 * of cropping a number, which is the failure that actually matters, since a
 * clipped "0.25×" reads as "0.2×". */
.matchgrid {
  display: grid; gap: 4px;
  grid-template-columns: repeat(auto-fit, minmax(calc(var(--text-md) * 4), 1fr));
}

/* The multiplier is what the cell is for, so it is the largest thing in it.
 *
 * It used to be 12px. Smaller than body text, with the type name almost
 * the same size beside it, so the cell read as two equal small labels and the
 * number had to be hunted for. The type name is the caption here; you already
 * know which type you are looking at from where the cell sits. */
.matchcell {
  border-radius: var(--radius-sm); display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 0;
  padding: 6px 3px 5px; cursor: default;
  border: 1px solid rgba(0, 0, 0, 0.28);
  line-height: 1.15; text-align: center;
}
.matchcell-x {
  font-family: var(--font-mono); font-weight: 700;
  font-size: var(--text-md); letter-spacing: -0.02em;
}
/* No opacity here. Dimming ink on an already-coloured cell is how the old
 * version lost its contrast; if it is worth printing it is worth reading. */
.matchcell-t {
  font-size: var(--text-2xs); text-transform: capitalize;
  letter-spacing: 0.01em;
}

/* ---- detail tabs ------------------------------------------------------- */

.tabs {
  display: flex; gap: 2px; flex-wrap: wrap; margin: var(--sp-4) 0 var(--sp-3);
  border-bottom: 1px solid var(--border); padding-bottom: var(--sp-2);
}
.tab {
  padding: 4px 9px; border: 0; border-radius: var(--radius-sm); cursor: pointer;
  background: transparent; color: var(--ink-muted); font-size: var(--text-xs);
  font-weight: 600; white-space: nowrap;
}
.tab:hover { background: var(--surface-hover); color: var(--ink); }
.tab.is-on { background: var(--brand); color: #fff; }
/* A tab with nothing behind it stays clickable â€” it still says "no Mega
 * forms", which is information â€” but it should not compete for attention. */
.tab.is-empty { color: var(--ink-faint); }
.tab.is-empty.is-on { color: #fff; }
/* The count sat on a translucent white pill, which lifted the background
 * toward the muted grey it was written in and left 2.8:1. Dark pill, ink one
 * step up: the number reads, and it still sits behind the tab label. */
.tab-n {
  display: inline-block; min-width: 15px; padding: 0 3px; margin-left: 3px;
  border-radius: 7px; background: rgba(0, 0, 0, 0.34);
  color: var(--ink-secondary);
  font-family: var(--font-mono); font-size: var(--text-3xs); text-align: center;
}
.tab.is-on .tab-n { background: rgba(0, 0, 0, 0.28); color: #fff; }

.subhead {
  font-size: var(--text-xs); font-weight: 600; color: var(--ink-secondary);
  margin: var(--sp-4) 0 var(--sp-2);
}
.subhead .tab-n { background: var(--surface-hover); color: var(--ink-muted); }

/* A learnset table cannot render narrower than its min-content, seven
 * columns and nine generation dots come to about 392px, so on a phone it is
 * 41px wider than the pane. Left alone it pushes the whole tab sideways;
 * given its own scroll container it takes that 41px out of itself instead. */
.movegroup { overflow-x: auto; }

/* Compact tables for the moves and locations tabs. */
.minitable { font-size: var(--text-xs); }

/* The learnset tables share one column grid so they line up when stacked.
 *
 * Fixed layout, because auto layout sizes each table from its own content:
 * three tables one under another each put Move, Type, Cat, Pow and Acc at a
 * different x, and reading down the page meant re-finding the columns at
 * every heading. Fixed widths mean the same column is in the same place in
 * all of them, and a longer move name no longer nudges Type sideways.
 *
 * Move is the only elastic column. Everything else is a number, a short
 * word or nine dots, and none of those want the slack. */
/* Capped, and with no floor: the table always fits its container.
 *
 * The cap is because Move is the only elastic column, so 100% of a wide panel
 * all lands on it, at 913px the table stretched Move to 569px and left a
 * canyon between a move's name and its type.
 *
 * No floor, because a minimum width is just a horizontal scrollbar with extra
 * steps. Move gives up its width first and ellipsises when it runs out; the
 * six columns that hold a number, a short word or nine dots keep theirs,
 * since none of them degrade gracefully. */
.movegroup .minitable {
  table-layout: fixed; width: 100%; max-width: 560px;
}
.movegroup col.c-lv   { width: 32px; }
.movegroup col.c-type { width: 76px; }
.movegroup col.c-cat  { width: 44px; }
.movegroup col.c-pow  { width: 48px; }
.movegroup col.c-acc  { width: 52px; }
.movegroup col.c-gens { width: 92px; }
/* Fixed layout will not shrink a cell to fit, so the one column holding
 * arbitrary-length text has to say what happens when it runs out. */
.movegroup .minitable td:nth-child(2) { overflow: hidden; text-overflow: ellipsis; }
.minitable thead th {
  position: static; background: none; border-bottom: 1px solid var(--grid);
  padding: 3px 6px; font-size: var(--text-2xs); text-transform: uppercase;
  letter-spacing: 0.05em; cursor: default; color: var(--ink-muted);
}
.minitable tbody td { padding: 4px 6px; border-bottom: 1px solid var(--grid); white-space: nowrap; }
.minitable tbody tr { cursor: default; }
.typedot {
  display: inline-block; width: 7px; height: 7px; border-radius: 2px;
  margin-right: 5px; vertical-align: middle;
}
.minitable td { text-transform: capitalize; }
.cat { font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: 0.04em; }
/* Ink variants, not the fill variants, see the note in tokens.css. */
.cat-physical { color: var(--ink-orange); }
.cat-special  { color: var(--ink-blue); }
.cat-status   { color: var(--ink-muted); }
.cell-sub { display: block; font-size: var(--text-3xs); color: var(--ink-muted); text-transform: none; }

/* Generation picker for the learnset. */
.genpicker { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; margin-bottom: var(--sp-3); }
.genpicker-label {
  font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--ink-muted); margin-right: 2px;
}
/* Same chips as the rail, but not the rail's fixed five columns.
   Five wide is a constraint of a 300px sidebar, not a layout decision worth
   inheriting: the detail pane has room for all nine on one line, and forcing
   two rows there would be collapsing something that fits. auto-fill lays down
   as many 19px tracks as the pane allows, so it is one row when there is space
   and wraps on its own when there is not. */
.genpicker .railjumps {
  margin-left: 0; grid-template-columns: repeat(auto-fill, 19px);
  flex: 1 1 auto; min-width: 0;
}

/* Nine fixed slots per move, Gen I on the left: which generations this
 * Pokémon can learn it in. A move that appears, vanishes and returns shows
 * that shape without a single word.
 *
 * The empty slots ARE the scale, and they used to be #2c2c2a on a #232322 row
 *, 1.12:1, which is not a colour, it is the absence of one. Without them a
 * move first learnable in Gen 4 rendered as six dots beginning at no
 * particular place, and "never had it" looked identical to "lost it for two
 * generations". 26 of Bulbasaur's 40 rows start with a gap and 7 have one in
 * the middle, so this was most of the column.
 *
 * Slot width is fixed and the marker scales inside it, so lit and unlit read
 * differently at a glance while every dot stays on its generation's column. */
.gendots { display: inline-flex; gap: 2px; align-items: center; }
.gendot {
  width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: #6f6e68;              /* 3.07:1 on the row, a track, not noise */
  transform: scale(0.7);
}
.gendot.is-on { background: var(--ink-blue); transform: none; }
/* The generation you are currently reading. Always lit, since the list is
 * built for it, so it gives the scale an origin you can find without
 * counting from the left edge. */
.gendot.is-now { box-shadow: 0 0 0 1.5px var(--ink-muted); }
.movechanged { color: var(--warning); font-weight: 700; margin-left: 2px; cursor: help; }

/* Encounter rate as a bar behind the number: a column of percentages is hard
 * to scan, and the comparison is the point. */
.ratebar {
  display: inline-block; width: 34px; height: 5px; border-radius: 3px;
  margin-right: 6px; vertical-align: middle;
  background: linear-gradient(to right, var(--seq-400) var(--r), var(--surface-sunken) var(--r));
}

/* ---- sprite timeline --------------------------------------------------- */

/* Generations run across, sprites run down.
 *
 * Stacked vertically it was a very long scroll, and comparing Gen 3 with Gen 7
 * meant remembering one while you found the other. Side by side you read the
 * whole 28 years in one glance, which is the entire reason the view exists. */
/* Wraps rather than scrolls.
 *
 * Laid out as a single row of columns it always fitted on a wide screen and
 * always needed a sideways scrollbar on anything else â€” and a horizontal
 * scrollbar inside a vertically scrolling panel is a thing people simply do
 * not find. Now the generations flow across and wrap onto the next row, so the
 * only scrolling is the page's own. */
.spritetime {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: var(--sp-4) var(--sp-3); align-items: start;
}
.spritetime-group { display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }
.spritetime-era {
  font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink-muted); font-weight: 700; margin: 0;
  padding-bottom: 4px; border-bottom: 1px solid var(--grid);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.spritetime-row { display: flex; flex-direction: column; gap: var(--sp-2); }
.spritetime-cell {
  margin: 0; width: 100%; padding: 6px 4px; cursor: pointer;
  background: var(--surface-sunken); border: 1px solid var(--border);
  border-radius: var(--radius); text-align: center;
  transition: border-color 0.15s, transform 0.15s;
}
.spritetime-cell:hover { border-color: var(--brand); transform: translateY(-2px); }
.spritetime-cell img {
  width: 100%; height: 62px; object-fit: contain; image-rendering: pixelated;
}
.spritetime-cell figcaption {
  font-size: var(--text-3xs); color: var(--ink-muted); line-height: 1.25; margin-top: 3px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- abilities --------------------------------------------------------- */

.abilitycard {
  background: var(--surface-raised); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--sp-3); margin-bottom: var(--sp-2);
}
.abilitycard-head { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.abilitycard-name { font-weight: 600; font-size: var(--text-sm); }
.abilitycard-flag {
  font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--warning); border: 1px solid currentColor; border-radius: 3px; padding: 0 4px;
}
.abilitycard-share { margin-left: auto; font-size: var(--text-2xs); color: var(--ink-muted); font-family: var(--font-mono); }
.abilitycard-effect { margin: 5px 0 0; font-size: var(--text-xs); color: var(--ink-secondary); line-height: 1.5; }

/* ---- similar ----------------------------------------------------------- */

.simlist { display: flex; flex-direction: column; gap: 3px; }
.simrow {
  display: flex; align-items: center; gap: var(--sp-2); width: 100%;
  padding: 4px 6px; cursor: pointer; text-align: left;
  background: var(--surface-sunken); border: 1px solid transparent;
  border-radius: var(--radius-sm);
}
.simrow:hover { border-color: var(--border-strong); background: var(--surface-hover); }
.simrow img { width: 30px; height: 30px; image-rendering: pixelated; flex: none; }
.simrow-name { flex: 1; font-size: var(--text-xs); }
.simrow-types { display: flex; gap: 3px; }
.simrow-bst { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-muted); }

/* ---- forms ------------------------------------------------------------- */

.formcard {
  background: var(--surface-raised); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--sp-3); margin-bottom: var(--sp-2);
}
.formcard-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.formcard-name { font-weight: 600; font-size: var(--text-sm); }
.formcard-bst { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-muted); }
.formcard-bst.is-up { color: var(--good); }
.formcard-bst.is-down { color: var(--critical); }
.formcard-types { display: flex; gap: 4px; margin: var(--sp-2) 0; flex-wrap: wrap; }
.formcard-flag {
  font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--warning); align-self: center;
}
.formstats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; }
.formstat {
  display: flex; flex-direction: column; align-items: center; gap: 0;
  background: var(--surface-sunken); border-radius: var(--radius-sm); padding: 4px 2px;
}
.formstat-label { font-size: var(--text-3xs); color: var(--ink-muted); }
.formstat-value { font-family: var(--font-mono); font-size: var(--text-xs); }
.formstat-delta { font-family: var(--font-mono); font-size: var(--text-3xs); color: var(--ink-faint); }
.formstat-delta.is-up { color: var(--good); }
.formstat-delta.is-down { color: var(--critical); }

/* ---- history ----------------------------------------------------------- */

.changerow {
  background: var(--surface-raised); border: 1px solid var(--border);
  border-radius: var(--radius); padding: var(--sp-3); margin-bottom: var(--sp-2);
}
.changerow-when {
  font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--ink-muted); margin-bottom: var(--sp-2);
}
.changerow-body { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.changeside { display: flex; gap: 4px; }
.changearrow { color: var(--ink-muted); }

/* Stages across, members down. */
/* Sized to be looked at.
 *
 * The first pass used 54px sprites and 9px captions â€” technically the whole
 * family, practically unreadable, and squandering the width the wide layout
 * had just handed it. The sprite is the point of this view, so it gets to be
 * the biggest thing in it. */
.evorow { display: flex; align-items: flex-start; gap: var(--sp-4); flex-wrap: wrap; }
.evostage {
  display: flex; flex-direction: column; gap: var(--sp-2);
  flex: 1 1 auto; min-width: 150px;   /* flex-grow is set per stage in views.js */
}
.evostage-members {
  display: grid; gap: var(--sp-2);
  grid-template-columns: repeat(auto-fit, minmax(136px, 1fr));
}
.evostage-label {
  font-size: var(--text-2xs); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-muted); font-weight: 700;
  padding-bottom: 5px; border-bottom: 1px solid var(--grid);
}
.evoarrow {
  align-self: center; color: var(--ink-faint);
  font-size: var(--text-xl); padding-top: 26px;
}
.evocard {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: var(--sp-3) var(--sp-2); cursor: pointer; text-align: center; color: inherit;
  background: var(--surface-sunken); border: 1px solid var(--border);
  border-radius: var(--radius); transition: border-color 0.15s, transform 0.15s;
}
.evocard:hover { border-color: var(--border-strong); transform: translateY(-2px); }
.evocard.is-current { border-color: var(--brand); background: var(--surface-raised); cursor: default; }
.evocard.is-current:hover { transform: none; }
.evocard img {
  width: 100%; height: 82px; object-fit: contain; image-rendering: pixelated;
}
.evocard-name { font-size: var(--text-sm); font-weight: 600; }
.evocard-bst { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-muted); }
.evocard-cond {
  font-size: var(--text-2xs); color: var(--ink-secondary); line-height: 1.4;
  margin-top: 5px; padding-top: 5px; border-top: 1px solid var(--grid);
  width: 100%;
}

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• grid â•â• */

.view-grid { display: flex; flex-direction: column; min-height: 0; }

.gridbar {
  display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-4); flex: none;
  background: var(--surface); border-bottom: 1px solid var(--border);
}
.gridbar-count { font-size: var(--text-sm); color: var(--ink-muted); }
.gridbar-count b { color: var(--ink); font-family: var(--font-mono); }
.gridbar-size { margin-left: auto; }
.gridbar-size .slider { width: 120px; }
.gridbar-labels { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.gridbar-labels input { accent-color: var(--brand); cursor: pointer; }

.gridwrap {
  flex: 1; min-height: 0; overflow-y: auto; outline: none;
  display: grid; gap: var(--sp-2); padding: var(--sp-3);
  grid-template-columns: repeat(auto-fill, minmax(var(--chip, 84px), 1fr));
  align-content: start;
}

/* Sized explicitly, not by intrinsic contribution.
 *
 * Left to `grid-auto-rows: auto`, every row collapsed to 13px â€” the height of
 * the two text lines â€” with the sprite overflowing and clipped away by
 * `overflow: hidden`. Whatever the precise cause in the intrinsic-sizing pass,
 * relying on it here buys nothing: the chip size is a number the user picks
 * with a slider, so stating it outright is both simpler and immune. */
.gridchip {
  position: relative; display: flex; flex-direction: column; align-items: center;
  justify-content: center;
  gap: 1px; padding: 6px 3px 5px; cursor: pointer; overflow: hidden;
  /* The sprite's height plus the fixed chrome under it: 11px of padding, 2px
   * of gaps, and two lines of ~11px and ~13px. Derived rather than guessed â€”
   * a round multiplier landed a pixel short and clipped every name. */
  min-height: calc(var(--chip, 84px) * 0.62 + 40px);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); color: inherit;
  transition: transform 0.12s, border-color 0.12s;
}
/* A wash of the primary type behind the sprite. Faint on purpose: enough that
 * type runs and generation blocks show up as bands when you zoom out, not
 * enough to compete with a thousand sprites. */
.gridchip::before {
  content: ""; position: absolute; inset: 0;
  background: var(--wash); opacity: 0.13; pointer-events: none;
}
.gridchip:hover { transform: translateY(-2px); border-color: var(--border-strong); }
.gridchip.is-selected { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand); }
.gridchip img {
  display: block; flex: none;
  width: 100%; height: calc(var(--chip, 84px) * 0.62); object-fit: contain;
  image-rendering: pixelated; position: relative;
}
.gridchip-no {
  font-family: var(--font-mono); font-size: var(--text-3xs); color: var(--ink-muted); position: relative;
}
.gridchip-name {
  font-size: var(--text-2xs); line-height: 1.2; text-align: center; position: relative;
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Labels off: the chips become a pure mosaic and pack much tighter. */
.gridwrap.is-bare .gridchip { padding: 4px; min-height: calc(var(--chip, 84px) * 0.8 + 10px); }
.gridwrap.is-bare .gridchip img { height: calc(var(--chip, 84px) * 0.8); }

@media (max-width: 720px) {
  .gridbar { padding: var(--sp-2) var(--sp-3); gap: var(--sp-2); }
  .gridbar-size { margin-left: 0; }
  .gridwrap { gap: 5px; padding: var(--sp-2) var(--sp-2) 34px; }
}

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• team â•â• */

.viewswitch-n {
  display: inline-block; min-width: 15px; padding: 0 4px; margin-left: 4px;
  border-radius: 8px; background: rgba(255, 255, 255, 0.18);
  font-family: var(--font-mono); font-size: var(--text-3xs); text-align: center;
}
.viewswitch-btn.is-active .viewswitch-n { background: rgba(0, 0, 0, 0.28); }

.teamadd {
  width: 100%; padding: 8px; margin-bottom: var(--sp-2); cursor: pointer;
  border: 1px dashed var(--border-strong); border-radius: var(--radius);
  background: transparent; color: var(--ink-secondary);
  font-size: var(--text-xs); font-weight: 600;
}
.teamadd:hover:not(:disabled) { border-color: var(--brand); color: var(--ink); }
.teamadd.is-on { border-style: solid; border-color: var(--good); color: var(--good); }
.teamadd:disabled { opacity: 0.45; cursor: not-allowed; }

.view-team { overflow-y: auto; }
.teamwrap { max-width: 1400px; margin: 0 auto; padding: var(--sp-4); }
.teamhead { margin-bottom: var(--sp-4); }
.teamhead h2 { font-size: var(--text-lg); }

.teamslots {
  display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: var(--sp-2); margin-bottom: var(--sp-4);
}
@media (max-width: 900px) { .teamslots { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.teamslot {
  position: relative; display: flex; flex-direction: column; align-items: center;
  gap: 2px; padding: var(--sp-3) var(--sp-2); cursor: pointer;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); min-height: 128px; justify-content: center;
}
.teamslot:hover { border-color: var(--border-strong); }
.teamslot.is-empty { border-style: dashed; color: var(--ink-faint); }
.teamslot img { width: 56px; height: 56px; object-fit: contain; image-rendering: pixelated; }
.teamslot-name { font-size: var(--text-xs); font-weight: 600; text-align: center; }
.teamslot-types { display: flex; gap: 3px; }
.teamslot-bst { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--ink-muted); }
.teamslot-plus { font-size: var(--text-xl); line-height: 1; }
.teamslot-hint { font-size: var(--text-2xs); }
.teamslot-drop {
  position: absolute; top: 4px; right: 4px; width: 20px; height: 20px;
  border-radius: 50%; border: 0; cursor: pointer; line-height: 1;
  background: var(--surface-hover); color: var(--ink-muted); font-size: 13px;
}
.teamslot-drop:hover { background: var(--critical); color: #fff; }

.teampanels { display: flex; flex-direction: column; gap: var(--sp-3); }

/* One row per attacking type: the type, a cell per member, and the verdict. */
.threatlist { display: flex; flex-direction: column; gap: 3px; }
.threatrow {
  display: grid; grid-template-columns: 96px 1fr 180px;
  align-items: center; gap: var(--sp-3);
  padding: 3px 6px; border-radius: var(--radius-sm);
}
.threatrow.is-hole { background: rgba(208, 59, 59, 0.12); }
.threatrow-type {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--text-xs); text-transform: capitalize;
}
.threatbar { display: flex; gap: 2px; }
.threatcell {
  flex: 1; min-width: 30px; padding: 3px 0; border-radius: 3px;
  font-family: var(--font-mono); font-size: var(--text-2xs); font-weight: 700; text-align: center;
}
.threatrow-verdict { font-size: var(--text-xs); color: var(--ink-muted); text-align: right; }
/* The ink red, not the fill red: --critical is a chart colour and lands at
 * 3.3:1 as 12px text on the row's tinted background. This is the one word in
 * the coverage table that says "you have a problem", so it is the last one
 * that should be hard to read. */
.threatrow.is-hole .threatrow-verdict b { color: var(--ink-red); }
@media (max-width: 720px) {
  .threatrow { grid-template-columns: 74px 1fr; gap: var(--sp-2); }
  .threatrow-verdict { grid-column: 1 / 3; text-align: left; }
}

.covgrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 4px; }
.covcell {
  display: flex; align-items: center; gap: 6px; padding: 5px 8px;
  border-radius: var(--radius-sm); background: var(--surface-sunken);
  font-size: var(--text-xs); text-transform: capitalize;
}
.covcell.is-blind { background: rgba(208, 59, 59, 0.14); }
.covcell-type { flex: none; }
.covcell-by {
  margin-left: auto; font-size: var(--text-2xs); color: var(--ink-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• analytics â•â• */

.view-analytics { display: flex; flex-direction: column; min-height: 0; }

/* Filters in one row above the charts, per the interaction spec. */
.filterbar {
  display: flex; align-items: center; gap: var(--sp-5); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface); border-bottom: 1px solid var(--border); flex: none;
}
.filterbar-group { display: flex; align-items: center; gap: var(--sp-2); }
.filterbar-label {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--ink-muted); font-weight: 600;
}
.chipset { display: flex; gap: 4px; flex-wrap: wrap; }
.chipset .chip { padding: 3px 9px; font-size: var(--text-xs); }
.chipset-types .chip { padding-left: 6px; display: inline-flex; align-items: center; gap: 5px; text-transform: capitalize; }
.chipset-types .chip-dot { width: 8px; height: 8px; border-radius: 2px; flex: none; }
/* Desktop has room for the chips permanently, so the toggle is mobile-only. */
.filterbar-toggle { display: none; }

.filterbar-actions { margin-left: auto; display: flex; align-items: center; gap: var(--sp-3); }
.filterbar-count { font-size: var(--text-sm); color: var(--ink-muted); }
.filterbar-count b { color: var(--ink); font-family: var(--font-mono); }

.dashboard {
  flex: 1; min-height: 0; overflow-y: auto;
  display: grid; gap: var(--sp-3); padding: var(--sp-3);
  grid-template-columns: repeat(12, minmax(0, 1fr));
  align-content: start;
}

.panel {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: var(--sp-4);
  grid-column: span 6; min-width: 0;
  display: flex; flex-direction: column;
}
.panel-wide  { grid-column: span 8; }
.panel-tall  { grid-column: span 6; }
.panel-full  { grid-column: span 12; }

.panel-tiles { grid-column: span 12; padding: 0; background: none; border: 0; }
/* Scoped to the dashboard's own children.
 *
 * Unscoped, this reached every .panel on the site. Including the collection
 * panels, which live in a two-track grid of their own. "span 12" of two
 * tracks is the whole row, so each of them went full width and their progress
 * bars stretched to 823px to show a 1% fill, with 839px between a label and
 * its own number. A responsive rule for one layout had quietly become a rule
 * about a class name. */
@media (max-width: 1280px) {
  .dashboard > .panel,
  .dashboard > .panel-wide,
  .dashboard > .panel-tall { grid-column: span 12; }
}

/* The dashboard gets denser as the window gets wider rather than just wider.
 *
 * A 2560px monitor showing the same two-panel layout stretched to double width
 * is a worse dashboard, not a better one: the charts gain nothing from the
 * extra pixels and you still scroll to compare anything. Past these widths the
 * panels narrow instead, so more of the argument is on screen at once and the
 * comparisons happen by looking rather than by remembering.
 *
 * The charts already measure their own container, so nothing here needs a
 * matching change in JavaScript, they re-render at whatever width they land
 * at, with type still at its designed size. */
@media (min-width: 1660px) {
  .dashboard { gap: var(--sp-4); padding: var(--sp-4); }
  .panel      { grid-column: span 4; }
  .panel-wide { grid-column: span 5; }
  .panel-tall { grid-column: span 3; }
  .panel-full { grid-column: span 12; }
}
@media (min-width: 2200px) {
  .panel      { grid-column: span 3; }
  .panel-wide { grid-column: span 4; }
  .panel-tall { grid-column: span 3; }
  .panel-full { grid-column: span 6; }
  .dashboard { grid-template-columns: repeat(12, minmax(0, 1fr)); }
}

.panel-head { margin-bottom: var(--sp-3); }
.panel-head h2 { font-size: var(--text-md); }
.panel-sub { margin: 4px 0 0; font-size: var(--text-xs); color: var(--ink-muted); line-height: 1.45; }
.panel-foot { margin-top: var(--sp-3); font-size: var(--text-xs); color: var(--ink-muted); }
.panel-foot b { color: var(--ink-secondary); font-family: var(--font-mono); }

.chart { position: relative; width: 100%; min-height: 0; }
.chart svg { display: block; width: 100%; height: auto; overflow: visible; }

/* Stat tiles: a headline number is not a chart, and four of them beat one
 * chart that tries to say four things. */
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-3); }
.tile {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: var(--sp-3) var(--sp-4);
}
.tile-label { font-size: var(--text-xs); color: var(--ink-muted); text-transform: uppercase; letter-spacing: 0.07em; }
.tile-value { font-size: var(--text-xl); font-weight: 600; line-height: 1.2; margin-top: 2px; }
.tile-note { font-size: var(--text-xs); color: var(--ink-secondary); }

/* Legends: always present for two or more series. */
.legend { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-top: var(--sp-3); font-size: var(--text-xs); color: var(--ink-secondary); }
.legend-item { display: inline-flex; align-items: center; gap: 6px; }
.legend-swatch { width: 11px; height: 11px; border-radius: 3px; flex: none; }
.legend-diverging { gap: 0; }
.legend-diverging .legend-step { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.legend-diverging .legend-chip { width: 46px; height: 11px; }
.legend-diverging .legend-chip:first-child { border-radius: 3px 0 0 3px; }
.legend-caption { color: var(--ink-muted); margin-left: var(--sp-3); }

/* ---- mode switches & the type lens ------------------------------------- */

.modeswitch {
  display: inline-flex; gap: 2px; padding: 3px; margin-bottom: var(--sp-3);
  background: var(--surface-sunken); border-radius: var(--radius);
}
.modeswitch button {
  padding: 4px 12px; border: 0; border-radius: 6px; background: transparent;
  color: var(--ink-muted); cursor: pointer; font-size: var(--text-xs); font-weight: 600;
}
.modeswitch button:hover { color: var(--ink); }
.modeswitch button.is-on { background: var(--brand); color: #fff; }

/* Tightened: the lens had a full --sp-4 between every group and a heading
 * block per column, which pushed the Defending half below the fold on a laptop.
 * The rhythm is now half that, and the multiplier badge sits on the same line
 * as its list rather than above it. */
.lens-picker { display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: var(--sp-3); }
.lens-picker .chip {
  padding: 2px 8px 2px 5px; font-size: var(--text-xs); text-transform: capitalize;
  display: inline-flex; align-items: center; gap: 4px;
}

.lens-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-3) var(--sp-5); }
@media (max-width: 760px) { .lens-grid { grid-template-columns: minmax(0, 1fr); } }

.lens-title {
  font-size: var(--text-xs); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--ink-secondary);
}
.lens-sub { margin: 1px 0 var(--sp-2); font-size: var(--text-2xs); color: var(--ink-muted); }
.lens-row {
  display: grid; align-items: start;
  /* Wide enough for "0.25×" at whatever the reader has set the text to. */
  grid-template-columns: calc(var(--text-2xs) * 4.8) 1fr;
  gap: 4px var(--sp-2); margin-bottom: 6px;
}
.lens-mult {
  grid-row: 1 / 3; padding: 2px 0; border-radius: var(--radius-sm);
  font-family: var(--font-mono); font-size: var(--text-2xs); font-weight: 700; text-align: center;
}
.lens-label { font-size: var(--text-2xs); color: var(--ink-muted); align-self: center; }
.lens-types { display: flex; flex-wrap: wrap; gap: 3px; }
.lens-types .typebadge { cursor: pointer; padding: 2px 7px 2px 5px; }
.lens-none { font-size: var(--text-xs); color: var(--ink-faint); font-style: italic; }

/* One row per opposing type: the name, what the focused type deals to it, and
 * what it deals back.
 *
 * One column, not several. Wrapping into two columns saved vertical space but
 * put the header in the first grid cell and nothing above the second, so half
 * the numbers were unlabelled and the rows read left-right-left down the
 * page. A single ordered list is the point, the sort carries the meaning,
 * and a sort you have to zigzag through is not one. */
.matchup {
  display: grid; grid-template-columns: minmax(0, 1fr);
  gap: 2px; margin-top: var(--sp-2);
}
.matchup-head, .matchup-row {
  display: grid; grid-template-columns: 1fr calc(var(--text-2xs) * 4.4) calc(var(--text-2xs) * 4.4);
  align-items: center; gap: var(--sp-2);
}
.matchup-head {
  font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--ink-muted); padding-bottom: 3px; border-bottom: 1px solid var(--grid);
}
.matchup-head span:not(:first-child) { text-align: center; }
.matchup-row { padding: 2px 0; }
.matchup-row .typebadge { cursor: pointer; padding: 2px 7px 2px 5px; justify-self: start; }
.matchup-x {
  padding: 2px 0; border-radius: var(--radius-sm); text-align: center;
  font-family: var(--font-mono); font-size: var(--text-2xs); font-weight: 700;
}
/* A quiet edge on the name, so which way a row leans is readable before you
 * compare the two numbers. The numbers still carry it; this only sorts the
 * list visually into "worth picking" and "worth avoiding". */
/* The whole border carries the meaning. A stripe on one edge of a pill reads
 * as a rendering artefact at this size; a coloured outline reads as a state. */
.matchup-row.is-good .typebadge { border-color: var(--ink-green); }
.matchup-row.is-bad  .typebadge { border-color: var(--ink-red); }

/* Type symbols. Sized in the markup; this only fixes their baseline. */
.typeicon { flex: none; vertical-align: -2px; }
/* The official symbols are small full-colour discs. object-fit keeps them
 * square if a badge ever constrains one axis, and they must not be smoothed
 * into mush at badge size. */
.typeicon-img { object-fit: contain; }
.typebadge { gap: 5px; }

.heat-controls, .scatter-controls { display: flex; align-items: center; gap: var(--sp-4); margin-bottom: var(--sp-3); flex-wrap: wrap; }
.slider-label { font-size: var(--text-xs); color: var(--ink-muted); white-space: nowrap; }
.slider-label b { color: var(--ink); font-family: var(--font-mono); }
.slider { width: 160px; accent-color: var(--brand); }
.heat-note { font-size: var(--text-xs); color: var(--warning); }
.field { display: flex; align-items: center; gap: 6px; font-size: var(--text-xs); color: var(--ink-muted); }

/* Chart internals, styled here rather than inline so a token change reaches
 * every chart at once. */
.axis-label { fill: var(--ink-muted); font-size: var(--text-2xs); font-family: var(--font); }
.axis-title { fill: var(--ink-secondary); font-size: var(--text-xs); font-weight: 600; }
.gridline { stroke: var(--grid); stroke-width: 1; }
.baseline  { stroke: var(--axis); stroke-width: 1; }
.datalabel { fill: var(--ink-secondary); font-size: var(--text-2xs); font-family: var(--font-mono); }
.mark-hit  { fill: transparent; cursor: pointer; }

.heatcell { stroke: var(--surface); stroke-width: 2; }        /* 2px surface gap */
.heatcell-hit { cursor: crosshair; }
.heatlabel { font-size: var(--text-3xs); font-family: var(--font-mono); pointer-events: none; }

/* The pair matrix readout. Fixed minimum height so pinning and unpinning does
 * not shunt the whole dashboard up and down. */
.pairreadout {
  display: flex; align-items: center; gap: var(--sp-2) var(--sp-3); flex-wrap: wrap;
  min-height: 44px; margin-bottom: var(--sp-2); padding: var(--sp-2) var(--sp-3);
  background: var(--surface-raised); border: 1px solid var(--border);
  border-radius: var(--radius); font-size: var(--text-xs);
  /* Sticks to the top of the scrolling dashboard so it stays on screen while
   * you are pointing at a bubble eight hundred pixels further down. Opaque,
   * because the grid scrolls underneath it. */
  position: sticky; top: 0; z-index: 3;
}
.pairreadout.is-pinned { border-color: var(--brand); }
.pairreadout-hint { color: var(--ink-faint); font-style: italic; }
.pairreadout-types {
  display: inline-flex; align-items: center; gap: 5px;
  font-weight: 600; text-transform: capitalize; font-size: var(--text-sm);
}
.pairreadout-slash { color: var(--ink-faint); margin: 0 2px; }
.pairreadout-mono { color: var(--ink-muted); font-weight: 400; font-size: var(--text-xs); }
.pairreadout-stats { color: var(--ink-secondary); }
.pairreadout-stats b { font-family: var(--font-mono); color: var(--ink); }
.pairreadout-sep { color: var(--ink-faint); margin: 0 3px; }
.pairreadout-eg {
  color: var(--ink-muted); flex: 1 1 200px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pairreadout-pin {
  margin-left: auto; padding: 1px 7px; border-radius: 999px;
  background: var(--brand); color: #fff; font-size: var(--text-3xs);
  text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700;
}

.treecell { stroke: var(--surface); stroke-width: 2; cursor: pointer; }
.treecell.is-dim { opacity: 0.28; }
.treelabel { fill: #0b0b0b; font-size: var(--text-2xs); font-weight: 600; pointer-events: none; }
.treelabel-sub { fill: rgba(11, 11, 11, 0.72); font-size: var(--text-3xs); font-family: var(--font-mono); pointer-events: none; }

.dot { stroke: var(--surface); stroke-width: 1.5; }           /* 2px-ish surface ring */
.dot.is-dim { opacity: 0.12; }
.dot.is-selected { stroke: var(--ink); stroke-width: 2; }
.brush { fill: rgba(57, 135, 229, 0.14); stroke: var(--series-1); stroke-width: 1; }

.tooltip {
  position: fixed; z-index: 100; pointer-events: none; opacity: 0;
  background: var(--surface-raised); border: 1px solid var(--border-strong);
  border-radius: var(--radius); padding: var(--sp-2) var(--sp-3);
  font-size: var(--text-xs); box-shadow: var(--shadow-lg);
  max-width: 260px; transition: opacity 0.12s;
}
.tooltip.is-on { opacity: 1; }
.tooltip-title { font-weight: 600; margin-bottom: 3px; }
.tooltip-row { display: flex; justify-content: space-between; gap: var(--sp-3); color: var(--ink-secondary); }
.tooltip-row b { font-family: var(--font-mono); color: var(--ink); font-weight: 500; }

/* The table view. Doubles as the accessibility fallback that lets a sub-3:1
 * fill or a colour-only distinction be read as numbers instead. */
.tablewrap { overflow: auto; max-height: 460px; border: 1px solid var(--border); border-radius: var(--radius); }
table { border-collapse: collapse; width: 100%; font-size: var(--text-xs); }
thead th {
  position: sticky; top: 0; z-index: 1; text-align: left;
  background: var(--surface-raised); padding: 7px var(--sp-3);
  border-bottom: 1px solid var(--border); cursor: pointer; white-space: nowrap;
  font-weight: 600; color: var(--ink-secondary);
}
thead th:hover { color: var(--ink); }
thead th.is-sorted { color: var(--ink); }
tbody td { padding: 5px var(--sp-3); border-bottom: 1px solid var(--grid); white-space: nowrap; }
tbody tr:hover { background: var(--surface-hover); }
tbody tr { cursor: pointer; }
td.num { font-family: var(--font-mono); text-align: right; }

.noscript { padding: var(--sp-6); text-align: center; color: var(--ink-secondary); }

/* Respect the OS setting. The 3D device still renders; it just stops spinning
 * on its own and snaps instead of easing. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* Narrow screens: the three-column dex collapses to the stage plus a scrolling
 * detail pane, and the rail becomes a horizontal strip. */
@media (max-width: 1024px) {
  /* The stage takes the larger share, not the detail pane.
   *
   * At 1fr/1.1fr the stage got 206px, which left a 96px sprite rendering at
   * 73. Downscaled below its own resolution, in a box 560px wide that a
   * square image cannot use. The detail pane scrolls and the stage cannot, so
   * the pane is the one that can afford to give. A floor as well as a ratio,
   * because on a very short window a ratio alone still collapses it. */
  .view-dex { grid-template-columns: 1fr;
              grid-template-rows: 88px minmax(216px, 1.15fr) minmax(0, 1fr); }
  .rail { border-right: 0; border-bottom: 1px solid var(--border); }
  .detail { border-left: 0; border-top: 1px solid var(--border); }
  .topbar { padding: var(--sp-2) var(--sp-3); }
  .search { order: 3; max-width: none; flex-basis: 100%; }
}

@media (max-width: 720px) {
  /* Header in two rows: the tabs hard left with the icon buttons opposite
     them, then search across the full width underneath.

     Search had been sharing a row with the icons, which left it about 280px
     and cut the placeholder off mid-word. It is the widest control here and
     the one that benefits most from width, so it gets the row. */
  .topbar { gap: var(--sp-2); padding: var(--sp-2) var(--sp-3) var(--sp-3); }
  .brand-text { font-size: var(--text-base); }
  /* Keeping the tabs and the buttons on one row, at every width a phone
     actually is.
     
     Measured on this markup: the three labels are 158px of text at 15px, the
     buttons are 102px, and the group's own padding and gaps are 8px. At 360
     the desktop paddings wanted 345px of a 336px line, so the buttons wrapped.
     A fixed smaller padding fixes 360 and breaks again at 320.

     So the padding scales with the viewport instead of stepping at a
     breakpoint. It is the one dimension here that can shrink without costing
     anything: the tap target keeps its 33px height throughout. */
  .viewswitch { order: 0; margin-left: 0; padding: 2px; min-width: 0; }
  .viewswitch-btn { padding: 5px clamp(4px, 1.6vw, 12px); }
  .spritepicker { order: 1; flex: none; margin-left: auto; gap: 6px; }
  /* A zero-height full-width flex item, so the wrap happens HERE rather than
     wherever the tabs and buttons happen to run out of room. */
  .topbar::after { content: ""; order: 2; flex-basis: 100%; height: 0; }
  .search { order: 3; flex: 1 1 100%; min-width: 0; max-width: none; }
  .search input { min-width: 0; }
  .search-kbd { display: none; }        /* no physical keyboard to press it */

  /* The caught count leaves the tab only when it has to. It is 19px including
     its gap; measured, 360px still has 30px of slack with it and 320px is 6px
     short. So 340, not the 400 this was first set to, which was hiding it on a
     375px iPhone that had 41px to spare. The number is the first thing on the
     page the tab opens, so it should go last. */
  @media (max-width: 340px) {
    .viewswitch-n { display: none; }
  }
  /* Under 360px the labels come down a step as well. Below this the text alone
     is wider than the room left once the buttons take their 102px. */
  @media (max-width: 360px) {
    .viewswitch-btn { font-size: var(--text-2xs); }
  }

  /* The nameplate, compacted for the 28px band set above. */
  .brand { height: 22px; padding: 0 10px; gap: 6px; }
  .brand-ball { width: 14px; height: 14px; border-width: 1.5px; }
  .brand-ball::after { width: 4px; height: 4px; border-width: 1.5px; }
  .brand-text { font-size: var(--text-xs); }

  /* The base rule anchors both popovers to the right edge of their button,
     which is right again now that the buttons sit at the right of the top row.
     This briefly anchored them left, from when the bar wrapped and put the
     buttons on the left instead. Only the width cap is still needed, for
     screens narrower than the panel. */
  .spritemenu-panel, .settings-menu {
    width: min(264px, calc(100vw - 2 * var(--sp-3)));
  }

  /* The list runs horizontally as a filmstrip: a vertical list would eat half
   * the screen to show six entries. app.js virtualises along the same axis. */
  .view-dex { grid-template-rows: 126px minmax(0, 1fr) minmax(0, 1.15fr); }
  .rail { flex-direction: column; }
  .rail-head { padding: 4px var(--sp-3) 2px; gap: 4px; }
  .rail-count { font-size: var(--text-sm); }
  .railjump { width: 22px; height: 22px; font-size: var(--text-xs); }
  .rail-scroll {
    overflow-x: auto; overflow-y: hidden;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
    /* Settle on whole tiles so a flick never leaves a sliver of the next one
     * on screen. Proximity rather than mandatory, so a long fast swipe still
     * carries its momentum instead of being caught by the nearest tile. */
    scroll-snap-type: x proximity;
  }
  .rail-row { scroll-snap-align: start; }
  .rail-scroll::-webkit-scrollbar { display: none; }
  .rail-items { display: flex; height: 100%; }
  .rail-row {
    flex-direction: column; justify-content: center; gap: 1px;
    width: 74px; min-width: 74px; max-width: 74px; height: 100%;
    padding: 4px 3px; overflow: hidden;
    border-left: 0; border-bottom: 3px solid transparent; text-align: center;
  }
  .rail-row.is-selected { border-left: 0; border-bottom-color: var(--brand); }
  .rail-sprite { width: 38px; height: 38px; }
  /* Every one of these is needed to make the ellipsis actually happen: the
   * flex child must be allowed to shrink (min-width), the box must be told its
   * width (100% of a fixed-width tile), and the text must be a block for
   * text-overflow to apply. Without them "Charmeleon" simply ran over its
   * neighbour instead of clipping. */
  .rail-meta { flex: none; width: 100%; min-width: 0; overflow: hidden; }
  .rail-name {
    display: block; width: 100%; font-size: var(--text-2xs); line-height: 1.25;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .rail-no { display: block; font-size: var(--text-3xs); }
  .rail-types { justify-content: center; }

  .flatcard { padding: var(--sp-2); gap: 2px; }
  /* The panel below already names it â€” see paintFlatCard in app.js. */
  .flatcard-caption { display: none; }

  .detail { padding: var(--sp-3); }
  .detail-name { font-size: var(--text-lg); }
  /* Tabs scroll sideways rather than wrapping to three rows of chips. */
  .tabs {
    flex-wrap: nowrap; overflow-x: auto; margin-top: var(--sp-3);
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tab { padding: 6px 11px; }           /* a comfortable touch target */
  /* The centre ball overhangs the screen's bottom edge by about 25px, so the
   * last rows of any scrolling pane need room to clear it. */
  .tabbody, .dashboard, .rail-scroll { padding-bottom: 34px; }

  /* The learnset's six fixed columns add to 344px, which on a 351px phone
   * pane leaves Move 7px, every move name ellipsised to nothing. Removing
   * the scrollbar only helps if what remains is readable, so on a phone the
   * fixed columns give back what they can spare and Move takes it: 24px of
   * level, a tighter dot pitch and 4px gutters buy it 91px, enough for most
   * names outright and a recognisable stem for the rest. */
  .movegroup .minitable thead th,
  .movegroup .minitable tbody td { padding-left: 4px; padding-right: 4px; }
  .movegroup col.c-lv   { width: 24px; }
  .movegroup col.c-type { width: 64px; }
  .movegroup col.c-cat  { width: 34px; }
  .movegroup col.c-pow  { width: 34px; }
  .movegroup col.c-acc  { width: 38px; }
  .movegroup col.c-gens { width: 66px; }
  .movegroup .gendots { gap: 1.5px; }
  .movegroup .gendot { width: 5px; height: 5px; }

  /* Fewer, bigger cells beat six cramped ones on a phone. */
  .matchgrid { grid-template-columns: repeat(auto-fit, minmax(calc(var(--text-md) * 4.4), 1fr)); }
  .formstats { grid-template-columns: repeat(3, 1fr); }

  /* The scatter's three selects stacked vertically were ~130px of chrome
   * above a 380px chart. Side by side they are one row. */
  .scatter-controls { gap: var(--sp-2); }
  .scatter-controls .field { flex: 1 1 30%; min-width: 0; flex-direction: column; align-items: stretch; gap: 2px; }
  .scatter-controls .field select { width: 100%; font-size: var(--text-xs); padding: 5px 6px; }
  .scatter-controls .field > span { font-size: var(--text-2xs); }

  /* The full matrix is unreadable at this width â€” the lens is the mobile
   * view, and the toggle stays so it is a choice rather than a restriction. */
  .chart-heat svg { min-width: 520px; }
  .chart-heat { overflow-x: auto; }

  /* The pair matrix scrolls rather than shrinking. Eighteen columns across
   * 375px gives 15px cells, which is under half a comfortable touch target;
   * at 500px they are 26px and a thumb can actually land on one. */
  #chart-pairs { overflow-x: auto; }
  #chart-pairs svg { min-width: 500px; }

  /* Analytics: one panel per row, and the filters fold away.
   *
   * Eighteen type chips plus nine generations plus four classes is about 250px
   * of a 812px screen. Left open permanently that is a third of the phone
   * spent on controls you touch once. */
  .filterbar { gap: var(--sp-2) var(--sp-3); padding: var(--sp-2) var(--sp-3); }
  .filterbar-group { flex-wrap: wrap; min-width: 0; flex-basis: 100%; }
  .filterbar-actions { margin-left: auto; }
  .filterbar-toggle {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 6px 12px; border-radius: 999px; cursor: pointer;
    background: var(--surface-sunken); border: 1px solid var(--border);
    color: var(--ink-secondary); font-size: var(--text-xs); font-weight: 600;
  }
  .filterbar-toggle-caret { transition: transform 0.18s; display: inline-block; }
  .filterbar-toggle[aria-expanded="true"] .filterbar-toggle-caret { transform: rotate(90deg); }
  .filterbar-toggle-n {
    min-width: 16px; padding: 0 4px; border-radius: 8px;
    background: var(--brand); color: #fff; font-family: var(--font-mono);
    font-size: var(--text-3xs); text-align: center;
  }
  /* Collapsed is the default; the toggle adds .is-open. */
  .filterbar:not(.is-open) .filterbar-group { display: none; }
  .dashboard { gap: var(--sp-2); padding: var(--sp-2); }
  .panel { padding: var(--sp-3); border-radius: var(--radius); }
  .panel-head h2 { font-size: var(--text-base); }
  .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tile-value { font-size: var(--text-lg); }
  .tablewrap { max-height: 320px; }

  /* Boot screen: the cards stack and the device needs the middle. */
      }

/* Very small phones: reclaim every remaining pixel of chrome. */
@media (max-width: 400px) {
  .spritepicker-label { display: none; }
  .matchgrid { grid-template-columns: repeat(3, 1fr); }
  .tiles { grid-template-columns: minmax(0, 1fr); }
}

/* The boot-screen collapse used to live here. Three.js removal left the block
 * half-deleted: its opening comment was never closed, so the @media rule below
 * it was swallowed, and the stray brace that followed took the next rule down
 * with it. That rule was `.view-collection { overflow-y: auto }`, so the
 * Collection page silently stopped scrolling. */

/* ══════════════════════════════════════════════════════ the caught dex ══ */

.view-collection { overflow-y: auto; }
.collectionwrap {
  max-width: 1100px; margin: 0 auto; padding: var(--sp-5) var(--sp-4) var(--sp-6);
  display: flex; flex-direction: column; gap: var(--sp-4);
}
/* The wrapper's gap only separates its own children, and the header, tiles
 * and panels are all children of this one inner block, so without a gap of
 * its own the stat tiles sat flush against the panel underneath them. */
.collectionbody {
  display: flex; flex-direction: column; gap: var(--sp-4);
}

/* Roomier than a dashboard panel. These are lists you read down rather than
 * charts you glance at, and the rows need air between them more than the
 * panel needs to be compact.
 *
 * grid-column is reset because .panel carries "span 6" for the dashboard's
 * twelve-track grid. Borrowing the class borrowed that too, and six of two
 * tracks is the whole row. */
.collectionpanels .panel {
  grid-column: auto;
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
}
.collectionpanels .panel-head { margin-bottom: var(--sp-3); }
.collection-empty {
  margin: 0; padding: var(--sp-5); text-align: center;
  background: var(--surface); border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg); color: var(--ink-secondary);
  font-size: var(--text-sm); line-height: 1.6;
}
.collection-empty b { color: var(--ink); }

.collectionpanels {
  display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
  align-items: start;
}
/* The headline number and its caption were touching. */
.collectionbody .tile { padding: var(--sp-3) var(--sp-4) var(--sp-4); }
.collectionbody .tile-sub { margin-top: 3px; }

/* One row per bucket: name, bar, count, percent. The bar is the comparison
 * and the number is the fact, so both are present, a bar alone cannot tell
 * you that you are three away rather than thirty. */
/* The bar is capped rather than elastic. A progress bar's job is comparison
 * between rows, and it does that at 300px exactly as well as at 800. Past
 * which the only thing extra width adds is distance between a label and the
 * number that belongs to it. */
.progrow {
  display: grid; align-items: center; gap: var(--sp-2);
  grid-template-columns: 8.5em minmax(0, 22em) 4.5em 2.6em;
  padding: 5px 0; font-size: var(--text-xs);
}
.progrow-label { color: var(--ink-secondary); text-transform: capitalize; }
.progrow-type { display: inline-flex; align-items: center; gap: 5px; text-transform: capitalize; }
.progrow-bar {
  position: relative; height: 9px; border-radius: 5px;
  background: var(--surface-sunken); overflow: hidden;
}
.progrow-fill {
  position: absolute; inset: 0 auto 0 0; border-radius: 5px;
  background: var(--ink-green);
}
/* Shiny sits on top of caught rather than beside it: every shiny is also a
 * catch, so two bars side by side would double-count the same Pokémon. */
.progrow-shiny {
  position: absolute; inset: 0 auto 0 0; border-radius: 5px;
  background: var(--warning);
}
.progrow-count { font-family: var(--font-mono); color: var(--ink-muted); text-align: right; }
.progrow-count b { color: var(--ink); }
.progrow-pct { font-family: var(--font-mono); color: var(--ink-muted); text-align: right; }
.progrow.is-done .progrow-label,
.progrow.is-done .progrow-pct { color: var(--ink-green); }
.progrow-left { grid-column: 2 / -1; font-size: var(--text-3xs); color: var(--ink-muted); }
.progrow-left.is-done { color: var(--ink-green); }

/* The neutral types, folded away. Styled like a row rather than like a
   disclosure widget, so it reads as the last line of the list. */
.threatfold { margin-top: var(--sp-2); }
.threatfold > summary {
  cursor: pointer; padding: var(--sp-2) 0; font-size: var(--text-2xs);
  color: var(--ink-muted); list-style-position: inside;
}
.threatfold > summary:hover { color: var(--ink-secondary); }
.threatfold > summary b { color: var(--ink-secondary); }
.threatfold[open] > summary { border-bottom: 1px solid var(--grid); margin-bottom: var(--sp-2); }

.nextrow { padding: var(--sp-2) 0; border-bottom: 1px solid var(--grid); }
.nextrow:last-child { border-bottom: 0; }
.nextrow-n { float: right; font-size: var(--text-2xs); color: var(--ink-muted); font-family: var(--font-mono); }
.nextrow-list { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 5px; }
.nextrow-list .typebadge { cursor: pointer; padding: 2px 8px; text-transform: none; }
.nextrow-more { font-size: var(--text-2xs); color: var(--ink-muted); align-self: center; margin-left: 2px; }

/* Backup. The code field is monospace and selectable on click, because the
 * single most likely thing to go wrong is a partial copy. */
.transferrow { display: flex; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.transfer-code {
  flex: 1; min-width: 0; padding: 7px 10px;
  background: var(--surface-sunken); border: 1px solid var(--border);
  border-radius: var(--radius); color: var(--ink-secondary);
  font-family: var(--font-mono); font-size: var(--text-2xs);
}
.transfer-code:focus { border-color: var(--series-1); outline: none; }
#transfer-status[data-tone="good"] { color: var(--ink-green); }
#transfer-status[data-tone="bad"] { color: var(--ink-red); }
.transfer-clear { margin-top: var(--sp-2); color: var(--ink-red); }
.transfer-clear:hover { border-color: var(--ink-red); }

@media (max-width: 760px) {
  .collectionpanels { grid-template-columns: minmax(0, 1fr); }
  .progrow { grid-template-columns: 7em minmax(0, 1fr) 4.2em 2.6em; }
}

/* The bench: your caught Pokémon, one click from a team slot.
 *
 * Sprites rather than names alone. A wall of 200 text chips is a search
 * problem; a wall of 200 sprites is a Pokémon you recognise instantly, which
 * is the entire reason the games put pictures in the box. */
.bench { margin-top: var(--sp-4); }
.bench-title {
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--ink-muted); font-weight: 600; margin-bottom: var(--sp-2);
}
.bench-full {
  margin-left: 6px; color: var(--warning); font-weight: 600;
  text-transform: none; letter-spacing: 0;
}
/* Bounded and scrollable: a full dex is 1025 chips, and a bench that pushes
 * the coverage analysis off the bottom of the page defeats its own purpose. */
.bench-list {
  display: flex; flex-wrap: wrap; gap: 4px;
  max-height: 232px; overflow-y: auto;
  padding: var(--sp-2); border: 1px solid var(--border);
  border-radius: var(--radius); background: var(--surface-sunken);
}
.benchchip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 9px 2px 3px; border-radius: 999px; cursor: pointer;
  background: var(--surface-raised); border: 1px solid var(--border);
  color: var(--ink-secondary); font-size: var(--text-2xs); font-weight: 600;
}
.benchchip:hover:not(:disabled) { border-color: var(--border-strong); color: var(--ink); }
.benchchip.is-on {
  background: #1f7a34; border-color: #2a9444; color: #fff;
}
.benchchip:disabled { opacity: 0.4; cursor: not-allowed; }
.benchchip-img {
  width: 22px; height: 22px; object-fit: contain; image-rendering: pixelated;
  flex: none;
}

/* ═══════════════════════════════════════════════ dashboard contents rail ══ */

.dashwrap {
  flex: 1; min-height: 0; display: grid;
  grid-template-columns: 168px minmax(0, 1fr);
}
.dashnav {
  overflow-y: auto; padding: var(--sp-3) var(--sp-2) var(--sp-3) var(--sp-3);
  display: flex; flex-direction: column; gap: 1px;
  border-right: 1px solid var(--border);
}
/* Sections sit under their page, indented, and only while it is open. */
.dashnav-item {
  text-align: left; padding: 5px 10px 5px 22px; border: 0;
  border-radius: var(--radius-sm);
  background: transparent; color: var(--ink-muted); cursor: pointer;
  font-size: var(--text-2xs); font-weight: 500; line-height: 1.3;
}
.dashnav-item:hover { background: var(--surface-hover); color: var(--ink); }
.dashnav-item.is-on { background: var(--brand-wash); color: var(--ink); }
/* The rail is a convenience, not the only way through the page. Below the
 * two-pane threshold it would cost more width than it returns. */
@media (max-width: 900px) {
  .dashwrap { grid-template-columns: minmax(0, 1fr); }
  .dashnav { display: none; }
}

/* ═════════════════════════════════════════════════ Generation I trainers ══ */

/* Three levels: a group label, the pages in it, the sections of the open one.
 * Indentation carries the hierarchy so the rail needs no disclosure arrows.
 * Everything relevant is already visible. */
.dashnav-group {
  padding: 8px 10px 4px; margin-top: var(--sp-3);
  color: var(--ink-faint); font-size: var(--text-3xs); font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.09em;
}
.dashnav-group:first-child { margin-top: 0; }
/* Selection fills the control rather than tagging its left edge. A 2px bar
 * beside a transparent button reads as a decoration on the rail; a filled
 * button reads as the thing you are on. */
.dashnav-page {
  text-align: left; padding: 6px 10px; border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--ink-secondary); cursor: pointer;
  font-size: var(--text-xs); font-weight: 600;
}
.dashnav-page:hover { background: var(--surface-hover); color: var(--ink); }
.dashnav-page.is-active { background: var(--brand); color: #fff; }

/* Two panes' worth of content in one column was 5,961px of scroll, with rows
 * using a third of their width and a range bar stretched to 961px. Panels sit
 * side by side instead, which shortens the scroll and, because each panel is
 * now about half as wide. Pulls every number back next to the label it
 * belongs to. */
/* align-content: start, and it matters only when the content is short.
 *
 * A grid's align-content defaults to stretch, so auto-sized rows grow to fill
 * the container. Every Generation I page overflows this pane, so the leftover
 * space was always zero and nothing showed. The Traffic page's empty state is
 * two short blocks, and the grid put 350px of nothing between the heading and
 * the sentence under it. Packing the rows at the top leaves the slack at the
 * bottom, where it belongs in a scrolling pane. */
.genpane {
  overflow-y: auto; padding: var(--sp-3);
  display: grid; align-items: start; align-content: start;
  /* min(420px, 100%), not 420px. A bare minimum larger than the container is
     still honoured, so on a 375px phone the track came out 420 wide and the
     pane scrolled sideways inside itself. The min() lets the last column
     collapse to whatever room there is. */
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
  gap: var(--sp-3);
}
/* The intro and the battle list want the full width: one is a paragraph, the
 * other has rows of party chips that wrap badly in half a column. */
.gen1-head, #gen1-trainers { grid-column: 1 / -1; }

/* The traffic page brings its own columns, so nothing in it should be laid out
   by this grid. Without this its heading took one column and the paragraph
   beside it took the other, which read as two unrelated blocks rather than a
   title and its description. */
#traffic-pane > * { grid-column: 1 / -1; }
/* Same for the map page. Left to the pane's auto-fit columns the map got a
   320px track, which is 20px a square. It lays itself out. */
#gen1-pane > .kanto,
#gen1-pane > .gen1-head,
#gen1-pane > .panel-foot { grid-column: 1 / -1; }
.genpane .panel { min-width: 0; }

/* Side-by-side panels share their row tracks.
 *
 * Two panels in a band have descriptions of different lengths, so below about
 * 1110px one of them wraps to a third line and its column header drops 17px
 * below its neighbour's. Nothing lines up across the band and it reads as
 * sloppy even though every panel is internally correct.
 *
 * Subgrid makes the two panels share the parent's rows rather than each
 * computing its own: heading block, column header, body. The taller heading
 * sets the track and both column headers sit on it, at any width and any text
 * size. Without subgrid the rule simply does not apply and the panels behave
 * as they do now.
 */
@supports (grid-template-rows: subgrid) {
  .genpane .panel.is-aligned {
    display: grid;
    grid-row: span 3;
    grid-template-rows: subgrid;
  }
  /* The body must not stretch to fill the shared track, or a short list would
   * be padded out to the height of a long one. */
  .genpane .panel.is-aligned > .panelrows { align-self: start; }
}
.gen1-loading { color: var(--ink-muted); font-size: var(--text-sm); padding: var(--sp-5); }
.gen1-head { padding: 0 var(--sp-2); }
.genpane .panel { grid-column: auto; }

/* Most-used: name, bar, count, level range. */
.usedrow {
  display: grid; max-width: 44em;
  grid-template-columns: 8.5em minmax(0, 1fr) 3em 5em;
  align-items: center; gap: var(--sp-2); padding: 3px 0; font-size: var(--text-xs);
}
.usedrow-name {
  text-align: left; border: 0; background: transparent; cursor: pointer;
  color: var(--ink-secondary); font-size: var(--text-xs); padding: 0;
}
.usedrow-name:hover { color: var(--ink); text-decoration: underline; }
.usedrow-bar { height: 9px; border-radius: 5px; background: var(--surface-sunken); overflow: hidden; }
.usedrow-bar span { display: block; height: 100%; background: var(--series-1); border-radius: 5px; }
.usedrow-n { font-family: var(--font-mono); text-align: right; color: var(--ink-muted); }
.usedrow-n b { color: var(--ink); }
.usedrow-lv { font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--ink-muted); text-align: right; white-space: nowrap; }

/* Level curve. Gyms are the steps, so they get the accent. */
.curverow {
  display: grid; max-width: 44em;
  grid-template-columns: 11em minmax(0, 1fr) 4.4em 3.8em;
  align-items: center; gap: var(--sp-2); padding: 2px 0; font-size: var(--text-2xs);
}
.curverow-place { color: var(--ink-muted); }
/* A track with a floating segment on it, so the bar reads as "from here to
 * here" rather than "this much". position:relative on the track is what makes
 * the segment's left offset mean a level. */
.curverow-bar {
  position: relative; height: 8px; border-radius: 4px;
  background: var(--surface-sunken);
}
.curverow-bar span {
  position: absolute; top: 0; bottom: 0;
  background: var(--ink-blue); border-radius: 4px;
}
.curverow.is-gym .curverow-place { color: var(--ink); font-weight: 700; }
.curverow.is-gym .curverow-bar span { background: var(--brand); }

/* Header rows. The bar column is self-explanatory; the numbers were not, and
 * "12  2" beside a chart is a riddle rather than a reading. */
.curverow-head, .usedrow-head {
  font-size: var(--text-3xs); color: var(--ink-muted);
  padding-bottom: 4px; margin-bottom: 3px;
  border-bottom: 1px solid var(--grid);
}
/* Column 1 is the row label and column 2 is the bar; the two number columns
 * right-align over the figures they name. */
.curverow-head span:nth-child(n+3),
.usedrow-head span:nth-child(n+3) { text-align: right; }
.curverow-lv { font-family: var(--font-mono); text-align: right; color: var(--ink); white-space: nowrap; }
.curverow-n { font-family: var(--font-mono); text-align: right; color: var(--ink-faint); }

/* Trainers by place: collapsed by default because 75 places open at once is
 * a wall, and the point of the list is to find one place. */
.placeblock { border-bottom: 1px solid var(--grid); }
.placeblock:last-child { border-bottom: 0; }
.placeblock-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--sp-3); padding: 7px 4px; cursor: pointer; list-style: none;
}
.placeblock-head::-webkit-details-marker { display: none; }
.placeblock-head::before {
  content: "▸"; color: var(--ink-muted); font-size: var(--text-2xs);
  transition: transform 0.15s; display: inline-block;
}
.placeblock[open] .placeblock-head::before { transform: rotate(90deg); }
.placeblock-name { flex: 1; font-size: var(--text-xs); font-weight: 600; color: var(--ink-secondary); }
.placeblock[open] .placeblock-name { color: var(--ink); }
.placeblock-meta { font-size: var(--text-3xs); color: var(--ink-muted); font-family: var(--font-mono); }

.trainerrow {
  display: grid; grid-template-columns: 8.5em minmax(0, 1fr);
  gap: var(--sp-2); padding: 4px 4px 4px var(--sp-3); align-items: baseline;
}
.trainerrow-class { font-size: var(--text-2xs); color: var(--ink-muted); text-transform: capitalize; }
.trainerrow.is-leader .trainerrow-class { color: var(--ink-red); font-weight: 700; }
.trainerrow.is-elite .trainerrow-class { color: var(--warning); font-weight: 700; }
.trainerrow-party { display: flex; flex-wrap: wrap; gap: 3px; }
.partymon {
  padding: 2px 8px; border-radius: 999px; cursor: pointer;
  background: var(--surface-raised); border: 1px solid var(--border);
  color: var(--ink-secondary); font-size: var(--text-2xs);
}
.partymon:hover { border-color: var(--border-strong); color: var(--ink); }
.partymon b { font-family: var(--font-mono); color: var(--ink-muted); font-weight: 700; }

@media (max-width: 760px) {
  .usedrow { grid-template-columns: 7em minmax(0, 1fr) 2.6em 4.2em; }
  .curverow { grid-template-columns: 7.5em minmax(0, 1fr) 4.2em 3.4em; }
  .trainerrow { grid-template-columns: minmax(0, 1fr); }
}

/* ══════════════════════════ the creature first, when there is less room ══
 *
 * On a short window the sprite was losing every argument it was in. Measured
 * at 1129x852: a 306px stage, of which the picture got 99px. Under a third,
 * while a 28px name, a sprite-set caption and 96px reserved for the two
 * action bars took the rest.
 *
 * That is backwards. The name is already on the detail panel beside it, and
 * the sprite set is already in the topbar picker; the one thing this panel
 * has that nothing else does is the Pokémon itself. So as height runs out the
 * text gives ground and the picture keeps it.
 *
 * Height rather than width, because the stage is a row of a vertical grid,
 * a narrow window is not the problem, a short one is.
 */
@media (max-height: 900px) {
  .flatcard { padding: var(--sp-2) var(--sp-3); gap: 4px; }
  .flatcard-name { font-size: calc(21px * var(--ui-scale)); line-height: 1.15; }
  .flatcard-no { font-size: calc(10px * var(--ui-scale)); }
  /* The sprite set is named in the topbar picker two inches away. */
  .flatcard-set { display: none; }
  .stage-bottom { gap: 4px; padding: 4px var(--sp-3) var(--sp-2); }
  .stage-bottom ~ .flatcard, .stage.is-flat .flatcard { padding-bottom: 52px; }
}

@media (max-height: 740px) {
  .flatcard-name { font-size: calc(17px * var(--ui-scale)); }
  .flatcard-no { display: none; }        /* the rail and the detail both show it */
  .stage-bottom ~ .flatcard, .stage.is-flat .flatcard { padding-bottom: 46px; }
}

/* The scope note on the trainer pane: which games this actually is. Quieter
 * than the sentence above it, because it answers a question rather than
 * making a claim. */
.gen1-scope { display: block; margin-top: 6px; color: var(--ink-faint); }

/* Red & Blue against Yellow: two teams on one row, with the level shift.
 * Full width, because a party is a sentence of six names and squeezing two of
 * them into half a column would wrap both into mush. */
#gen1-changed { grid-column: 1 / -1; }
.cmprow {
  display: grid; grid-template-columns: 7.5em 1fr 1fr 4em;
  gap: var(--sp-3); align-items: baseline;
  padding: 5px 0; font-size: var(--text-2xs);
  border-bottom: 1px solid var(--grid);
}
.cmprow:last-of-type { border-bottom: 0; }
.cmprow-head { border-bottom: 1px solid var(--border); font-size: var(--text-3xs); }
.cmprow-head span:last-child { text-align: right; }
.cmprow-who { font-weight: 700; color: var(--ink-secondary); text-transform: capitalize; }
.cmprow-team { color: var(--ink-muted); line-height: 1.45; }
.cmprow-team em { color: var(--ink-faint); font-style: italic; }
.cmprow-delta { font-family: var(--font-mono); text-align: right; color: var(--ink-muted); }
/* The direction of the change is the point, so it is the thing with colour. */
.cmprow.is-harder .cmprow-delta { color: var(--ink-red); font-weight: 700; }
.cmprow.is-easier .cmprow-delta { color: var(--ink-green); font-weight: 700; }
.cmprow.is-harder .cmprow-who { color: var(--ink); }
.cmprow.is-easier .cmprow-who { color: var(--ink); }
@media (max-width: 860px) {
  .cmprow { grid-template-columns: 1fr 1fr; gap: 4px var(--sp-2); }
  .cmprow-who { grid-column: 1 / 3; }
  .cmprow-delta { grid-column: 2; text-align: right; }
}
/* Rebuilt but no harder: a change of character, not of difficulty. */
.cmprow.is-swapped .cmprow-delta { color: var(--ink-blue); font-weight: 700; }
.cmprow.is-swapped .cmprow-who { color: var(--ink); }

/* Only in one game. Full width: four blocks of chips read as a list, not a
 * column. */
#gen1-only { grid-column: 1 / -1; }
.onlyblock { padding: var(--sp-3) 0; border-bottom: 1px solid var(--grid); }
.onlyblock:last-child { border-bottom: 0; padding-bottom: 0; }
.onlyblock-title { font-size: var(--text-xs); font-weight: 700; color: var(--ink-secondary); }
.onlyblock-sub { margin: 2px 0 var(--sp-2); font-size: var(--text-2xs); color: var(--ink-muted); line-height: 1.5; }
.onlychips { display: flex; flex-wrap: wrap; gap: 4px; }
.onlychips .typebadge { text-transform: none; cursor: default; }
.onlychips button.typebadge { cursor: pointer; }
/* The two games get the two accents used everywhere else for them. */
.typebadge.is-rb { border-color: var(--ink-red); color: var(--ink); }
.typebadge.is-yellow { border-color: var(--warning); color: var(--ink); }
.onlylist { display: flex; flex-direction: column; gap: 3px; }
.onlylist-row {
  display: grid; grid-template-columns: 10em 12em 1fr;
  gap: var(--sp-2); font-size: var(--text-2xs); align-items: baseline;
}
.onlylist-who { font-weight: 700; color: var(--warning); }
.onlylist-where { color: var(--ink-secondary); }
.onlylist-team { color: var(--ink-muted); }
@media (max-width: 760px) {
  .onlylist-row { grid-template-columns: 1fr; gap: 1px; }
}

/* Deployment against standing: how often a species is used, and how much of
 * that use is a fight that matters. Two numbers per row because collapsing
 * them into one loses the finding. */
#gen1-favour { grid-column: 1 / -1; }
.favourlist { display: flex; flex-direction: column; gap: 1px; }
.favourrow {
  display: grid; grid-template-columns: 9em 3em minmax(0, 20em) 3em 6em;
  gap: var(--sp-2); align-items: center;
  padding: 3px 0; font-size: var(--text-2xs);
}
.favourrow-head {
  font-size: var(--text-3xs); color: var(--ink-muted);
  padding-bottom: 4px; margin-bottom: 4px; border-bottom: 1px solid var(--grid);
}
.favourrow-head span:nth-child(2) { text-align: right; }
.favourrow-name {
  text-align: left; border: 0; background: transparent; cursor: pointer; padding: 0;
  color: var(--ink-secondary); font-size: var(--text-2xs); font-weight: 600;
}
.favourrow-name:hover { color: var(--ink); text-decoration: underline; }
.favourrow-n { font-family: var(--font-mono); text-align: right; color: var(--ink); }
.favourrow-bar { height: 8px; border-radius: 4px; background: var(--surface-sunken); overflow: hidden; }
.favourrow-bar span { display: block; height: 100%; border-radius: 4px; }
.favourrow.is-boss .favourrow-bar span { background: var(--brand); }
.favourrow.is-filler .favourrow-bar span { background: var(--ink-muted); }
.favourrow-share { font-family: var(--font-mono); text-align: right; color: var(--ink-muted); }
.favourrow.is-boss .favourrow-share { color: var(--ink-red); font-weight: 700; }
.favourrow-cls { font-size: var(--text-3xs); color: var(--ink-faint); }
@media (max-width: 760px) {
  .favourrow { grid-template-columns: 7em 2.6em minmax(0, 1fr) 2.8em; }
  .favourrow-cls { display: none; }
}

/* Column definitions. Three numbers with no labels is a puzzle; the panel
 * should say what it counts before it counts it. */
.deflist { margin: var(--sp-2) 0 0; font-size: var(--text-2xs); line-height: 1.5; }
.deflist dt {
  float: left; clear: left; width: 8.5em; font-weight: 700;
  color: var(--ink-secondary);
}
.deflist dd { margin: 0 0 4px 9em; color: var(--ink-muted); }

/* Every row opens to show the parties behind its numbers. */
.favouritem { border-bottom: 1px solid var(--grid); }
.favouritem:last-child { border-bottom: 0; }
.favouritem > summary { cursor: pointer; list-style: none; }
.favouritem > summary::-webkit-details-marker { display: none; }
.favouritem > summary:hover { background: var(--surface-hover); }
.favouritem[open] > summary { background: var(--surface-hover); }
.favouritem > summary::before {
  content: "▸"; color: var(--ink-faint); font-size: var(--text-3xs);
  position: absolute; margin-left: -11px; transition: transform 0.15s;
}
.favouritem[open] > summary::before { transform: rotate(90deg); }
.favourrow { position: relative; }

.favourwhere {
  padding: var(--sp-2) 0 var(--sp-3) var(--sp-4);
  display: flex; flex-direction: column; gap: 1px;
}
/* The level column holds a range once a fight is stored with several teams
 * ("L22-23"), which is wider than the single level the 3.5em was cut for. */
.favourwhere-head, .favourwhere-row {
  display: grid; grid-template-columns: 7em 9em 1fr 5em;
  gap: var(--sp-2); font-size: var(--text-3xs); align-items: baseline;
}
.favourwhere-head { color: var(--ink-faint); padding-bottom: 3px; border-bottom: 1px solid var(--grid); }
.favourwhere-row { color: var(--ink-muted); padding: 2px 0; }
.favourwhere-row span:nth-child(2) { color: var(--ink-secondary); text-transform: capitalize; }
.favourwhere-row span:last-child {
  font-family: var(--font-mono); text-align: right; white-space: nowrap;
}
/* A boss party is the thing the share counts, so it is marked in the list it
 * is counted from. */
.favourwhere-row.is-boss span:nth-child(2) { color: var(--ink-red); font-weight: 700; }
.favourwhere-open {
  align-self: start; margin-top: var(--sp-2); padding: 3px 10px;
  border-radius: 999px; cursor: pointer; font-size: var(--text-3xs);
  background: var(--surface-raised); border: 1px solid var(--border); color: var(--ink-secondary);
}
.favourwhere-open:hover { border-color: var(--border-strong); color: var(--ink); }
@media (max-width: 760px) {
  .deflist dt { float: none; width: auto; }
  .deflist dd { margin-left: 0; margin-bottom: var(--sp-2); }
  .favourwhere-head, .favourwhere-row { grid-template-columns: 1fr 1fr 3.2em; }
  .favourwhere-head span:nth-child(3), .favourwhere-row span:nth-child(3) { grid-column: 1 / 3; }
}
/* "3 starter branches" beside a place: says why one fight has a level range. */
.favourwhere-var { color: var(--ink-faint); font-style: normal; font-size: var(--text-3xs); }

/* A Pokemon name in the traffic table opens it, the way the name does
   everywhere else. The rest of the rows are labels, not links. */
.usedrow-name.is-clickable { cursor: pointer; }
.usedrow-name.is-clickable:hover { color: var(--ink); text-decoration: underline; }

/* ═══════════════════════════════════════════════════════════ the town map ══
 *
 * The site's own surfaces, not a map palette. A first pass painted sea, land
 * and roads in map colours; it fought everything around it and got dropped.
 * Squares read by kind and by whether anyone is there, which is the part that
 * carries information.
 */
.kanto {
  display: grid; gap: var(--sp-4); align-items: start;
  /* The map gets the room. Sharing a 1fr split with the detail panel left it
     320px wide, which is 20px a square. */
  grid-template-columns: minmax(0, 1.55fr) minmax(280px, 1fr);
}

.kanto-frame {
  background: var(--surface-sunken);
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: var(--sp-3); overflow: hidden;
}
.kanto-map { width: 100%; height: auto; display: block; }

/* Under the squares, and quiet: this is the ground they sit on, not another
   thing to read. */
.kanto-roads line {
  stroke: var(--surface-raised); stroke-width: 12; stroke-linecap: round;
}

.kantocell { cursor: pointer; outline: none; }
.kantocell rect {
  fill: var(--surface-raised); stroke: var(--border); stroke-width: 1;
  transition: fill 0.12s;
}
.kantocell text {
  font-family: var(--font-mono); font-size: 13px; fill: var(--ink-muted);
  pointer-events: none;
}
.kantocell.is-town rect { fill: #2a2320; stroke: var(--border-strong); }
.kantocell.is-place rect { fill: #1e2429; }
.kantocell.has-trainers rect { stroke: var(--ink-faint); }
.kantocell.has-trainers text { fill: var(--ink-secondary); }
.kantocell:hover rect,
.kantocell:focus-visible rect { fill: var(--brand-wash); stroke: var(--brand); }
.kantocell.is-on rect { fill: var(--brand); stroke: var(--brand); }
.kantocell.is-on text { fill: #fff; }

/* The panel scrolls itself. Indigo Plateau carries 38 Pokemon, which ran to
   three screens and pushed the map off the top of the page. */
.kanto-detail {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); padding: var(--sp-3);
  max-height: 620px; overflow-y: auto;
}
.kanto-hint { color: var(--ink-faint); font-size: var(--text-xs); margin: 0; }
.kanto-name { font-size: var(--text-lg); margin-bottom: 2px; }
.kanto-meta { color: var(--ink-secondary); font-size: var(--text-xs); margin: 0 0 var(--sp-2); }
.kanto-inside { color: var(--ink-muted); font-size: var(--text-2xs); margin: 0 0 var(--sp-2); }
.kanto-list { display: flex; flex-direction: column; gap: 6px; }
.kanto-row {
  display: grid; grid-template-columns: 7.5em minmax(0, 1fr); gap: var(--sp-2);
  align-items: baseline; padding-top: 6px; border-top: 1px solid var(--grid);
}
.kanto-who {
  font-size: var(--text-3xs); color: var(--ink-secondary);
  text-transform: capitalize;
}
.kanto-variants {
  display: block; font-style: normal; color: var(--ink-faint);
  font-size: var(--text-3xs); text-transform: none;
}
.kanto-party { display: flex; flex-wrap: wrap; gap: 4px; }
.kanto-mon {
  border: 1px solid var(--border); background: var(--surface-sunken);
  color: var(--ink-secondary); border-radius: 999px; cursor: pointer;
  padding: 2px 8px; font-size: var(--text-3xs);
}
.kanto-mon:hover { color: var(--ink); border-color: var(--border-strong); }
.kanto-mon i { font-style: normal; color: var(--ink-faint); font-family: var(--font-mono); }

@media (max-width: 900px) {
  .kanto { grid-template-columns: minmax(0, 1fr); }
  .kanto-detail { max-height: none; }
}

/* Wild encounters for a map square. A grid rather than a list because the
   question is "what lives here", which is answered by scanning, and the
   commonest ones are at the top where a scan starts. */
.kanto-wild { margin-top: var(--sp-3); }
.kanto-sub {
  font-size: var(--text-3xs); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--ink-muted); margin-bottom: var(--sp-2);
  padding-top: var(--sp-2); border-top: 1px solid var(--border);
}
.kanto-sub span { font-family: var(--font-mono); color: var(--ink-secondary); }
.wildgrid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px; }
.wildcell {
  display: grid; align-items: center; gap: var(--sp-2);
  grid-template-columns: 7em minmax(0, 1fr) 2.8em 3.6em;
  padding: 3px var(--sp-2); border: 0; border-radius: var(--radius);
  background: none; color: var(--ink-secondary); cursor: pointer;
  font-size: var(--text-3xs); text-align: left;
}
.wildcell:hover { background: var(--surface-hover); color: var(--ink); }
.wildcell-name { text-align: left; }
.wildcell-rate {
  height: 6px; border-radius: 3px; background: var(--surface-sunken);
  overflow: hidden; display: block;
}
.wildcell-rate > span { display: block; height: 100%; background: var(--ink-green); }
.wildcell-n, .wildcell-lv { font-family: var(--font-mono); text-align: right; color: var(--ink-muted); }
/* The method is a header now, so it is asked once per group instead of
   repeated on every row. Quieter than the section title above it: this is a
   subdivision, not a new subject. */
.wildmethod {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: var(--text-3xs); font-weight: 600; color: var(--ink-secondary);
  margin: var(--sp-2) 0 3px; padding-bottom: 2px;
  border-bottom: 1px solid var(--grid);
}
.wildmethod:first-of-type { margin-top: 0; }
.wildmethod span { font-family: var(--font-mono); color: var(--ink-faint); font-weight: 400; }

/* A pinned square keeps its fill and gains a ring, so it stays findable on the
   map while the pointer is over on the panel. */
.kantocell.is-pinned rect {
  fill: var(--brand); stroke: #fff; stroke-width: 2.5;
}
.kanto-pin {
  border: 1px solid var(--brand); background: var(--brand-wash);
  color: var(--ink-secondary); border-radius: 999px; cursor: pointer;
  padding: 1px 9px; font-size: var(--text-3xs); vertical-align: middle;
}
.kanto-pin:hover { color: var(--ink); border-color: #fff; }
