@font-face {
  font-family: "Press Start 2P";
  src: url("assets/fonts/PressStart2P.ttf") format("truetype");
  font-display: swap;
}

@font-face {
  font-family: "VT323";
  src: url("assets/fonts/VT323.ttf") format("truetype");
  font-display: swap;
}

:root {
  --px: "Press Start 2P", monospace;
  --vt: "VT323", monospace;
  /* No wordmark face. Every page title used to be set in Basset, loaded from a
     Typekit kit. The site's only runtime third-party host. The drawn logo in
     the corner is the wordmark now, so the page title beside it is ordinary UI
     text in --px and the kit is gone from every <head>. */

  /* Control type scale. Three sizes, keyed to what kind of control it is.
     Before this, the same kind of button was 9px on the desktop header, 8px
     in the mobile bar and 7px in a dropdown, because each spot had been tuned
     on its own.

       --btn-fs      primary controls: buttons, tabs, page links
       --btn-fs-sm   dense controls: type chips, segmented controls, menu options
       --field-fs    things you type in or choose from: search fields, selects

     The mobile block near the bottom of this file drops each one a step; that
     is the only place they change. Nothing should set a font-size on a control
     directly. If a label does not fit, shorten the label or widen the
     control. */
  --btn-fs: 9px;
  --btn-fs-sm: 8px;
  --field-fs: 19px;

  --paper: #f8efd8;
  --paper-2: #efe3c2;
  --paper-ink: #2a251a;
  --paper-dim: #6b6250;
  /* Rules and outlines on paper surfaces. Its own variable and not --paper-ink,
     which these used to borrow: that one has to go light in dark mode because
     it is the TEXT colour, which turned every detail tab's outline and the rule
     under them white. A border is not text and does not flip with it. Same
     value as --paper-ink in the light palette, so nothing moves there. */
  --paper-edge: #2a251a;

  --c-normal: #a8a77a; --c-fire: #f08030; --c-water: #6390f0;
  --c-electric: #f7d02c; --c-grass: #7ac74c; --c-ice: #96d9d6;
  --c-fighting: #d3425f; --c-poison: #b567ce; --c-ground: #e2bf65;
  --c-flying: #a98ff3; --c-psychic: #f95587; --c-bug: #a6b91a;
  --c-rock: #b6a136; --c-ghost: #7975d4; --c-dragon: #6f35fc;
  --c-dark: #75574c; --c-steel: #b7b7ce; --c-fairy: #d685ad;

  /* One colour per game, for the version chips on a dex entry. Taken from each
     game's box or mascot, so Sword is Zacian blue and Shield is Zamazenta red.
     Every version key in data.json needs an entry here or its chip falls back
     to the plain grey. */
  /* Gen 1 */
  --v-red: #d0342c; --v-blue: #3d6dc4; --v-yellow: #f2c218;
  --v-red-japan: #d0342c; --v-green-japan: #3fa34a; --v-blue-japan: #5b8fd6;
  /* Gen 2 */
  --v-gold: #d5a533; --v-silver: #a7aeb5; --v-crystal: #5fc4d8;
  /* Gen 3 */
  --v-ruby: #c0392b; --v-sapphire: #2a6fbe; --v-emerald: #2f9e5e;
  --v-firered: #ef6a2b; --v-leafgreen: #63b544; --v-colosseum: #6b5b8a;
  --v-xd: #7a4fa3;
  /* Gen 4 */
  --v-diamond: #7fb2dd; --v-pearl: #e39ab5; --v-platinum: #8e9aa6;
  --v-heartgold: #d5a533; --v-soulsilver: #a7aeb5;
  /* Gen 5 */
  --v-black: #33333a; --v-white: #e8e8e8; --v-black-2: #33333a;
  --v-white-2: #e8e8e8;
  /* Gen 6 */
  --v-x: #2f7ec4; --v-y: #c0392b; --v-omega-ruby: #b8342a;
  --v-alpha-sapphire: #2360a8;
  /* Gen 7 */
  --v-sun: #f0972b; --v-moon: #5a52a8; --v-ultra-sun: #e0761b;
  --v-ultra-moon: #463f92; --v-lets-go-pikachu: #f2c218; --v-lets-go-eevee: #b98254;
  /* Gen 8 */
  --v-sword: #2f80c4; --v-shield: #c8353c; --v-the-isle-of-armor: #3fa38c;
  --v-the-crown-tundra: #8cc0dd; --v-brilliant-diamond: #7fb2dd; --v-shining-pearl: #e39ab5;
  --v-legends-arceus: #c9a44c;
  /* Gen 9 */
  --v-scarlet: #d4432c; --v-violet: #7c4fa8; --v-the-teal-mask: #2f9e93;
  --v-the-indigo-disk: #3f4fa8; --v-legends-z-a: #3fa34a;
  /* Gen 10: the soft mint of the Pokemon Winds wordmark. Pale, so the chip
     takes dark ink like the other light games. */
  --v-winds-waves: #8ed3a6;
}

/* ---------- Handheld-screen palette (bright, reads on any wallpaper) ----------
   JS sets --wall, --scene, --accent, --accent-ink and --field per wallpaper. */

body {
  --field: #cbb78a;
  --wall: none;
  --scene: none;
  --accent: #e5484d;
  --accent-ink: #fff;

  --panel: color-mix(in srgb, var(--field) 14%, #f7f2e6);
  --panel-2: color-mix(in srgb, var(--field) 22%, #ece3cd);
  --edge: #37301f;
  --edge-hi: #fffdf3;
  --ink: #2b2519;
  --ink-dim: #7a7058;
}

/* ---------- Dark mode ----------

   A standalone toggle in the header, unlike Dream World and Porygon's Cache
   where the palette is tied to which companion is out. Class on <html>, set
   pre-paint by an inline script in index.html so a reload never flashes light.

   The wallpaper is untouched. --field, --wall and --accent are
   the scenery and the wallpaper picker owns them; this flips only the surfaces
   drawn ON that scenery: panels, cards, borders, text. So Forest stays
   Forest at midnight, and every wallpaper keeps working without a second
   dark-mode colour being chosen for each of the eighteen.

   --panel and --panel-2 keep mixing in --field exactly as the light palette
   does, so a panel still picks up a tint from the wallpaper behind it instead
   of going flat grey. Only the base they mix into changes. */
.dex-dark {
  --paper: #1b2133;
  --paper-2: #232a40;
  --paper-ink: #e9edf9;
  --paper-dim: #99a4c2;
  /* Matches --edge, so a rule on a dialog reads like every other border on the
     page instead of glowing white against it. */
  --paper-edge: #05070e;
}
.dex-dark body {
  --panel: color-mix(in srgb, var(--field) 12%, #191f2f);
  --panel-2: color-mix(in srgb, var(--field) 18%, #141926);
  --edge: #05070e;
  --edge-hi: #3a4460;
  --ink: #e9edf9;
  --ink-dim: #98a3c1;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

/* Site-wide: on iOS Safari the rubber-band overscroll bounce reveals blank
   space (or whatever sits behind a fixed overlay) past the top/bottom edge of
   the page. overscroll-behavior kills that bounce everywhere for free, so no
   position:fixed body-locking hack is needed (that trick pins the ENTIRE page
   as one compositor layer, which is expensive/crash-prone on a page this
   long). */
html, body { overscroll-behavior-y: none; }

/* Site-wide: showModal() moves focus to the first control inside the dialog,
   and Safari paints its blue ring for that even though nobody pressed a key.
   It landed on the first sprite in the archive and on the close X elsewhere.
   Chrome hides it under its own :focus-visible heuristics. That is why it only
   showed up on iPhone and desktop Safari.

   Every focus style on this site is already written against :focus-visible, so
   drop the UA ring whenever that does not match. A browser without
   :focus-visible throws the whole rule out and keeps its ring, which is the
   safe way to fail. */
:focus:not(:focus-visible) { outline: none; }

body {
  margin: 0;
  font-family: var(--vt);
  color: var(--ink);
  background: var(--field) var(--wall) repeat top left;
  background-size: 96px auto;
  min-height: 100vh;
  transition: background-color 0.4s;
}


/* Particle field sits above the wallpaper, below the cards. */
#fx {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

img { image-rendering: pixelated; }
img.smooth, .smooth { image-rendering: auto; }

/* ---------- Pixel building blocks ---------- */

.pxbox {
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  box-shadow:
    inset 0 0 0 2px var(--edge-hi),
    4px 4px 0 rgba(0, 0, 0, 0.35);
}

/* ---------- Top bar ---------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--panel-2);
  border-bottom: 3px solid var(--edge);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
  /* Extend the opaque header up into the iOS status-bar / notch zone so scrolled
     content can't peek through it there. Zero on every device without a safe
     area (env resolves to 0px), so this changes nothing off iOS. */
  padding-top: env(safe-area-inset-top, 0px);
}

.topbar-inner {
  max-width: 1280px;
  margin: 0 auto;
  /* Vertical and horizontal padding differ: 22px top and bottom is the
     header's rhythm, but the sides stay at 24px so the brand row lines up with
     the filter band and the grid, which both sit on a 24px gutter. */
  padding: 22px 24px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.brand-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

/* One optical rhythm across the whole lockup: sprite art, wordmark, divider and
   subtitle all sit 12px apart TO the eye. `gap` alone can't deliver that,
   because two of these carry invisible space a box-level gap knows nothing
   about: the sprite's transparent canvas margin and the wordmark's italic
   overhang. Both are cancelled at their source below, so this number means
   what it says. */
.brand { display: flex; align-items: center; gap: 12px; }

.brand h1 {
  margin: 0;
  font-family: var(--px);
  font-size: 16px;
  letter-spacing: 0.04em;
  text-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4);
}

/* Every page title is now live text in Basset, replacing the old wordmark
   SVGs, so the <h1> carries the real words and the title is selectable and
   translatable.

   Where 28px comes from: Basset's caps are 0.667 of the font size, and the
   wordmark this replaced drew its letters 18.6px tall (a 30px tall box whose
   glyphs filled 152.03 of a 245.23 viewBox). 28 x 0.667 = 18.7, so the title
   sits at the same weight against the sprite as it always did. Basset is a
   caps-only face; the markup stays sentence case so the text reads correctly
   when the font does not load, and the shaping does the rest.

   line-height:1 replaces the old 0: that zero only worked because the child
   was a block <img>. Left on text it collapses the h1 and lets long titles
   stack on themselves on a phone.

   Scoped inside .brand for specificity: every one of these titles is already
   in a .brand, and a bare .brand-logo (0,1,0) loses to `.brand h1` (0,1,1)
   above, which would serve the whole site's wordmarks in 16px Press Start 2P.
   The old .brand .tcg-logo rule was written around the same problem. */
/* The page title, beside the corner logo. Press Start 2P, not the Basset
   wordmark face it replaced: with a drawn logo carrying the brand, the title is
   just a label and the site already has a face for those.

   15px, not the 28 Basset ran at. Basset's caps are 0.667 of the font size, so
   28px drew 18.7px letters; Press Start 2P fills its em almost entirely, and 15
   here lands at about the same cap height. No margin-right either, that
   reserved the overhang of a steep italic, and this face is upright.

   Scoped inside .brand: a bare .brand-logo (0,1,0) loses to `.brand h1` (0,1,1)
   above, which would serve every title in 16px. That specificity trap has bitten
   this rule before. */
.brand .brand-logo {
  font-family: var(--px);
  font-size: 15px;
  line-height: 1.4;
  letter-spacing: 0.04em;
  white-space: nowrap;
  color: var(--ink);
  /* No drop shadow. `.brand h1` above throws one, which suited a big wordmark
     but reads as noise on a 15px label sitting under the drawn logo. */
  text-shadow: none;
}

/* The page header shape, everywhere but the dex: corner logo, then the page
   title with its subheading stacked underneath. */
.brand-mark { flex: 0 0 auto; line-height: 0; }
.brand-mark img { display: block; height: 44px; width: auto; }

.brand-stack {
  display: flex;
  flex-direction: column;
  /* Tight: the title and its subheading are one unit, not two rows. */
  gap: 3px;
  min-width: 0;
}

/* Inside the stack the subheading sits UNDER the title, so it drops the divider
   rule and the indent it needs when it sits beside one. */
.brand-stack .brand-tagline {
  padding-left: 0;
  border-left: 0;
  font-size: 8px;
  line-height: 1.5;
}

/* The dex keeps the drawn logo AS its title, with no page name beside it. Scoped to its own
   class so every other page keeps its text title.

   margin-right goes back to 0: the 3.5px above reserves the italic overhang of
   a Basset glyph, and an image has no overhang to reserve. line-height:0 stops
   the h1 keeping a text line's worth of height under the picture. */
.brand .brand-logo-img {
  margin-right: 0;
  line-height: 0;
}
.brand .brand-logo-img img {
  display: block;
  height: 44px;
  width: auto;
}

/* The subheading naming what a page holds. It now sits under its title inside
   .brand-stack, which zeroes the divider below; the rule and indent here are
   what it uses anywhere it still sits beside something. Hidden on the narrower
   widths by the queries further down. */
.brand-tagline {
  margin: 0;
  padding-left: 12px;
  border-left: 2px solid var(--edge-hi);
  font-family: var(--px);
  font-size: 9px;
  line-height: 1.7;
  letter-spacing: 0.04em;
  color: var(--ink-dim);
  /* Never wrap: a two-line subtitle beside a one-line wordmark looks like a
     layout accident, and the extra height pushes the sticky header taller. It
     is hidden entirely below the breakpoint instead. */
  white-space: nowrap;
}
/* Each page's line is a different length, so each disappears at its own width;
   neither defers to the longer one. The numbers are measured: brand + gap +
   buttons + padding comes to 1059px on Dream World and 1246px on Porygon's
   Cache, and below that the topbar wraps the buttons onto a second row, which
   is worse than losing the subtitle. */
@media (max-width: 1080px) {
  .dw-body .brand-tagline { display: none; }
}
@media (max-width: 1270px) {
  .pc-body .brand-tagline { display: none; }
}
/* World Archives, measured the same way: brand + tagline is 609px and its
   actions 463px, which with the 12px gap and 40px of padding needs 1124px
   before the buttons wrap onto a second row. 1130 keeps a little margin. */
@media (max-width: 1130px) {
  .wa-body .brand-tagline { display: none; }
}

/* UNUSED as of the header rework: every page now opens with the drawn corner
   logo, and no markup carries this class. Kept because the sprite is the
   obvious thing to reach for if a compact header is ever wanted again, and the
   arithmetic below took measuring. bebe.webp holds 5x art with a 20px top and
   5px bottom margin on a 155px canvas, so her ink sits 1.65px low once
   contained into 34px, and the negative inline margin pulls her empty canvas
   out of the flex gap. */
.brand-sprite {
  --box: 34px;
  width: var(--box);
  height: var(--box);
  margin-inline: calc((var(--box) * 95 / 155 - var(--box)) / 2);
  object-fit: contain;
  image-rendering: pixelated;
  transform: translateY(-2px);
}


.hdr-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }

/* Desktop: .filters is transparent to layout, so the dex-tabs / controls / type
   rows sit directly in the header exactly as before. Mobile turns it into a
   collapsible overlay (see media query). */
.filters { display: contents; }

/* Desktop header splits into two zones, mirroring Dream World: site-level
   navigation on the brand row (Bebe's PC, Donate, Dream World, Tier List
   Builder), then everything that acts on the dex grouped below a divider. The
   controls previously ran straight on from the brand row as one long block,
   which made the header hard to scan. This also leaves an obvious place to add
   another top-level section alongside Dream World later.

   The band lives OUTSIDE .topbar-inner (see index.html) so it spans the full
   header width like Dream World's tab band, instead of stopping at the content
   cap. Doing it from inside that container would have needed a 100vw escape,
   which overflows horizontally wherever scrollbars take up layout space.

   Scoped to a min-width that pairs with the 720px mobile breakpoint, leaving
   the base rule alone, because mobile turns .filters into a drop-down overlay
   with its own display, background and borders; styling the base rule would
   leak into that. */
@media (min-width: 721px) {
  .filters {
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* Full-bleed band, but the padding re-centres its contents onto the same
       column as the brand row above and the grid below. max() keeps a sane
       gutter once the viewport is narrower than the content cap. Top and bottom
       match, so the first control row sits as far from the divider as the last
       one sits from the header edge. */
    padding: 18px max(24px, calc((100% - 1280px) / 2 + 24px)) 18px;
    border-top: 3px solid var(--edge);
    background: var(--panel);
  }
  /* Collapsed: the whole band goes, divider included, so the header shrinks to
     just the navigation row. */
    body.filters-collapsed .filters { display: none; }
  /* .topbar-inner keeps its base 24px all round, so the brand row sits with
     equal space above it and below it before the divider. */
}

/* Shown only on mobile: the hamburger and the relocated search+shiny bar. */
.mobile-only { display: none; }
.mobile-primary { display: none; }

.count {
  font-family: var(--px);
  font-size: 8px;
  line-height: 1.6;
  color: var(--ink-dim);
  text-align: right;
  max-width: 300px;
}

/* Counter sits just right of the "Bebe's PC" logo. No margin of its own: it is
   the third beat of the lockup and takes the shared 12px like everything else.
   The old 2px was propping up a gap the wordmark's overhang was eating. */
.brand .count {
  align-self: center;
  text-align: left;
  white-space: nowrap;
}

/* ---------- Theme picker ---------- */

/* ---------- Wallpaper picker ---------- */

.wp-wrap { position: relative; }

/* One line, always: names like "Snow 2" and "Team Aqua" were wrapping into a
   two-line button wherever the bar got tight. The button grows instead. */
.wp-btn { display: flex; align-items: center; gap: 7px; white-space: nowrap; }

/* Trim the vertical padding (higher specificity than .px-btn) so the swatch
   doesn't make this button taller than the plain buttons beside it. */
.px-btn.wp-btn { padding: 6px 12px; }

.wp-swatch {
  width: 18px;
  height: 15px;
  border: 2px solid var(--edge);
  background-size: cover;
  background-position: center;
  image-rendering: pixelated;
}

.wp-pop[hidden] { display: none; }

.wp-pop {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 120;
  width: 320px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  padding: 12px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 6px 6px 0 rgba(0, 0, 0, 0.4);
  animation: pop-in 0.18s ease-out both;
}

.wp-opt {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 5px 4px 4px;
  background: none;
  border: 2px solid transparent;
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out;
}

.wp-opt img {
  width: 100%;
  height: 42px;
  object-fit: cover;
  object-position: top;
  border: 2px solid var(--edge);
  image-rendering: pixelated;
}

.wp-opt span {
  font-family: var(--px);
  font-size: 7px;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.wp-opt:hover { transform: translateY(-2px); }
.wp-opt:hover span { color: var(--ink); }

.wp-opt.on { border-color: var(--accent); }
.wp-opt.on span { color: var(--ink); }

/* ---------- Companion picker ---------- */

/* Same shape as the wallpaper picker, but the swatch/options are PMD portraits
   instead of scenery. "None" reuses Mew's portrait, greyed out. */
.fol-wrap { position: relative; }

.fol-btn { display: flex; align-items: center; gap: 7px; }
.px-btn.fol-btn { padding: 6px 12px; }

.fol-swatch {
  width: 18px;
  height: 15px; /* Match .wp-swatch so the button isn't taller than its neighbours */
  border: 2px solid var(--edge);
  background-size: cover;
  background-position: center;
  image-rendering: pixelated;
}

.fol-swatch.dim { filter: grayscale(1) opacity(0.45); }

.fol-pop[hidden] { display: none; }

.fol-pop {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 120;
  width: 288px;
  display: grid;
  /* Minmax(0, ...) because a bare 1fr can't shrink below the label's
     min-content width, which let the longest name (JIRACHI) steal space and
     squeeze the shorter ones into slivers. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
  padding: 12px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 6px 6px 0 rgba(0, 0, 0, 0.4);
  animation: pop-in 0.18s ease-out both;
}

.fol-opt {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 5px 4px 4px;
  background: none;
  border: 2px solid transparent;
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out;
}

/* A fixed square (width:100% would vary) keeps every option the same shape
   whatever its label. 44px = the 40px portrait + 2px border each side
   (border-box), so the pixel art still renders 1:1; a fractional scale would
   blur it. */
.fol-opt img {
  width: 44px;
  height: 44px;
  object-fit: contain;
  border: 2px solid var(--edge);
  background: var(--panel);
  image-rendering: pixelated;
}

.fol-opt img.dim { filter: grayscale(1) opacity(0.45); }

.fol-opt span {
  font-family: var(--px);
  font-size: 7px;
  text-transform: uppercase;
  color: var(--ink-dim);
  white-space: nowrap;
}

.fol-opt:hover { transform: translateY(-2px); }
.fol-opt:hover span { color: var(--ink); }

.fol-opt.on { border-color: var(--accent); }
.fol-opt.on span { color: var(--ink); }


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

.dex-tabs { display: flex; gap: 6px; flex-wrap: wrap; }

.dex-tab {
  font-family: var(--px);
  font-size: var(--btn-fs);
  padding: 9px 13px 8px;
  color: var(--ink-dim);
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out, box-shadow 0.12s ease-out, color 0.12s;
}

.dex-tab:hover { color: var(--ink); transform: translate(-1px, -1px); box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); }
.dex-tab:active { transform: translate(1px, 1px); box-shadow: none; }

.dex-tab.on {
  color: var(--accent-ink);
  background: var(--accent);
  box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.25), 3px 3px 0 rgba(0, 0, 0, 0.3);
}

/* On desktop the action buttons (sprite toggle/wallpaper/team/mew/guide/shiny)
   sit on the dex-tabs row; the sprite toggle starts the right-aligned group so
   the count stays next to "Other" on the left. */
.dex-tabs { align-items: center; }
.dex-tabs > .spr-toggle { margin-left: auto; }

/* Sprite/HOME toggle: a plain square icon button, no label. */
/* Needs the .px-btn double-class specificity to zero the padding: plain
   .px-btn { padding: 9px 12px } is declared later and would otherwise win,
   leaving a 4px content box that squeezes the icon to a sliver. Same fix the
   wallpaper button uses. */
.px-btn.spr-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Square, sized to the 33px the sibling .px-btns resolve to (9px padding +
     9px line box + 3px borders), so the row of buttons lines up flush. */
  width: 33px;
  height: 33px;
  padding: 0;
  flex: 0 0 auto;
}
/* Sits a little inset in the 33px square. The disc isn't at an exact integer
   scale; it relies on shape-rendering:crispEdges (set in the markup) to keep
   its steps hard-edged at this size. */
.spr-toggle svg { width: 20px; height: 20px; display: block; flex: 0 0 auto; }

/* Positive/negative pair, both carrying the active wallpaper's accent colour
   (JS sets --accent per wallpaper, so these follow the scenery automatically):
     pixel mode  -> blocky disc filled with the accent on the plain button
     HOME mode   -> inverted, the whole square fills with the accent and a
                    smooth circle is knocked out of it.
   The knockout uses --accent-ink, which JS derives per wallpaper as
   black-or-white against that accent, so it lands on white for these mid-dark
   accents and reads exactly like a punched hole. */
.spr-toggle .spr-px rect { fill: var(--accent); }
.spr-toggle .spr-round circle { fill: var(--accent-ink); }
.px-btn.spr-toggle.on { background: var(--accent); }

/* The tier toolbar stretches its controls to 38px (set by the search field's
   taller box), so the square is 38px there, versus the header's 33px.
   Both numbers are "match the siblings"; aspect-ratio can't do it
   automatically, since a flex item's main size still comes from its content. */
.tier-toolbar .px-btn.spr-toggle {
  width: 38px;
  height: 38px;
}

/* .on marks HOME mode, so the smooth circle shows there and the disc otherwise. */
.spr-toggle .spr-round { display: none; }
.spr-toggle.on .spr-px { display: none; }
.spr-toggle.on .spr-round { display: block; }

/* ---------- Site-wide search ----------
   Sits at the left of the header actions, before Donate. Not to be confused
   with #q in the filter band, which narrows the grid; this one navigates. */
/* .topbar-inner is capped at max-width 1280px, so the header's budget does NOT
   grow with the screen, there is the same room at 2560px as at 1280px, and
   this width has to be measured against it, not picked.

   Measured, the other controls take 529px of the 712px this row gets. At 220px
   the dark-mode and collapse buttons were pushed onto a second line at EVERY
   width, so this is 170px. Dropping Bebe's sprite from the corner
   gave the brand back 46px, so there is slack now, but re-measure before
   spending it, and re-measure again if anything else joins this row. */
.site-search { position: relative; flex: 0 1 170px; min-width: 120px; }
/* .px-btn.ss-open, not .ss-open: .px-btn is defined further down this file,
   so a bare class ties on specificity and loses on source order, which is
   why .spr-toggle, .dark-toggle and .filters-toggle are all written this way
   too. Left plain, .px-btn's 9px 12px padding squeezed the magnifier into a
   9px box and it rendered 3px wide. */
.px-btn.ss-open { display: none; }
#siteSearchInput {
  width: 100%;
  height: 33px;                 /* Matches the buttons either side of it */
  padding: 0 10px;
  font-family: var(--vt);
  font-size: 17px;
  color: var(--ink);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  outline: none;
}
#siteSearchInput::placeholder { color: var(--ink-dim); }
#siteSearchInput:focus { box-shadow: inset 0 0 0 2px var(--accent); }

.ss-pop {
  position: absolute;
  top: calc(100% + 6px);
  /* Start flush with the box; app.js nudges it after each render so it stays
     on screen. A fixed anchor cannot work here: the header row wraps at some
     widths, which moves this box from the right-hand end of the row to the
     left, and whichever side were pinned would be the wrong one half the
     time. */
  left: 0;
  /* Wider than the bar: "Gym Leaders, Elite Four and Champions" has nowhere
     to go in 220px. */
  width: max(430px, 100%);
  z-index: 60;
  max-height: 60vh;
  overflow: auto;
  padding: 4px;
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.35);
}
.ss-row {
  width: 100%;
  display: grid;
  /* The label gets a floor and the hint gives way: with plain `auto` on the
     hint, a long one squeezed "Dream World" down to "Dream W...". */
  grid-template-columns: 28px minmax(110px, 1fr) minmax(0, auto);
  align-items: center;
  gap: 8px;
  padding: 6px 7px;
  font-family: var(--vt);
  font-size: 17px;
  text-align: left;
  color: var(--ink);
  background: none;
  border: 0;
  border-radius: 3px;
  cursor: pointer;
}
.ss-row.on { color: var(--accent-ink); background: var(--accent); }
.ss-art { width: 28px; height: 28px; object-fit: contain; image-rendering: pixelated; }
.ss-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ss-kind {
  font-family: var(--px);
  font-size: 7px;
  text-transform: uppercase;
  color: var(--ink-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ss-row.on .ss-kind { color: var(--accent-ink); opacity: 0.85; }
.ss-empty {
  padding: 10px 8px;
  font-family: var(--vt);
  font-size: 17px;
  color: var(--ink-dim);
}

/* Below this width the header cannot hold the bar AND the four controls beside
   it: measured at 900px there were 31px spare, so the row wrapped onto two
   lines. Instead of let that happen the bar becomes a square icon and the
   field opens over the header when clicked. The threshold is where a usable
   ~190px field stops fitting, not a round number picked by eye.

   1080px is already a breakpoint further up this file, for the Dream World
   tagline. Same number, unrelated reason. Do not merge the two blocks. */
@media (max-width: 1080px) {
  .site-search { flex: 0 0 auto; min-width: 0; }
  .px-btn.ss-open {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 33px;
    height: 33px;
    padding: 0;
  }
  .px-btn.ss-open svg { width: 19px; height: 19px; color: var(--accent); }
  /* Absolute, so opening the field never reflows the row it sits in. */
  #siteSearchInput {
    position: absolute;
    right: 0;
    top: 0;
    width: 260px;
    z-index: 61;
    display: none;
  }
  .site-search.open #siteSearchInput { display: block; }
  .site-search.open .px-btn.ss-open { visibility: hidden; }
}

/* Dark mode toggle. Same 33px square as the collapse control beside it, and
   the icon carries the wallpaper's accent like the sprite toggle does, so it
   belongs to the row instead of sitting on it. */
.px-btn.dark-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 33px;
  height: 33px;
  padding: 0;
  flex: 0 0 auto;
}
/* One size for both: each icon's artwork fills its own viewBox edge to edge
   (measured, not assumed), so equal boxes give equal ink. An earlier pair
   needed different numbers because that sun carried its rays as padding around
   a much smaller disc, if the artwork is ever swapped again, check that
   before trusting this single rule. */
.dark-toggle svg { width: 18px; height: 18px; display: block; fill: var(--accent); }
/* Shows the mode you are in, not the one you would switch to: sun while light,
   moon once dark. Matches .spr-toggle, which also shows its current state. */
.dark-toggle .dm-moon { display: none; }
.dex-dark .dark-toggle .dm-sun { display: none; }
.dex-dark .dark-toggle .dm-moon { display: block; }

/* Collapse control for the filter band. Square icon button like the sprite
   toggle, so it lines up with its neighbours in the row. The triangle points up
   while the band is open (click to hide) and flips when it is collapsed. */
.px-btn.filters-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 33px;
  height: 33px;
  padding: 0;
  flex: 0 0 auto;
}
.filters-toggle svg {
  width: 12px;
  height: 8px;
  fill: currentColor;
  transition: rotate 0.18s ease-out;
}
/* The archive pages' toolbar and the card browser's, collapsed by the same
   body flag. NOT inside a media query: these two collapse at every width,
  , and that is what the arrow is for on a phone, where the header is just
   the logo and this control and everything else lives in the strip below. */
body.filters-collapsed .beta-toolbar,
body.filters-collapsed .tier-toolbar,
body.filters-collapsed .tcg-toolbar { display: none; }

body.filters-collapsed .filters-toggle svg { rotate: 180deg; }

/* Button icons are inline SVGs, replacing the &#10022; and &#9776; characters.
   Those were sized by font-size, so at the 8-9px these rows use they were
   unreadable, and their baselines never lined up with the label. These sit in
   a flex row with the text, which also centres the pair properly. */
.btn-ico { fill: currentColor; flex: 0 0 auto; }
.ico-shiny { width: 13px; height: 13px; }
/* 13x10 is 1.30; the mark's own viewBox is 431.29x332.48, or 1.297. Close
   enough that the box needs no change. NOTE the markup drops the
   `shape-rendering="crispEdges"` the old three-bar icon carried: that mark sat
   on a 10x8 integer grid where snapping was free, and this one does not, so
   snapping would round its three bars to different thicknesses. */
.ico-menu { width: 13px; height: 10px; }
/* Alignment only. No `display` here: .adv-toggle is .mobile-only, and
   `.mobile-only { display: none }` further up carries the same specificity,
   so setting display on this shared rule would win on document order alone and
   leak the Filters button onto desktop. Each button gets its display where it
   is actually shown. */
.shiny-tab, .adv-toggle {
  align-items: center;
  justify-content: center;
  gap: 6px;
}
/* The 13px icon is taller than the 9px text line it sits beside, so it would
   otherwise push this button past the dex tabs it lines up with. Give the
   difference back from the vertical padding to hold 32px. Mobile and the tier
   toolbar set an explicit height, so they are unaffected either way. */
.shiny-tab {
  display: inline-flex;
  padding-top: 7px;
  padding-bottom: 6px;
}

/* Shiny toggle gets a golden sparkle when active. */
.shiny-tab.on {
  color: #3a2c00;
  background: linear-gradient(#ffe485, #f5c518);
  box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.45), 3px 3px 0 rgba(0, 0, 0, 0.3);
}

/* ---------- "Other" category picker (dex-tabs row) ---------- */

.other-select { display: none; }   /* Mobile swaps this in for the popover */

.other-wrap { position: relative; display: inline-flex; }

.other-pop[hidden] { display: none; }

/* ---------- Archives menu ---------- */

/* One button opening the list of sibling collection pages, used by both
   layouts: it sits in the header on desktop and on its own row in the mobile
   bar, styled after the "Other" menu below. The links inside are the same <a>
   elements in either place, so this has to override their header button
   styling outright; adding to it isn't enough. */
.archives-wrap { position: relative; display: flex; }
.archives-btn {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.archives-pop[hidden] { display: none; }
.archives-pop {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 120;
  width: max(190px, 100%);
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 6px 6px 0 rgba(0, 0, 0, 0.4);
  animation: pop-in 0.18s ease-out both;
}
.archives-pop .archives-opt {
  display: flex;
  align-items: center;
  justify-content: center;
  height: auto;
  padding: 11px 10px 10px;
  font-size: var(--btn-fs-sm);
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 3px;
}
.archives-pop .archives-opt:hover { color: var(--ink); }

/* The entry for the page you are already on. It keeps its slot so the menu
   reads identically everywhere, but has no href and does not respond, so it
   cannot be mistaken for somewhere else to go. */
.archives-pop .archives-opt.on {
  background: var(--accent);
  color: var(--accent-ink);
  cursor: default;
  pointer-events: none;
}

.other-pop {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 120;
  width: 230px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 6px 6px 0 rgba(0, 0, 0, 0.4);
  animation: pop-in 0.18s ease-out both;
}

.other-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 10px 8px;
  font-family: var(--px);
  font-size: var(--btn-fs-sm);
  /* Full ink: a menu row is pressable, and dim means "toggle is off". */
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out, color 0.12s, background 0.12s;
}

.other-opt:hover { color: var(--ink); transform: translate(-1px, -1px); }

.other-opt.on {
  color: var(--accent-ink);
  background: var(--accent);
  border-color: var(--edge);
}

.oc-count { font-size: 8px; color: var(--ink-dim); }
.other-opt.on .oc-count { color: var(--accent-ink); }

/* ---------- Controls ---------- */

.controls { display: flex; gap: 8px; flex-wrap: wrap; align-items: stretch; }

.search-wrap { position: relative; flex: 1 1 200px; min-width: 180px; }

.search-icon {
  position: absolute;
  left: 10px;
  top: 50%;
  translate: 0 -50%;
  width: 14px;
  height: 14px;
  color: var(--ink-dim);
  pointer-events: none;
}

#search, #tlSearch, .select, .px-btn {
  font-family: var(--vt);
  font-size: var(--field-fs);
  color: var(--ink);
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  outline: none;
}

#search, #tlSearch {
  width: 100%;
  padding: 7px 12px 6px 32px;
  transition: box-shadow 0.12s;
}

#search::placeholder, #tlSearch::placeholder { color: var(--ink-dim); }
#search:focus, #tlSearch:focus { box-shadow: inset 0 0 0 2px var(--accent); }

.select {
  appearance: none;
  -webkit-appearance: none;
  padding: 7px 30px 6px 12px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7'%3E%3Cpath d='M0 0h12L6 7z' fill='%23888'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  cursor: pointer;
  transition: transform 0.12s ease-out, box-shadow 0.12s;
}

.select:hover { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); }
.select:focus { box-shadow: inset 0 0 0 2px var(--accent); }

.px-btn, .clear-btn {
  font-family: var(--px);
  font-size: var(--btn-fs);
  padding: 9px 12px;
  color: var(--ink);
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out, box-shadow 0.12s ease-out;
}

.px-btn:hover, .clear-btn:hover { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3); }
.px-btn:active, .clear-btn:active { transform: translate(1px, 1px); box-shadow: none; }

.clear-btn { color: #fff; background: #d3425f; border-color: #47101d; }

.px-btn.on {
  color: var(--accent-ink);
  background: var(--accent);
  box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.25);
}

/* Dream World nav + its Back link are <a>s styled as buttons, so they need the
   underline reset to match the <button>s beside them. */
.beta-back-short { display: none; }

/* An author `display` beats the `hidden` attribute at any specificity, so any
   rule that sets display needs this companion or `hidden` silently does
   nothing. That is exactly how the parked Trading Cards button shipped visible.
   Every other toggled component here carries the same guard. */
.beta-link[hidden], .beta-back-btn[hidden] { display: none; }

.beta-link, .beta-back-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  color: var(--ink);
}

/* Donate: gold, with a gentle glow pulse so it catches the eye without shouting.
   An <a>, so it needs the flex/text-decoration reset to sit like the buttons. */
.donate-btn {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  color: #3a2c05;
  background: linear-gradient(180deg, #ffe27a, #f4c517);
  border-color: #7a5c0f;
  animation: donate-pulse 2.2s ease-in-out infinite;
}
@keyframes donate-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(245, 197, 24, 0); }
  50%      { box-shadow: 0 0 9px 1px rgba(245, 197, 24, 0.8); }
}
.donate-btn:hover {
  transform: translate(-1px, -1px);
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3), 0 0 9px 1px rgba(245, 197, 24, 0.7);
  filter: brightness(1.06);
  animation: none;
}
.donate-btn:active { transform: translate(1px, 1px); box-shadow: none; }
/* Respect reduced-motion: drop the pulse, keep the gold. */
@media (prefers-reduced-motion: reduce) { .donate-btn { animation: none; } }

/* ---------- Type chips ---------- */

/* Chips stretch to fill the row edge-to-edge (flush with the controls above),
   sitting on one line and wrapping to more flush rows on narrow viewports. */
.type-row { display: flex; gap: 5px; flex-wrap: wrap; }

.type-chip {
  --tc: #999;
  flex: 1 1 auto;
  text-align: center;
  padding: 6px 6px 5px;
  font-family: var(--px);
  font-size: var(--btn-fs-sm);
  text-transform: uppercase;
  color: var(--ink-dim);
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out, box-shadow 0.12s ease-out, background 0.12s, color 0.12s;
}

.type-chip:hover { color: var(--ink); transform: translate(-1px, -2px); box-shadow: 2px 3px 0 rgba(0, 0, 0, 0.3); }
.type-chip:active { transform: translate(1px, 1px); box-shadow: none; }

.type-chip.on {
  color: #16130c;
  background: var(--tc);
  box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.35), 2px 2px 0 rgba(0, 0, 0, 0.35);
}

/* Two types already picked: the rest go inert (no third type exists). */
.type-chip.chip-off {
  opacity: 0.3;
  pointer-events: none;
}

/* ---------- Grid ---------- */

main { max-width: 1280px; margin: 0 auto; padding: 24px; }

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(156px, 1fr));
  gap: 16px;
}

/* Cards are <a> elements (crawlable, and middle-click/cmd-click behave as
   people expect), so the link defaults have to be cleared: the underline would
   otherwise run through every child and the blue would leak into anything the
   rules below do not colour explicitly. */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px 10px 12px;
  font-family: inherit;
  color: inherit;
  text-decoration: none;
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 4px 4px 0 rgba(0, 0, 0, 0.35);
  cursor: pointer;
  content-visibility: auto;
  contain-intrinsic-size: 208px;
  transition: transform 0.13s ease-out, box-shadow 0.13s ease-out;
  animation: pop-in 0.28s cubic-bezier(0.25, 1.4, 0.5, 1) both;
  animation-delay: calc(var(--i, 0) * 12ms);
}

@keyframes pop-in {
  from { opacity: 0; transform: scale(0.88) translateY(10px); }
  to { opacity: 1; transform: none; }
}

.card:hover {
  transform: translate(-3px, -3px);
  box-shadow: inset 0 0 0 2px var(--edge-hi), 8px 8px 0 rgba(0, 0, 0, 0.35);
}

.card:active { transform: translate(1px, 1px); box-shadow: inset 0 0 0 2px var(--edge-hi), 2px 2px 0 rgba(0, 0, 0, 0.35); }

.card .art {
  height: 96px;
  display: grid;
  place-items: center;
}

.card img { object-fit: contain; }

/* HOME (3D) renders fill their frame (~90%), while the Gen 1-5 pixel sprites
   only occupy ~40-70% of theirs, so the HOME renders looked much bigger in the
   grid. Shrink the smooth (HOME / form / Z-A) renders toward the pixel sprites'
   footprint. Uses the individual `scale` property (relative to the sprite's own
   size, so it tracks the pixel sprites at any viewport). Kept off `transform`
   so the hover bounce, which animates translateY, doesn't reset it. */
.card img.smooth { scale: 0.75; }

.card:hover img { animation: sprite-bounce 0.5s steps(2) infinite; }

@keyframes sprite-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}

.card .num {
  position: absolute;
  top: 8px;
  right: 10px;
  font-family: var(--px);
  font-size: 8px;
  color: var(--ink-dim);
}

.card .name {
  margin-top: 8px;
  font-family: var(--px);
  font-size: 9px;
  line-height: 1.5;
  text-align: center;
  color: var(--ink);
}

.card .types { display: flex; gap: 4px; margin-top: 8px; }

.type-badge {
  --tc: #888;
  padding: 4px 6px 3px;
  font-family: var(--px);
  font-size: 7px;
  text-transform: uppercase;
  color: #16130c;
  background: var(--tc);
  border: 2px solid var(--edge);
  border-radius: 2px;
  box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.35);
}

/* ---------- Empty state ---------- */

.empty { text-align: center; padding: 80px 0; }
.empty-title { font-family: var(--px); font-size: 14px; margin: 0 0 10px; }
.empty-sub { font-size: 20px; color: var(--ink-dim); margin: 0; }

/* ---------- Detail dialog ---------- */

.detail {
  width: min(880px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  padding: 0;
  border: 4px solid var(--edge);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink);
  box-shadow: inset 0 0 0 3px var(--edge-hi), 10px 10px 0 rgba(0, 0, 0, 0.45);
  overflow: hidden;
}

/* ---------- Mode attachment: Pokedex / Cards ---------- */

/* The tabs hang outside the dialog's left edge, so the dialog can no longer
   clip its children. Only the 4px corner radius relied on that, and .detail-body
   still clips its own scroll, so nothing else changes. */
.detail { overflow: visible; }

.dmodes {
  position: absolute;
  /* Flush with the card's top edge. -4px backs out the dialog's own border, so
     the tab's outer edge lines up with the card's outer edge; otherwise it
     starts a border's width below it. */
  top: -4px;
  left: 0;
  transform: translateX(-100%);
  display: flex;
  flex-direction: column;
  /* Zero gap makes the tabs one continuous strip: they share an edge and look
     like a single attachment clipped to the card. */
  gap: 0;
  z-index: 2;
}

/* REQUIRED. An author `display` beats the `hidden` attribute at any
   specificity, so without this `dModes.hidden = true` does nothing: the tabs
   stay on screen for visitors who do not have the Cards flag, and clicking
   TCG flips the mode while renderDetail (which checks TCG_ON) keeps drawing
   the Pokedex. Every other component here that pairs `hidden` with a display
   rule carries the same line (.wp-pop, .other-pop, .tier-page, ...); this one
   was missed. */
.dmodes[hidden] { display: none; }

/* Taller than it is wide. The card icon is 685x918, so it needs vertical room
   to grow: a square tab caps it at the width and it stays small and illegible
   however large the tab gets. */
.dmode {
  width: 46px;
  height: 62px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 0;
  cursor: pointer;
  color: var(--paper-dim);
  background: var(--panel-2);
  border: 4px solid var(--edge);
  border-right: none;
  border-radius: 0;
  box-shadow: inset 2px 2px 0 var(--edge-hi);
  transition: color 0.12s, background 0.12s;
}

/* The connected-strip treatment, and ONLY where there is a strip. Mobile lays
   these out as two separate segmented buttons, and these rules leaking into it
   gave the second one no top border, so it sat 3px higher than its neighbour
   and neither looked like a rectangle. */
/* 912, not 721: the squeezed band below it lays the strip out horizontally,
   and the shared-edge rule there took the TCG button's TOP border away --
   cropped flat against its neighbour in a row where they no longer touch. */
@media (min-width: 912px) {
  /* Only the outer corners of the strip are rounded; the join stays square. */
  .dmodes > .dmode:first-child { border-radius: 5px 0 0 0; }
  .dmodes > .dmode:last-child { border-radius: 0 0 0 5px; }
  /* One shared edge, no doubled-up border at the join. */
  .dmodes > .dmode + .dmode { border-top: none; }
}

.dmode:hover { color: var(--ink); }

/* The selected tab looks continuous with the card behind it: same fill as
   the dialog, so the strip and the card look like one object. */
.dmode.on {
  color: var(--ink);
  background: var(--panel);
  cursor: default;
}

/* No ring of any kind on these tabs (Sam's call). Keyboard focus reuses the
   hover shift instead, so tabbing is still followable without drawing a box or
   a line around the tab. */
.dmode:focus-visible { outline: none; color: var(--ink); background: var(--panel); }

/* Treat every tap as a tap, never the start of a pan. Without this the browser
   holds the touch back to see whether a scroll is beginning, and inside a
   scrollable dialog it can decide the gesture was a scroll and drop the click. */
.dmode { touch-action: manipulation; }

/* Sized by HEIGHT: the Pokeball is near enough square but the card icon is
   685x918, so forcing both into one square box would squash one or shrink the
   other. Equal height keeps them looking like a matched pair. */
.dmode-ico {
  /* The viewBox carries ~4% padding so the stroke is not clipped; the extra
     pixel over 30 gives that back so the mark renders the size it did
     before. */
  height: 31px;
  width: auto;
  fill: currentColor;
  flex: 0 0 auto;
}

/* The card faces in the TCG icon. They were a hardcoded cream in the source,
   which only looked right on one background. Tracking the tab's own fill
   keeps the linework legible in both states, and on the red active tab that
   mobile uses. */
.dmode { --ico-face: var(--panel-2); }
.dmode.on { --ico-face: var(--panel); }

/* The two icons are drawn differently: the Pokeball is solid mass, the cards
   are outlines. At 19px those outlines thin out to almost nothing and the pair
   stops looking like a set, so the card icon gets its strokes fattened to carry
   the same weight. 18 units against a 918-unit viewBox; heavier than that and
   the little Pokeball on the card closes up into a blob. */
.ico-tcg path {
  stroke: currentColor;
  stroke-width: 18;
  stroke-linejoin: round;
}
/* Desktop keeps the tabs to icons. The labels are for the mobile bar, where
   there is room for them and no tooltip to fall back on. */
.dmode-label { display: none; }

.detail::backdrop { background: rgba(8, 6, 4, 0.65); }

.detail[open] { animation: dialog-pop 0.24s cubic-bezier(0.25, 1.4, 0.5, 1) both; }
.detail[open]::backdrop { animation: fade-in 0.2s ease both; }

@keyframes dialog-pop {
  from { opacity: 0; transform: scale(0.88) translateY(16px); }
  to { opacity: 1; transform: none; }
}

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

.detail-body {
  display: grid;
  grid-template-columns: 310px 1fr;
  max-height: calc(100vh - 72px);
  overflow: auto;
  /* Keep the scroll here. Without this, reaching either end hands the gesture
     to the page behind the dialog, so the grid scrolls away underneath while
     the entry you are reading stays put. */
  overscroll-behavior: contain;
}

.d-left {
  position: sticky;
  top: 0;
  align-self: start;
  padding: 26px 20px 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.d-hero-box {
  height: 200px;
  display: grid;
  place-items: center;
}

.d-hero {
  width: 192px;
  height: 192px;
  object-fit: contain;
  image-rendering: pixelated;
  transform-origin: center;
}
/* The pop lives on the wrapper, not the image. fitHero() puts a transform on
   the image, and this animation ends on "transform: none", which would wipe
   it every time a sprite loaded. */
.d-hero-btn { animation: hero-pop 0.3s cubic-bezier(0.25, 1.5, 0.5, 1) both; }

@keyframes hero-pop {
  from { opacity: 0; transform: scale(0.7); }
  to { opacity: 1; transform: none; }
}

.d-num { font-family: var(--px); font-size: 9px; color: var(--ink-dim); margin-top: 10px; }
/* Long names wrap: "Mega Magearna (Original)" takes two lines. The column
   centres the heading as a block but not the text inside it, so a second line
   would sit hard against the left edge while everything around it is centred.
   line-height:normal resolves to 1.0 in this face, which leaves the two lines
   touching. */
.d-name {
  font-family: var(--px);
  font-size: 17px;
  line-height: 1.3;
  text-align: center;
  margin: 6px 0 0;
}
.d-genus { font-size: 19px; color: var(--ink-dim); margin-top: 4px; }

.d-types { display: flex; gap: 6px; margin-top: 10px; }
.d-types .type-badge { font-size: 8px; padding: 5px 8px 4px; }

/* ---------- Detail stat block ---------- */

/* Sits between the types and the sprite thumbs. --tc is the species' primary
   type colour, already set on .detail-body by renderVariant. */
.d-facts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  width: 100%;
  margin-top: 14px;
  padding: 10px;
  background: var(--tc);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.25);
}

.d-fact { display: flex; flex-direction: column; gap: 3px; }

.d-fact-k {
  font-family: var(--px);
  font-size: 7px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.35);
}

.d-fact-v {
  font-size: 16px;
  color: #fff;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.35);
}

.d-sec-h {
  align-self: flex-start;
  font-family: var(--px);
  font-size: 8px;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin: 14px 0 7px;
}

/* Two columns spanning the full width of the height/weight bar above: normal
   abilities left, hidden right. Multiples stack vertically inside their column. */
.d-abils {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: start;
  gap: 6px 8px;
  width: 100%;
  margin-top: 14px; /* Matches the leading on .d-sec-h, which this grid replaces */
}

.d-abil-col { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.d-abil-col .d-sec-h { margin: 0 0 1px; }

.ab-chip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  width: 100%;
  padding: 6px 8px;
  font-family: inherit;
  font-size: 13px;
  text-align: left;
  color: var(--ink);
  background: var(--panel-2);
  border: 2px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out;
}

.ab-chip > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ab-chip:hover { transform: translateY(-2px); }
.ab-chip.hidden-ab { border-style: dashed; }
.ab-chip.on { border-color: var(--accent); }

/* Non-interactive chips: a new Z-A ability with no blurb, or an unrevealed one. */
.ab-chip.static { cursor: default; }
.ab-chip.static:hover { transform: none; }
.ab-chip.unk { color: var(--ink-dim); font-style: italic; }
.d-fact-v.unk { color: var(--ink-dim); }

/* Spans both columns so the blurb gets the full width. */
.ab-pop-slot { grid-column: 1 / -1; }
.ab-pop-slot:empty { display: none; }

/* The "?" affordance that hints the blurb is one click away. */
.ab-q {
  display: grid;
  place-items: center;
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  font-family: var(--px);
  font-size: 7px;
  color: var(--panel-2);
  background: var(--ink-dim);
  border-radius: 50%;
}

.ab-chip:hover .ab-q { background: var(--accent); color: var(--accent-ink); }

.ab-pop {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 9px 10px;
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 3px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3);
  animation: pop-in 0.15s ease-out both;
}

.ab-pop b { font-family: var(--px); font-size: 8px; text-transform: uppercase; }
.ab-pop span { font-size: 14px; line-height: 1.45; color: var(--ink-dim); }
.ab-pop .dim { font-style: italic; }

.d-weaks { display: flex; flex-wrap: wrap; gap: 5px; width: 100%; }

.weak-badge {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 5px 7px 4px;
  font-family: var(--px);
  font-size: 8px;
  text-transform: uppercase;
  color: #fff;
  background: var(--tc);
  border: 2px solid var(--edge);
  border-radius: 3px;
  text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
}

.weak-badge b { font-size: 7px; opacity: 0.85; }

/* Chips are a FIXED width, no flex-grow: a wrapped row with fewer chips would
   otherwise stretch them larger than the rows above (Eevee's last row).
   Card size wins over edge alignment, though at 78px a three-stage family still
   lands flush with the sprite grid: 3*78 + 2*10 (arrows) + 4*4 (gaps) = 270. */
.d-evos {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  justify-content: center;
  gap: 4px;
  width: 100%;
}

.evo-arrow {
  display: flex;
  flex: 0 0 10px;
  width: 10px;
  align-items: center;
  justify-content: center;
  font-family: var(--px);
  font-size: 10px;
  color: var(--ink-dim);
}

.evo-chip {
  display: flex;
  flex: 0 0 78px;
  width: 78px;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 5px 3px 4px;
  background: var(--panel-2);
  border: 2px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out;
}

/* Branching families (Eevee, Charcadet, Ralts) stack their stages vertically
   with a turned arrow between them, so siblings look like parallel options
   and the layout stays clearly different from the linear single-row form. */
.d-evos.branched { flex-direction: column; align-items: center; gap: 2px; }

.evo-stage {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: stretch;
  gap: 4px;
  width: 100%;
}

.evo-arrow.down {
  flex: 0 0 auto;
  width: auto;
  transform: rotate(90deg);
  padding: 3px 0;
}

.evo-chip img { width: 40px; height: 40px; object-fit: contain; image-rendering: pixelated; }
.evo-chip span {
  font-family: var(--px);
  font-size: 6px;
  color: var(--ink-dim);
  text-align: center;
  line-height: 1.3;
  width: 100%;
  overflow-wrap: anywhere; /* Long names (e.g. Gigantamax forms) shouldn't widen the chip */
}
.evo-chip:hover { transform: translateY(-2px); }
.evo-chip:hover span { color: var(--ink); }

/* The species you're already looking at: highlighted and inert. */
.evo-chip.on { border-color: var(--accent); cursor: default; }
.evo-chip.on span { color: var(--ink); }
.evo-chip.on:hover { transform: none; }

.d-thumbs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  width: 100%;
}

.d-thumb {
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 6px 2px 4px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out, box-shadow 0.12s;
}

.d-thumb:hover { transform: translate(-1px, -1px); box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); }

.d-thumb.on { box-shadow: inset 0 0 0 2px var(--accent); }

/* The box is fixed; fitThumb() scales what's inside it so every sprite's
   artwork ends up the same visual size. A Weedle B/W sprite is a small figure
   on a big transparent canvas while its HOME render fills the frame, so at one
   fixed img size the pixel sprites looked half the size of everything else. */
.d-thumb img {
  width: 42px;
  height: 42px;
  object-fit: contain;
  transform-origin: center;
}
.d-thumb .tl { font-family: var(--px); font-size: 6px; text-transform: uppercase; color: var(--ink-dim); }
/* Marks the Gen VI+ pixel sprites as community work, not Game Freak's. */
.d-thumb .tl-note { font-size: 5px; opacity: 0.75; }

.d-cat {
  font-family: var(--px);
  font-size: 7px;
  color: var(--accent-ink);
  background: var(--accent);
  border: 2px solid var(--edge);
  border-radius: 2px;
  padding: 2px 4px 1px;
  margin-left: 4px;
  vertical-align: middle;
}

/* ---------- Form switcher ---------- */

.d-forms-h {
  width: 100%;
  margin: 18px 0 8px;
  font-family: var(--px);
  font-size: 9px;
  text-transform: uppercase;
  color: var(--ink-dim);
  border-top: 2px dashed var(--edge);
  padding-top: 12px;
}

.d-forms {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  width: 100%;
}

.form-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 6px 2px 4px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out, box-shadow 0.12s;
}

.form-chip img { width: 46px; height: 46px; object-fit: contain; image-rendering: auto; }
.form-chip span {
  font-family: var(--px);
  font-size: 6px;
  line-height: 1.4;
  text-align: center;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.form-chip:hover { transform: translate(-1px, -1px); box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3); }
.form-chip:hover span { color: var(--ink); }
.form-chip.on { box-shadow: inset 0 0 0 2px var(--accent); }
.form-chip.on span { color: var(--ink); }

/* Grid card "has forms" marker */
.card .forms-tag {
  position: absolute;
  top: 8px;
  left: 10px;
  font-family: var(--px);
  font-size: 7px;
  color: var(--ink-dim);
}

/* Entries read like a paper dex page in every theme */
.d-right {
  padding: 16px 24px 20px;
  background: var(--paper);
  color: var(--paper-ink);
  border-left: 3px solid var(--edge);
}

.d-right h3 {
  margin: 0 0 14px;
  font-family: var(--px);
  font-size: 10px;
  text-transform: uppercase;
  color: var(--paper-ink);
  border-bottom: 3px solid var(--paper-edge);
  padding-bottom: 8px;
}

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

.d-tabs {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  gap: 5px;
  padding: 8px 0 8px;
  margin-bottom: 8px;
  background: var(--paper);
  border-bottom: 3px solid var(--paper-edge);
}

.d-tab {
  font-family: var(--px);
  font-size: 9px;
  padding: 8px 11px 7px;
  color: var(--paper-dim);
  background: var(--paper-2);
  border: 3px solid var(--paper-edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out, color 0.12s;
}

.d-tab:hover { color: var(--paper-ink); transform: translateY(-1px); }
.d-tab.on { color: var(--paper); background: #c23a2f; }

/* Mew's tour highlight on the detail tabs. */
.d-tabs.tour-hl {
  outline: 4px solid #ffd94a;
  outline-offset: 3px;
  border-radius: 4px;
  animation: spot-blink 1s steps(2) infinite;
}

.d-sub { margin: 0 0 12px; font-size: 18px; color: var(--paper-dim); }
.d-sub b { color: var(--paper-ink); }
.d-empty { font-size: 19px; color: var(--paper-dim); padding: 10px 0; }

/* ---------- Evolution tab ---------- */

.evo-step {
  padding: 12px 0 14px;
  border-bottom: 2px dashed color-mix(in srgb, var(--paper-dim) 40%, transparent);
}
.evo-step:last-child { border-bottom: none; }

.evo-step-mons { display: flex; align-items: center; gap: 6px; }

.evo-mon {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px 4px 4px;
  background: none;
  border: 2px solid transparent;
  border-radius: 4px;
  cursor: pointer;
  font-family: var(--vt);
  font-size: 19px;
  color: var(--paper-ink);
}
.evo-mon img { width: 44px; height: 44px; object-fit: contain; image-rendering: pixelated; }
.evo-mon:hover { border-color: color-mix(in srgb, var(--paper-dim) 55%, transparent); }

.evo-step-arrow { font-family: var(--px); font-size: 12px; color: var(--paper-dim); }

.evo-methods { list-style: none; margin: 6px 0 0; padding: 0 0 0 6px; }
.evo-methods li {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 4px 0;
  font-size: 18px;
  line-height: 1.45;
  color: var(--paper-ink);
}

/* The generation/form label on a method that only applies in some games. */
.evo-when {
  flex: 0 0 auto;
  font-family: var(--px);
  font-size: 8px;
  text-transform: uppercase;
  color: var(--paper);
  background: var(--paper-dim);
  padding: 3px 6px 2px;
  border-radius: 3px;
}

/* ---------- Cards tab ---------- */

/* Now that the cards own the whole dialog, the strip runs horizontally in every
   view. That also keeps it out of the page's way: the dialog scrolls
   vertically and the strip scrolls horizontally, so a drag can never be
   ambiguous the way two nested vertical scrollers were.

   Height drives the sizing here, because height is the scarce dimension (the
   dialog is capped at calc(100vh - 64px)). Width follows from the 8:11 aspect
   the CDN serves (600x825, measured). */
.tcg {
  /* Leaves room for the header, caption and thumbnail strip inside the
     dialog's calc(100vh - 64px) cap, down to roughly a 700px-tall window. */
  --card-h: min(56vh, 440px);
  --card-w: calc(var(--card-h) * 8 / 11);
  /* Whatever is left either side of the centre card: it is both the padding
     that lets the first and last card reach the middle, and the width of the
     fade, so the two can never drift apart. */
  --fade: calc((100% - var(--card-w)) / 2);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding-top: 4px;
}

/* A real scroller, no prev/next buttons needed: drag, flick, wheel or swipe,
   and each card snaps to the middle. The mask fades the strip out at each end
   so neighbours trail off instead of being cut in half by an edge. The fade
   has to be painted on the scroller itself. That is why it is a mask and not
   an overlay: an overlay would sit still while the cards move under it, and
   would swallow the pointer. */
.tcg-strip {
  width: 100%;
  /* A little taller than the card so the lifted centre slide is not clipped. */
  flex: 0 0 calc(var(--card-h) + 10px);
  height: calc(var(--card-h) + 10px);
  padding-inline: var(--fade);
  /* Makes the strip the slides' offsetParent, so goTo()'s offsetLeft maths is
     measured against the scroller and not something further up the tree. */
  position: relative;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  display: flex;
  align-items: center;
  gap: 20px;
  /* The solid band is exactly the centre card's box, so the fade only ever
     touches the cards either side of the one being read. Both this and the
     padding above are derived from --fade, so they cannot drift apart. */
  -webkit-mask-image: linear-gradient(to right, transparent 0,
    #000 var(--fade), #000 calc(100% - var(--fade)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0,
    #000 var(--fade), #000 calc(100% - var(--fade)), transparent 100%);
}
.tcg-strip::-webkit-scrollbar { display: none; }
.tcg-strip:focus-visible { outline: 3px solid var(--paper-dim); outline-offset: 3px; }

/* Draggable with a mouse as well as a wheel. Snapping has to come off while a
   drag is in progress: it fights a manual scrollLeft and yanks the strip back
   mid-gesture. bindCardSlider settles onto the nearest card on release. */
.tcg-strip { cursor: grab; }
.tcg-strip.dragging { cursor: grabbing; scroll-snap-type: none; }
.tcg-strip.dragging .tcg-slide { cursor: grabbing; }

/* Claims horizontal pans for the strip. Without it the browser has to guess
   between "pan this strip sideways" and "scroll the dialog behind it", and
   inside a vertically scrolling dialog the parent tends to win, which made
   the cards feel stuck on a phone. */
.tcg-strip, .tcg-film { touch-action: pan-x; }

/* An <img> is natively draggable, so pressing a card and moving started the
   browser's own image drag instead of scrolling the strip. The gesture was
   being stolen before pointermove could act on it, and the half-finished drag
   then swallowed the click that followed. */
.tcg-slide, .tcg-thumb { user-select: none; -webkit-user-select: none; }
.tcg-slide img, .tcg-thumb img {
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: none;
}

.tcg-slide {
  flex: 0 0 var(--card-w);
  width: var(--card-w);
  height: var(--card-h);
  scroll-snap-align: center;
  display: grid;
  place-items: center;
  border-radius: 10px;
  background: color-mix(in srgb, var(--paper-dim) 14%, transparent);
  overflow: hidden;
  cursor: pointer;
  /* Cards away from the middle sit back a little, so the read one leads. */
  transform: scale(0.9);
  opacity: 0.55;
  transition: transform 0.18s ease, opacity 0.18s ease;
}

.tcg-slide.on { transform: scale(1); opacity: 1; cursor: default; }

.tcg-slide img { width: 100%; height: 100%; object-fit: contain; display: block; }

/* A card whose art 404s: keep the frame, say so, drop the broken-image icon. */
.tcg-slide.no-art::after {
  content: "No card image";
  font-family: var(--px);
  font-size: 8px;
  color: var(--paper-dim);
  text-align: center;
  padding: 0 12px;
}
.tcg-slide.no-art img { display: none; }

@media (prefers-reduced-motion: reduce) {
  .tcg-slide { transition: none; }
}

/* In card mode the dex panes are not rendered at all, so the body is a single
   column and the gallery gets the full width of the dialog. */
.detail-body.is-tcg {
  grid-template-columns: minmax(0, 1fr);
  padding: 22px 20px 26px;
  overflow: hidden;
}

/* Centred, with the two arrows flanking the name. Left-aligned put the forward
   arrow at the far right of the pane, directly underneath the dialog's close
   button, where it could not be clicked at all. */
.d-tcg-head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-bottom: 12px;
}

.d-tcg-name { font-family: var(--px); font-size: 13px; color: var(--paper-ink); }
.d-tcg-dex {
  font-family: var(--px);
  font-size: 9px;
  color: var(--paper-dim);
  font-variant-numeric: tabular-nums;
}

/* Stepping between Pokemon, kept up here with the name. */
.tcg-mon-nav {
  font-family: var(--px);
  font-size: 10px;
  line-height: 1;
  padding: 8px 12px;
  flex: 0 0 auto;
}
.d-tcg-head .tcg-mon-nav:first-child { order: -1; }

/* The dialog's own prev/next sit right beside the artwork, where they look
   like card controls, not Pokemon controls, so card mode hides them and uses
   the pair in the header. */
.detail-body.is-tcg ~ .dnav { display: none; }

/* ---------- Cards tab: thumbnail strip ---------- */

.tcg-film {
  --thumb-w: 34px;
  width: 100%;
  /* Same reason as .tcg-strip: without this the thumbnails' offsetParent is the
     dialog, so revealThumb() would compute its scroll target against the wrong
     origin and leave the active thumbnail off screen. */
  position: relative;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 2px 2px 6px;
  scrollbar-width: none;
  cursor: grab;
  /* Do NOT add scroll-behavior: smooth. revealThumb() runs on every scroll
     frame while the cards are being flicked, and a smooth scroller restarts
     its animation on each call, so the strip lags behind the card it is meant
     to be tracking. */
}
.tcg-film::-webkit-scrollbar { display: none; }
.tcg-film.dragging { cursor: grabbing; }

.tcg-thumb {
  flex: 0 0 var(--thumb-w);
  width: var(--thumb-w);
  height: calc(var(--thumb-w) * 11 / 8);
  padding: 0;
  border: 2px solid transparent;
  border-radius: 3px;
  background: color-mix(in srgb, var(--paper-dim) 16%, transparent);
  overflow: hidden;
  cursor: pointer;
  opacity: 0.5;
  transition: opacity 0.12s, border-color 0.12s;
}

.tcg-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.tcg-thumb:hover { opacity: 0.85; }
.tcg-thumb.on { opacity: 1; border-color: #c23a2f; cursor: default; }
/* Same red is already the ACTIVE thumbnail's border, so a red focus ring here
   would be two different meanings in one colour. */
.tcg-thumb:focus-visible { outline: 2px dashed var(--edge); outline-offset: 2px; }
.tcg-thumb.no-art img { display: none; }

@media (prefers-reduced-motion: reduce) {
  .tcg-thumb { transition: none; }
}

.tcg-meta {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  text-align: center;
}

.tcg-name { font-size: 19px; color: var(--paper-ink); }
.tcg-set { font-size: 17px; color: var(--paper-dim); }
.tcg-count {
  font-family: var(--px);
  font-size: 8px;
  color: var(--paper-dim);
  font-variant-numeric: tabular-nums;
}

.move-h {
  margin: 16px 0 8px;
  font-family: var(--px);
  font-size: 9px;
  text-transform: uppercase;
  color: #c23a2f;
}
.move-h span { color: var(--paper-dim); }
.move-h:first-child { margin-top: 4px; }

.move-list, .loc-list { display: flex; flex-direction: column; }

.move {
  display: grid;
  grid-template-columns: 52px 1fr auto auto 34px;
  align-items: center;
  gap: 8px;
  padding: 6px 0;
  border-bottom: 2px dashed var(--paper-2);
  font-size: 18px;
}
.move:last-child { border-bottom: none; }

.move-lead { font-family: var(--px); font-size: 8px; color: var(--paper-dim); }
.move-name { font-size: 19px; }
.move .type-badge { font-size: 7px; padding: 3px 5px 2px; border-width: 2px; }

.move-cat-ic {
  height: 17px;
  width: auto;
  display: block;
  image-rendering: pixelated;
}
.move-pow { font-family: var(--px); font-size: 8px; text-align: right; color: var(--paper-ink); }

.loc {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 10px;
  padding: 7px 0;
  border-bottom: 2px dashed var(--paper-2);
  font-size: 19px;
}
.loc:last-child { border-bottom: none; }
.loc-method { font-family: var(--px); font-size: 7px; color: var(--paper); background: #7a7058; padding: 3px 6px 2px; border-radius: 2px; }
.loc-lv { font-family: var(--px); font-size: 8px; color: var(--paper-dim); }

.entry {
  padding: 12px 0;
  border-bottom: 2px dashed var(--paper-2);
  animation: pop-in 0.25s ease both;
  animation-delay: calc(var(--i, 0) * 25ms);
}

.entry:last-child { border-bottom: none; }

.entry .vers { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 6px; }

.ver-chip {
  font-family: var(--px);
  font-size: 7px;
  text-transform: uppercase;
  padding: 3px 6px 2px;
  border-radius: 2px;
  /* --vc is set per chip from the palette above. A version with no colour
     falls back to the old grey, so a new game still renders. */
  color: #fff;
  background: var(--vc, var(--paper-2));
  border: 2px solid rgba(0, 0, 0, 0.35);
}
/* The pale games: white ink on Yellow or White is unreadable. This list is
   generated from the palette by contrast, so re-check it if a colour moves. */
.ver-chip[data-v="yellow"],
.ver-chip[data-v="gold"],
.ver-chip[data-v="silver"],
.ver-chip[data-v="crystal"],
.ver-chip[data-v="emerald"],
.ver-chip[data-v="firered"],
.ver-chip[data-v="leafgreen"],
.ver-chip[data-v="diamond"],
.ver-chip[data-v="pearl"],
.ver-chip[data-v="platinum"],
.ver-chip[data-v="heartgold"],
.ver-chip[data-v="soulsilver"],
.ver-chip[data-v="white"],
.ver-chip[data-v="white-2"],
.ver-chip[data-v="x"],
.ver-chip[data-v="sun"],
.ver-chip[data-v="ultra-sun"],
.ver-chip[data-v="lets-go-pikachu"],
.ver-chip[data-v="lets-go-eevee"],
.ver-chip[data-v="sword"],
.ver-chip[data-v="the-isle-of-armor"],
.ver-chip[data-v="the-crown-tundra"],
.ver-chip[data-v="brilliant-diamond"],
.ver-chip[data-v="shining-pearl"],
.ver-chip[data-v="legends-arceus"],
.ver-chip[data-v="the-teal-mask"],
.ver-chip[data-v="green-japan"],
.ver-chip[data-v="blue-japan"],
.ver-chip[data-v="legends-z-a"],
.ver-chip[data-v="winds-waves"] {
  color: #16130c;
  border-color: rgba(0, 0, 0, 0.22);
}

/* The filtered game keeps its own colour and gets a ring instead, so the
   highlight doesn't fight the palette. */
.ver-chip.hl {
  box-shadow: 0 0 0 2px var(--paper), 0 0 0 4px #c23a2f;
}

.entry p { margin: 0; font-size: 20px; line-height: 1.35; }

.entry.hl { background: #fff8e6; outline: 3px solid #c23a2f; outline-offset: -3px; padding: 12px 8px; }

/* ---------- Related Entries ---------- */

/* A heading, not another entry: the section reads as a break from the list
   above it, so it gets the tab typeface at label size. */
.rel-head {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 3px solid var(--paper-edge);
  font-family: var(--px);
  font-size: 10px;
  color: var(--paper-ink);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.rel-note { margin: 6px 0 0; font-size: 16px; color: var(--paper-dim); }
/* The source's name wears the version-chip shape so the row reads as one
   strip of tags, but keeps ink colours: it is a link, not a game. */
.rel-src {
  font-family: var(--px);
  font-size: 7px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  padding: 3px 6px 2px;
  color: var(--paper);
  background: var(--paper-ink);
  border: none;
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out;
}
.rel-src:hover { transform: translateY(-1px); background: #c23a2f; }
.rel-src:focus-visible { outline: 2px solid #c23a2f; outline-offset: 2px; }
.rel-hl { font-weight: 700; color: #c23a2f; }

/* ---------- Dialog buttons ---------- */

.dclose, .dnav {
  position: absolute;
  z-index: 5;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  font-family: var(--px);
  font-size: 12px;
  color: var(--ink);
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out, box-shadow 0.12s;
}

.dclose:hover, .dnav:hover { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.35); }
.dclose:active, .dnav:active { transform: translate(1px, 1px); box-shadow: none; }

.dclose { top: 12px; right: 12px; }
.dnav-prev { bottom: 12px; right: 56px; }
.dnav-next { bottom: 12px; right: 12px; }

/* ---------- Footer ---------- */

footer {
  max-width: 1280px;
  margin: 0 auto;
  padding: 8px 24px 28px;
  display: flex;
  justify-content: center;
  gap: 8px;
}

/* Credits live in a popover so the text sits on a solid panel and stays
   readable on any wallpaper; straight on the background it wouldn't be. */
.credits-modal {
  width: min(560px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  padding: 0;
  border: 4px solid var(--edge);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink);
  box-shadow: inset 0 0 0 3px var(--edge-hi), 10px 10px 0 rgba(0, 0, 0, 0.45);
  overflow: hidden;
}
.credits-modal::backdrop { background: rgba(8, 6, 4, 0.65); }
.credits-modal[open] { animation: dialog-pop 0.24s cubic-bezier(0.25, 1.4, 0.5, 1) both; }
.credits-modal[open]::backdrop { animation: fade-in 0.2s ease both; }

.credits-body {
  padding: 28px 26px 26px;
  overflow-y: auto;
  max-height: calc(100vh - 72px);
  overscroll-behavior: contain;
}
.credits-title {
  margin: 0 0 16px;
  font-family: var(--px);
  font-size: 13px;
  text-transform: uppercase;
  border-bottom: 3px solid var(--ink);
  padding-bottom: 10px;
}
.credits-body p {
  font-size: 18px;
  line-height: 1.6;
  color: var(--ink-dim);
  margin: 0 0 14px;
}
.credits-body p:last-child { margin-bottom: 0; }
.credits-dev {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 2px dashed var(--edge);
  color: var(--paper-ink);
}

/* The legal notice opens the dialog and is set apart from the credits proper
   by colour and a rule, not by size: every paragraph in here is body text and
   is set the same. */
.credits-body .credits-legal {
  color: var(--paper-ink);
}
.credits-body .credits-legal:first-child {
  padding-bottom: 4px;
}
.credits-body .credits-legal + :not(.credits-legal) {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 2px dashed var(--edge);
}
.credits-body a { color: var(--ink); text-decoration: underline; }
.credits-body a:hover { color: var(--accent); }

/* ---------- Mew guide ---------- */

#mew {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 300;
  pointer-events: none;
  image-rendering: pixelated;
  background-repeat: no-repeat;
  display: none;
}

.mew-overlay {
  position: fixed;
  inset: 0;
  z-index: 250;
  background: rgba(8, 6, 4, 0.55);
  animation: fade-in 0.25s ease both;
}

.mew-spot {
  position: fixed;
  z-index: 260;
  border: 4px solid #ffd94a;
  border-radius: 4px;
  box-shadow: 0 0 0 4000px rgba(8, 6, 4, 0.55);
  pointer-events: none;
  transition: all 0.35s cubic-bezier(0.3, 1.2, 0.4, 1);
  animation: spot-blink 1s steps(2) infinite;
}

@keyframes spot-blink {
  0%, 100% { border-color: #ffd94a; }
  50% { border-color: #fff; }
}

/* Mystery Dungeon style dialogue: navy gradient panel, peach rounded frame,
   emotion portrait on the left. */
.mew-box {
  position: fixed;
  left: 50%;
  bottom: 28px;
  translate: -50% 0;
  z-index: 270;
  width: min(680px, calc(100vw - 32px));
  display: flex;
  gap: 14px;
  padding: 15px 18px;
  font-family: var(--vt);
  color: #fdfdff;
  background: linear-gradient(#28457a, #14264c);
  border: 4px solid #f7ddac;
  border-radius: 16px;
  box-shadow: 0 0 0 3px #0a1120, 10px 10px 0 rgba(0, 0, 0, 0.45);
  animation: dialog-pop 0.25s cubic-bezier(0.25, 1.4, 0.5, 1) both;
}

.mew-portrait {
  flex: 0 0 auto;
  align-self: center;
  width: 74px;
  height: 74px;
  image-rendering: pixelated;
  background: #0e1830;
  border: 3px solid #f7ddac;
  border-radius: 10px;
}

.mew-box .content { flex: 1 1 auto; min-width: 0; }

.mew-box .who {
  display: inline-block;
  font-family: var(--px);
  font-size: 9px;
  padding: 4px 8px 3px;
  margin-bottom: 7px;
  color: #14264c;
  background: #f7ddac;
  border-radius: 4px;
}

.mew-box .txt {
  min-height: 46px;
  font-size: 22px;
  line-height: 1.3;
}

.mew-box .txt::after {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  margin-left: 6px;
  border-left: 7px solid transparent;
  border-right: 7px solid transparent;
  border-top: 9px solid #f7ddac;
  vertical-align: middle;
  animation: spot-blink 0.8s steps(2) infinite;
  opacity: 0;
}

.mew-box.done .txt::after { opacity: 1; }

.mew-box .row {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 10px;
}

.mew-box .px-btn { font-size: 8px; background: #33518a; color: #fff; border-color: #0a1120; }
.mew-box .px-btn.primary { background: #e8b64a; color: #14264c; }

/* Contextual speech bubble that follows Mew around. */
.mew-bubble {
  position: fixed;
  z-index: 260;
  display: flex;
  gap: 8px;
  align-items: center;
  max-width: 270px;
  padding: 8px 11px;
  font-family: var(--vt);
  font-size: 18px;
  line-height: 1.2;
  color: #fdfdff;
  background: linear-gradient(#28457a, #14264c);
  border: 3px solid #f7ddac;
  border-radius: 12px;
  box-shadow: 0 0 0 2px #0a1120, 4px 4px 0 rgba(0, 0, 0, 0.4);
  pointer-events: none;
  animation: dialog-pop 0.2s cubic-bezier(0.25, 1.4, 0.5, 1) both;
}

.mew-bubble img {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  image-rendering: pixelated;
  background: #0e1830;
  border: 2px solid #f7ddac;
  border-radius: 6px;
}

/* ---------- Add-to-team button (detail) ---------- */

.d-team-add {
  margin-top: 10px;
  font-size: 8px;
  color: var(--accent-ink);
  background: var(--accent);
  border-color: var(--edge);
}

.d-allsprites {
  margin-top: 12px;
  width: 100%;
  font-size: 8px;
}

/* ---------- Sprite archive modal ---------- */

.arch-modal {
  width: min(760px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  padding: 0;
  border: 4px solid var(--edge);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink);
  box-shadow: inset 0 0 0 3px var(--edge-hi), 10px 10px 0 rgba(0, 0, 0, 0.45);
  overflow: hidden;
}
.arch-modal::backdrop { background: rgba(8, 6, 4, 0.65); }
.arch-modal[open] { animation: dialog-pop 0.24s cubic-bezier(0.25, 1.4, 0.5, 1) both; }

.arch-head {
  padding: 16px 20px 12px;
  border-bottom: 3px solid var(--edge);
  background: var(--panel-2);
}
.arch-head h2 {
  margin: 0;
  font-family: var(--px);
  font-size: 13px;
}
.arch-note {
  margin: 6px 0 0;
  font-family: var(--vt);
  font-size: 15px;
  color: var(--ink-dim);
}

.arch-body {
  padding: 14px 20px 20px;
  max-height: calc(100vh - 168px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.arch-gen { margin-bottom: 16px; }
.arch-gen h3 {
  margin: 0 0 8px;
  font-family: var(--px);
  font-size: 9px;
  color: var(--ink-dim);
  border-bottom: 2px solid var(--edge);
  padding-bottom: 6px;
}

.arch-games {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 10px;
}

.arch-game {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 6px 6px;
  background: var(--panel-2);
  border: 2px solid var(--edge);
  border-radius: 3px;
}
.arch-imgs { display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 68px; }
/* Every sprite is its own button into the expanded view. The pair shares the
   cell evenly so a shiny never reads as the smaller of the two, and both shrink
   to fit a narrow column instead of overflowing it. */
.arch-shot {
  display: block;
  flex: 0 1 68px;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  transition: transform 0.12s ease;
}
.arch-shot:hover { transform: scale(1.1); }
.arch-shot:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.arch-img {
  display: block;
  width: 100%;
  height: 68px;
  object-fit: contain;
  image-rendering: auto;
}
.arch-img.px { image-rendering: pixelated; }

/* A pixel sprite bigger than the box it is drawn into (the Z-A Megas are 120 or
   140 against the card's 96) is being shrunk, and nearest-neighbour shrinking
   drops rows and breaks outlines. app.js tags those on load. */
.card img.px-down { image-rendering: auto; }

/* FitThumb() scales these well past their layout box, and an img captures
   clicks across its whole box however transparent the pixels are. A Black/White
   sprite is blown up 2.2x, which put its box over the "All sprites" button and
   ate the click. The button around each image is the real target, so take the
   images out of hit testing. Same reason for the detail thumbs and the hero:
   their handlers sit on .d-thumb and #dHeroBtn. */
.arch-img,
.arch-zoom-img,
.d-thumb img,
.d-hero { pointer-events: none; }

.arch-label {
  font-family: var(--px);
  font-size: 7px;
  text-align: center;
  color: var(--ink-dim);
  line-height: 1.5;
}
.arch-empty {
  font-family: var(--vt);
  font-size: 17px;
  color: var(--ink-dim);
  text-align: center;
  padding: 30px 0;
}

/* The expanded sprite, shown in place of the grid inside the same modal. */
.arch-zoom {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 16px 20px 22px;
}
.arch-zoom[hidden] { display: none; }
.arch-back { align-self: flex-start; font-size: 8px; }
/* One sprite or two: each takes an equal share of the row, capped so a lone
   sprite doesn't sprawl. */
.arch-zoom-row {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: 18px;
  width: 100%;
}
.arch-zoom-shot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  flex: 1 1 0;
  min-width: 0;
  max-width: 320px;
}
/* Square, and sized off its own width, so two sprites side by side on a phone
   don't each reserve a column of empty space below them. */
.arch-zoom-img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  max-height: min(320px, calc(100vh - 300px));
  object-fit: contain;
  image-rendering: pixelated;
}
.arch-zoom-cap {
  font-family: var(--px);
  font-size: 10px;
  line-height: 1.5;
  text-align: center;
}
/* Sits outside .arch-zoom-cap, so it has to name the pixel font itself rather
   than inherit it: at 7px the body font it was falling back to is unreadable. */
.arch-zoom-tag {
  font-family: var(--px);
  font-size: 8px;
  text-transform: uppercase;
  color: var(--ink-dim);
}

@media (max-width: 720px) {
  .arch-zoom { padding: 12px 16px 18px; }
  .arch-zoom-row { gap: 10px; }
  .arch-zoom-cap { font-size: 9px; }
}

/* ---------- Team dock ---------- */

.team-dock {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 180;
  background: var(--panel-2);
  border-top: 3px solid var(--edge);
  box-shadow: 0 -3px 0 rgba(0, 0, 0, 0.3);
}

.team-dock[hidden] { display: none; }

.team-dock-inner {
  max-width: 1280px;
  margin: 0 auto;
  padding: 10px 24px;
  display: flex;
  align-items: center;
  gap: 14px;
}

.team-label {
  font-family: var(--px);
  font-size: 10px;
  color: var(--ink-dim);
  flex: 0 0 auto;
}

.team-slots { display: flex; gap: 8px; flex: 1 1 auto; }

.team-slot {
  position: relative;
  width: 62px;
  height: 62px;
  display: grid;
  place-items: center;
  background: var(--panel);
  border: 3px dashed var(--edge);
  border-radius: 4px;
  transition: transform 0.12s, box-shadow 0.12s;
}

.team-slot.filled { border-style: solid; box-shadow: inset 2px 2px 0 var(--edge-hi); }
.team-slot.over { transform: translateY(-3px); box-shadow: 0 0 0 3px var(--accent); }
/* A filled slot can be dragged onto another to swap the two. Fading the one
   being carried is what tells you the gesture took: .over marks the target,
   and without this nothing marks the source. */
.team-slot.filled { cursor: grab; }
.team-slot.dragging { opacity: 0.4; cursor: grabbing; }

.team-slot img { width: 54px; height: 54px; object-fit: contain; image-rendering: auto; }
.slot-plus { font-family: var(--px); font-size: 16px; color: var(--ink-dim); }

.slot-x {
  position: absolute;
  top: -8px;
  right: -8px;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  /* The X is drawn, not typed. As Press Start 2P's "\00d7" its ink sat 0.56px
     above and left of its own advance box, which is enough to read as
     off-centre inside a 20px circle. A symmetric 8x8 icon has no such lean.
     padding:0 resets the UA's 1px 6px, which left a 4px content box. */
  padding: 0;
  color: #fff;
  background: #d3425f;
  border: 2px solid var(--edge);
  border-radius: 50%;
  cursor: pointer;
}
.slot-x svg { width: 8px; height: 8px; display: block; fill: currentColor; }

.team-dock-actions { display: flex; gap: 8px; flex: 0 0 auto; }
.card-btn { color: var(--accent-ink); background: var(--accent); }
.card-btn:disabled { opacity: 0.5; cursor: not-allowed; filter: grayscale(0.6); }
.team-clear { color: #fff; background: #d3425f; border-color: #47101d; }

/* ---------- Trainer card editor ---------- */

.card-modal {
  width: min(1040px, calc(100vw - 32px));
  /* Dvh, not vh: on a phone or a short window with browser chrome that hides
     on scroll, vh is the TALLEST the viewport ever gets, so the modal is sized
     past the bottom of the screen it is actually being shown on. */
  max-height: calc(100dvh - 48px);
  padding: 0;
  border: 4px solid var(--edge);
  border-radius: 6px;
  background: var(--panel);
  color: var(--ink);
  box-shadow: inset 0 0 0 3px var(--edge-hi), 10px 10px 0 rgba(0, 0, 0, 0.45);
  overflow: hidden;
}
.card-modal::backdrop { background: rgba(8, 6, 4, 0.66); }

.card-modal-body {
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  max-height: calc(100dvh - 48px);
  /* TWO separate things stopped the column from scrolling, and it needs both.
     1. The single implicit row was sized `auto`, i.e. to its tallest item, so
        it grew past the container's max-height instead of being capped by it.
        minmax(0, 1fr) caps the row at the space actually available.
     2. Grid items default to min-height:auto and refuse to shrink below their
        content, which would keep the column at full height inside that capped
        row. min-height:0 on the items (below) releases that.
     Without both, the controls column stood at its full ~1170px, the modal
     clipped it at overflow:hidden, and Export PNG, its last child: sat
     below the cut with no scrollbar to reach it. Nothing looked broken; the
     button was not there on any window shorter than about 1250px. */
  grid-template-rows: minmax(0, 1fr);
  min-height: 0;
}

.card-canvas-wrap {
  padding: 22px;
  display: grid;
  place-items: center;
  background: var(--panel-2);
  min-height: 0;
}
/* The canvas is 1200x750, so its height follows the column width. Capping it
   against the viewport as well stops a short-and-wide window from handing the
   preview a row taller than the screen. */
#cardCanvas { width: 100%; height: auto; max-height: calc(100dvh - 92px); object-fit: contain; image-rendering: auto; }

.card-controls {
  padding: 22px 22px 20px;
  border-left: 3px solid var(--edge);
  overflow: auto;
  display: flex;
  flex-direction: column;
  gap: 18px;
  overscroll-behavior: contain;
  min-height: 0;
}

.cc-field { display: flex; flex-direction: column; gap: 8px; }
.cc-field > span {
  font-family: var(--px);
  font-size: 9px;
  text-transform: uppercase;
  color: var(--ink-dim);
}

#cardName {
  font-family: var(--vt);
  font-size: 22px;
  padding: 8px 12px;
  color: var(--ink);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  outline: none;
  text-transform: uppercase;
}
#cardName:focus { box-shadow: inset 0 0 0 2px var(--accent); }

.cc-row3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
.cc-row3 .cc-field > span { font-size: 8px; }

.cc-input {
  width: 100%;
  font-family: var(--vt);
  font-size: 20px;
  padding: 8px 10px;
  color: var(--ink);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  outline: none;
}
.cc-input:focus { box-shadow: inset 0 0 0 2px var(--accent); }

.cc-select {
  appearance: none;
  -webkit-appearance: none;
  font-family: var(--vt);
  font-size: 19px;
  padding: 8px 30px 8px 12px;
  color: var(--ink);
  background: var(--panel-2)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7'%3E%3Cpath d='M0 0h12L6 7z' fill='%23888'/%3E%3C/svg%3E")
    no-repeat right 12px center;
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  outline: none;
}
.cc-select:focus { box-shadow: inset 0 0 0 2px var(--accent); }

/* One row per party member: position, sprite (tap for shiny), nickname, order.
   A row per Pokemon, not the old single strip of sprites, because a
   nickname field needs a line of its own to be typeable. Listed top to bottom
   in the order they are drawn left to right on the card. */
.party-list { display: flex; flex-direction: column; gap: 6px; }
.party-row {
  display: grid;
  /* Number, order arrows, sprite, nickname. minmax(0, 1fr) on the nickname,
     not 1fr: a track sized to an input's default width refuses to shrink and
     would push the row past the edge of the controls column at narrow widths. */
  grid-template-columns: 14px auto auto minmax(0, 1fr);
  align-items: center;
  gap: 8px;
}
.party-num {
  font-family: var(--px);
  font-size: 9px;
  color: var(--ink-dim);
  text-align: right;
}
.party-nick {
  width: 100%;
  min-width: 0;
  font-family: var(--vt);
  font-size: 17px;
  padding: 5px 8px;
  color: var(--ink);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  outline: none;
}
.party-nick:focus { box-shadow: inset 0 0 0 2px var(--accent); }
.party-nick::placeholder { color: var(--ink-dim); opacity: 0.75; }

.party-move { display: flex; flex-direction: column; gap: 2px; }
.party-arrow {
  width: 22px;
  height: 15px;
  display: grid;
  place-items: center;
  font-size: 8px;
  line-height: 1;
  color: var(--ink);
  background: var(--panel-2);
  border: 2px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
}
.party-arrow:hover:not(:disabled) { background: var(--accent); color: var(--accent-ink); }
.party-arrow:disabled { opacity: 0.3; cursor: not-allowed; }

.party-opt {
  position: relative;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  padding: 2px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s;
}
.party-opt:hover { transform: translateY(-2px); }
.party-opt img { width: 36px; height: 36px; object-fit: contain; }
.party-opt .party-star {
  position: absolute;
  top: -7px;
  right: -7px;
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  font-size: 10px;
  color: #7a7058;
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 50%;
}
/* The badge is 18px wide with 2px borders, so the mark has 14px of room.
   Smaller than the header toggle's 13px to keep it off the ring. */
.party-opt .party-star .ico-shiny { width: 10px; height: 10px; }
.party-opt.on { border-color: #f5c518; box-shadow: inset 0 0 0 2px #f5c518; }
.party-opt.on .party-star { color: #14264c; background: #f5c518; border-color: #a8850a; }

.cc-swatches { display: flex; gap: 4px; flex: 0 0 auto; }
.cc-swatch {
  width: 24px;
  height: 24px;
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s;
}
.cc-swatch:hover { transform: translateY(-2px); }
.cc-swatch.on { box-shadow: 0 0 0 2px var(--accent-ink), 0 0 0 5px var(--accent); }

/* Presets and the custom picker on one line. Eight 24px swatches, 4px apart,
   plus the label and its square come to ~312px against the ~316px the controls
   column gives at the narrowest desktop width, so the swatches are
   24px here and not the 26px they were when they had the row to themselves.
   Wrap is a safety valve: below that width it drops to two lines instead of
   pushing the picker out through the side of the column. */
.cc-color-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.cc-custom {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  /* Same face and size as the section labels above it, not the VT323 body
     text it used to use. */
  font-family: var(--px);
  font-size: 9px;
  text-transform: uppercase;
  color: var(--ink-dim);
}
/* Square, and the same square as a preset swatch beside it. */
.cc-custom input { width: 24px; height: 24px; padding: 0; border: 3px solid var(--edge); border-radius: 3px; background: none; cursor: pointer; }

/* Generation tabs over the trainer grid. Numerals, not "Gen 1", so all
   five fit one row at the width the controls column actually gets. */
.gen-tabs { display: flex; gap: 4px; }
.gen-tab {
  flex: 1 1 0;
  min-width: 0;
  padding: 6px 2px;
  font-family: var(--px);
  font-size: 9px;
  color: var(--ink);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
}
.gen-tab:hover { background: rgba(0, 0, 0, 0.08); }
.gen-tab.on { color: var(--accent-ink); background: var(--accent); }

.trainer-pick {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
  gap: 6px;
  max-height: 240px;
  overflow: auto;
  padding: 8px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
}
.trainer-opt {
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  padding: 2px;
  background: none;
  border: 2px solid transparent;
  border-radius: 3px;
  cursor: pointer;
}
.trainer-opt img { width: 90%; height: 90%; object-fit: contain; image-rendering: pixelated; }
/* Gen 1, 2, 4 and 5 draw one cell out of a shared sheet, so the sprite is a
   background instead of an <img>. Sized inline by team.js, which is the only
   place that knows the sheet's cell dimensions. */
.trainer-cell {
  display: block;
  background-repeat: no-repeat;
  image-rendering: pixelated;
}
.trainer-opt:hover { background: rgba(0, 0, 0, 0.08); }
.trainer-opt.on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 18%, transparent); }

/* Pinned to the foot of the scrolling column. margin-top:auto alone only holds
   it at the bottom of the CONTENT, so on a short window it scrolled away with
   everything else, and it is the one control the editor exists to reach.
   The negative margins and matching padding let the panel background run to
   the column's edges so rows pass behind it, not beside it. */
.cc-actions {
  margin-top: auto;
  position: sticky;
  bottom: -20px;
  margin-inline: -22px;
  margin-bottom: -20px;
  padding: 12px 22px 20px;
  background: var(--panel);
  border-top: 3px solid var(--edge);
}
.card-export {
  width: 100%;
  font-size: 11px;
  padding: 12px;
  color: var(--accent-ink);
  background: var(--accent);
  border-color: var(--edge);
}

/* ---------- Tier list builder ---------- */

/* Full-page, stream-friendly view: takes over the whole viewport (no dex behind
   it), with its own top bar. Sits over everything else, including the team dock. */
.tier-page {
  position: fixed;
  inset: 0;
  z-index: 190;
  display: flex;
  flex-direction: column;
  background: var(--field) var(--wall) repeat top left;
  background-size: 96px auto;
  color: var(--ink);
  animation: fade-in 0.18s ease both;
}
.tier-page[hidden] { display: none; }
body.tier-open { overflow: hidden; }

.tier-topbar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 20px;
  background: var(--panel-2);
  border-bottom: 3px solid var(--edge);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
}
/* No .tier-topbar .brand h1 size override: it used to be dead weight (the h1
   held an <img>), and now it would outrank .brand .brand-logo and shrink this
   one title out of step with every other page. */
.tier-topbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
/* Adding the collapse arrow took this row past what a 1280px laptop has spare.
   The arrow is last in the row, so the arrow is what wrapped, landing on a
   line of its own under Donate. Shortening Back reclaimed the width; this is the
   backstop for anything added later.

   The brand gives up the space instead of the buttons: the tagline can
   ellipsis without anything being lost, whereas a button that wraps is a button
   in the wrong place. Both min-widths are needed. A flex item's automatic
   minimum size is its content, so without them the stack refuses to shrink at
   all. The title itself is left alone; it is the one thing here worth a wrap. */
.tier-topbar .brand { min-width: 0; }
.tier-topbar .brand-stack { min-width: 0; }
.tier-topbar .brand-tagline {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The dark toggle rides into the builder bar with the wallpaper button. It is
   .hide-mobile in the header, and the builder is desktop-only anyway, so it
   needs its square shape restated here where that class is not filtering it. */
.tier-topbar-actions .dark-toggle { display: inline-flex; }
.tl-export { color: var(--accent-ink); background: var(--accent); }

.tier-toolbar {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 8px;
  padding: 12px 20px;
  background: var(--panel-2);
  border-bottom: 3px solid var(--edge);
}
.tier-toolbar .select { flex: 0 1 220px; }
.tl-search-wrap { flex: 1 1 260px; min-width: 200px; }

/* Live search-add results, styled like the "Other" category popover. */
.tl-results {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 60;
  max-height: 260px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 6px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 6px 6px 0 rgba(0, 0, 0, 0.4);
}
.tl-results[hidden] { display: none; }

.tl-result {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  font-family: var(--vt);
  font-size: 17px;
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  text-align: left;
}
.tl-result:hover { background: var(--accent); color: var(--accent-ink); }
.tl-result img { width: 26px; height: 26px; object-fit: contain; }
.tl-result .tl-r-num { font-family: var(--px); font-size: 7px; color: var(--ink-dim); }
.tl-result:hover .tl-r-num { color: var(--accent-ink); }
.tl-result.empty { cursor: default; color: var(--ink-dim); justify-content: center; }
.tl-result.empty:hover { background: var(--panel); color: var(--ink-dim); }

/* The wallpaper lives on .tier-page; this lays out the rows + pool to fill the
   full remaining height (the pool flex-grows into whatever's left, so no dead
   space below a fixed-height pool). Tier rows keep their
   natural height but cap at 55% with their own scrollbar as a safety valve for
   very full tiers, so the pool always keeps meaningful room. */
.tier-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  padding: 16px 20px 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Flex:0 0 auto (not shrinkable): a shrinkable flex item with overflow:auto
   defaults its min-size to 0, so it would get crushed toward nothing by
   .tier-pool's flex-grow instead of keeping its natural content height. */
.tier-rows {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 0 0 auto;
  max-height: 55%;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.tier-row {
  display: flex;
  align-items: stretch;
  gap: 0;
  flex: 0 0 auto;
  min-height: 100px;
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 4px 4px 0 rgba(0, 0, 0, 0.3);
  overflow: hidden;
}

.tier-label {
  flex: 0 0 74px;
  display: grid;
  place-items: center;
  font-family: var(--px);
  font-size: 22px;
  color: #2a2113;
  border-right: 3px solid var(--edge);
  text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.3);
}
.tier-label.tier-S { background: linear-gradient(#ffe485, #f5c518); }
.tier-label.tier-A { background: linear-gradient(#ff9c8a, #e8543f); color: #fff; text-shadow: 1px 1px 0 rgba(0,0,0,.35); }
.tier-label.tier-B { background: linear-gradient(#ffc978, #f0a030); }
.tier-label.tier-C { background: linear-gradient(#c3e896, #7ac74c); }
.tier-label.tier-D { background: linear-gradient(#a6c6ff, #6390f0); color: #fff; text-shadow: 1px 1px 0 rgba(0,0,0,.35); }
.tier-label.tier-F { background: linear-gradient(#c9b6d9, #8a6ba3); color: #fff; text-shadow: 1px 1px 0 rgba(0,0,0,.35); }

.tier-cell {
  flex: 1 1 auto;
  min-height: 94px;
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 8px;
  padding: 8px;
  transition: background 0.12s;
}
.tier-cell.over { background: color-mix(in srgb, var(--accent) 18%, transparent); }

/* Upload sits beside the Pre-Built Pools select and should read as its twin,
   so it takes the select's typography instead of the pixel font .px-btn gives
   every other button. Padding matches the select's minus the arrow gutter, so
   the two are the same height. */
.px-btn.tl-upload {
  font-family: var(--vt);
  font-size: var(--field-fs);
  font-weight: normal;
  letter-spacing: normal;
  padding: 7px 12px 6px;
}

/* Result line for a CSV upload: how many landed, and what didn't match.
   Sits under the toolbar and clears itself after a few seconds. */
.tl-notice {
  flex: 0 0 auto;
  /* Inset to match .tier-scroll's 20px so it lines up with the tier rows
     under it, and no bottom margin: .tier-scroll's own 16px top padding is
     the gap below, which balances the 16px above. */
  margin: 16px 20px 0;
  padding: 8px 10px 7px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--paper-ink);
  background: var(--paper);
  border: 2px solid var(--edge);
  border-radius: 3px;
}
.tl-notice[hidden] { display: none; }
.tl-notice b { font-weight: normal; color: var(--accent); }
.tl-notice .tl-miss { color: var(--paper-dim); overflow-wrap: anywhere; }

/* Flex:0 1 auto (not grow): the pool sizes to its own content (so a small
   pool like Gigantamax's 34 stays compact) but can still SHRINK if that
   content is taller than the space left in .tier-scroll, at which point
   .tier-pool-grid's own overflow-y:auto takes over as a scrollbar (e.g. the
   full 1113-species dex). min-height is just a floor for the empty state. */
.tier-pool {
  flex: 0 1 auto;
  min-height: 140px;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 4px 4px 0 rgba(0, 0, 0, 0.3);
  overflow: hidden;
}
.tier-pool-head {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 10px 12px 8px;
  font-family: var(--px);
  font-size: 9px;
  text-transform: uppercase;
  color: var(--ink-dim);
  border-bottom: 2px solid var(--edge);
}
.tl-count { color: var(--ink); }
.tier-pool-grid {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 8px;
  padding: 12px;
  transition: background 0.12s;
  overscroll-behavior: contain;
}
.tier-pool-grid.over { background: color-mix(in srgb, var(--accent) 14%, transparent); }
.tl-empty {
  width: 100%;
  padding: 16px 4px;
  text-align: center;
  font-family: var(--vt);
  font-size: 16px;
  color: var(--ink-dim);
}

/* A single draggable Pokemon tile: just the PNG, no card/border. Hover grows it
   and selection glows the sprite's own silhouette (drop-shadow hugs the alpha,
   so there's no square outline). */
.tl-sprite {
  position: relative;
  width: 76px;
  height: 76px;
  display: grid;
  place-items: center;
  background: none;
  border: none;
  cursor: grab;
  transition: transform 0.1s;
}
.tl-sprite:active { cursor: grabbing; }
.tl-sprite img {
  width: 70px;
  height: 70px;
  object-fit: contain;
  transition: filter 0.1s;
}
.tl-sprite:hover { transform: scale(1.12); z-index: 1; }

/* Bigger in the pool than in the tiers. The pool is where you're reading
   sprites to pick them out, so it gets the room; a filled tier row wants to
   fit as many as it can. */
.tier-pool-grid .tl-sprite { width: 92px; height: 92px; }
.tier-pool-grid .tl-sprite img { width: 86px; height: 86px; }
.tl-sprite.selected img {
  filter: drop-shadow(0 0 2px var(--accent)) drop-shadow(0 0 2px var(--accent)) drop-shadow(0 0 1px var(--accent));
}
.tl-sprite .slot-x {
  opacity: 0;
  transition: opacity 0.1s;
}
.tl-sprite:hover .slot-x { opacity: 1; }

/* Drag-to-reorder insertion point: a bar in the gap on the side of the tile
   the dragged item would land on. */
.tl-sprite.drop-before::before,
.tl-sprite.drop-after::after {
  content: "";
  position: absolute;
  top: 4px;
  bottom: 4px;
  width: 3px;
  border-radius: 2px;
  background: var(--accent);
}
.tl-sprite.drop-before::before { left: -6px; }
.tl-sprite.drop-after::after { right: -6px; }

/* ---------- Dream World ---------- */

/* Dream World is its OWN page (dreamworld.html), not an overlay on the dex.
   As a normal document it gets normal scrolling, a normal sticky header and
   nothing rendered behind it, so there is no second page to bleed past the
   screen edges on iOS, the class of bug the overlay version kept hitting.
   Layout here mirrors the main dex, which already works.

   The mode class lives on <html> (stamped pre-paint by an inline script in
   dreamworld.html so the palette never flashes), hence .dw-light / .dw-dark
   and not body.*. */

.dw-body {
  min-height: 100vh;
  color: var(--ink);
}

/* The sky is a fixed backdrop, not a scrolling background: at ~150k px of
   grid, background-attachment:fixed misbehaves on iOS and a scrolling image
   would stretch absurdly. body's background-color matches the sky so any
   overscroll past the artwork stays in-palette instead of flashing white. */
.dw-body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
}
.dw-light .dw-body { background-color: #b0479f; }
.dw-light .dw-body::before { background-image: url("assets/dreamworld/pink.png"); }
.dw-dark .dw-body { background-color: #061544; }
.dw-dark .dw-body::before { background-image: url("assets/dreamworld/night.png"); }

/* Header + generation tabs pin together as one unit, mirroring the main dex's
   .topbar. The safe-area padding sits on the wrapper so its opaque background
   fills the iOS status-bar / notch zone and the menu colour runs as one
   continuous block to the very top of the screen. */
.dw-topbar-wrap {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--panel-2);
  border-bottom: 3px solid var(--edge);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
  padding-top: env(safe-area-inset-top, 0px);
}

/* ---------- Dream World palettes (light: Musharna / dark: Darkrai) ---------- */

.dw-light .dw-body {
  --field: #e7c4e6;
  --panel: #fbeef8;
  --panel-2: #f4dcef;
  --edge: #5a3a58;
  --edge-hi: #fffdfb;
  --ink: #3a2438;
  --ink-dim: #8a6288;
  --paper: #fdf3fb;
  --paper-2: #f6e6f3;
  --paper-ink: #3a2438;
  --paper-dim: #8a6288;
  --accent: #e879bb;
  --accent-ink: #fff;
}
.dw-dark .dw-body {
  --field: #1a2244;
  --panel: #222d52;
  --panel-2: #2b3865;
  --edge: #0a0f24;
  --edge-hi: #4a5a8e;
  --ink: #e9eefc;
  --ink-dim: #a6b4dc;
  --paper: #1d2749;
  --paper-2: #26325a;
  --paper-ink: #e9eefc;
  --paper-dim: #a4b2da;
  --accent: #8272ff;
  --accent-ink: #fff;
}

/* The wordmark inherits var(--ink), which this block already flips, so the
   night palette needs nothing further for the title. */

/* ---------- Porygon's Cache ----------
   Dream World's sibling: same layout, same sticky header, same two-mode
   toggle, different subject. Dream World holds the leaks; this holds official
   sprites that never reached a main series game. Everything structural is
   shared with .beta-* and only the shell and palette are restated. */

.pc-body {
  min-height: 100vh;
  color: var(--ink);
}
/* Same fixed backdrop as .dw-body, and for the same reasons; see the note
   there about background-attachment and 150k px of grid. */
.pc-body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  background-position: center bottom;
  background-size: cover;
  background-repeat: no-repeat;
  /* The city art is pixel work at 576x324, so let it scale up hard-edged
     instead of being smoothed into mush. */
  image-rendering: pixelated;
}
.pc-light .pc-body { background-color: #7fa6d8; }
.pc-light .pc-body::before { background-image: url("assets/porygon/day.png"); }
.pc-dark .pc-body { background-color: #0b1030; }
.pc-dark .pc-body::before { background-image: url("assets/porygon/night.png"); }

.pc-topbar-wrap {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--panel-2);
  border-bottom: 3px solid var(--edge);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
  padding-top: env(safe-area-inset-top, 0px);
}

/* Day is Porygon-Z: the blues and cyans of a screen, warmed slightly so it
   doesn't feel as cold as the night mode. */
.pc-light .pc-body {
  --field: #c3d4ee;
  --panel: #eef3fc;
  --panel-2: #dbe5f7;
  --edge: #2f3f63;
  --edge-hi: #ffffff;
  --ink: #1e2b47;
  --ink-dim: #61729a;
  --paper: #f4f8ff;
  --paper-2: #e4ecf9;
  --paper-ink: #1e2b47;
  --paper-dim: #61729a;
  --accent: #2f8fd8;
  --accent-ink: #fff;
}
/* Night is Iron Valiant: the same city with the lights on, so the accent
   shifts to the magenta of its blades. */
.pc-dark .pc-body {
  --field: #141a38;
  --panel: #1e2647;
  --panel-2: #27305a;
  --edge: #080b1e;
  --edge-hi: #47548a;
  --ink: #e6ecfc;
  --ink-dim: #9fb0dc;
  --paper: #1a2140;
  --paper-2: #232c52;
  --paper-ink: #e6ecfc;
  --paper-dim: #9fb0dc;
  --accent: #e05fa8;
  --accent-ink: #fff;
}

/* Same as Dream World: the title rides var(--ink) and needs no override. */

/* ---------- World Archives ----------
   The third sibling. Same .beta-* structure, spacing and wordmark as Dream
   World and Porygon's Cache; only the shell and palette are restated here,
   exactly as those two do.

   The one real difference is that it runs THREE modes instead of a pair, one
   per time of day, each with its own companion: day/Sigilyph, dusk/Runerigus,
   night/Relicanth. Nothing structural changes for that; there are three
   palettes below instead of two, and world.js cycles them where the siblings
   toggle. */

.wa-body {
  min-height: 100vh;
  color: var(--ink);
}
/* Same fixed backdrop as .dw-body and .pc-body, for the same reasons; see
   the note on .dw-body about background-attachment and 150k px of grid. */
.wa-body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  background-position: center bottom;
  background-size: cover;
  background-repeat: no-repeat;
  /* Pixel art: hold the hard edges when it scales past 1:1 on a wide
     display, or the browser smooths them. */
  image-rendering: pixelated;
}
/* Each background-color is sampled from its own art's sky, so the moment
   before the image paints is the right colour and not a flash of white. */
.wa-day .wa-body { background-color: #37b6e8; }
.wa-day .wa-body::before { background-image: url("assets/world/day.png"); }
.wa-dusk .wa-body { background-color: #6a5a8e; }
.wa-dusk .wa-body::before { background-image: url("assets/world/dusk.png"); }
.wa-night .wa-body { background-color: #0e1c3d; }
.wa-night .wa-body::before { background-image: url("assets/world/night.png"); }

.wa-topbar-wrap {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--panel-2);
  border-bottom: 3px solid var(--edge);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
  padding-top: env(safe-area-inset-top, 0px);
}

/* Day is Sigilyph: the greens of a forest in full sun, accented with the
   yellow of its wings. */
.wa-day .wa-body {
  --field: #cbe7c0;
  --panel: #eef8e6;
  --panel-2: #dcefd0;
  --edge: #2c4a2a;
  --edge-hi: #ffffff;
  --ink: #1d3320;
  --ink-dim: #5a7a54;
  --paper: #f4fbee;
  --paper-2: #e6f4dc;
  --paper-ink: #1d3320;
  --paper-dim: #5a7a54;
  --accent: #3f9e46;
  --accent-ink: #fff;
}
/* Dusk is Runerigus: the same forest under a sunset, so the accent takes the
   burnt orange that edges the clouds. */
.wa-dusk .wa-body {
  --field: #4a3f63;
  --panel: #f3ecdf;
  --panel-2: #e5d9c6;
  --edge: #3a2b46;
  --edge-hi: #fff3d0;
  --ink: #33243d;
  --ink-dim: #75617f;
  --paper: #f7f1e5;
  --paper-2: #ebe0cd;
  --paper-ink: #33243d;
  --paper-dim: #75617f;
  --accent: #e0862f;
  --accent-ink: #fff;
}
/* Night is Relicanth: the deep blues of the sea floor it never left, lit only
   by the one lamp in the art. */
.wa-night .wa-body {
  --field: #14203f;
  --panel: #1d2a4d;
  --panel-2: #26355f;
  --edge: #070d20;
  --edge-hi: #4a5c90;
  --ink: #e4ecfb;
  --ink-dim: #9cadd6;
  --paper: #1a2545;
  --paper-2: #223055;
  --paper-ink: #e4ecfb;
  --paper-dim: #9cadd6;
  --accent: #3fb0c9;
  --accent-ink: #fff;
}

/* The mode toggle reuses Dream World's, so it needs the same swatch sizing. */
.wa-mode-btn { display: flex; align-items: center; gap: 7px; }
.px-btn.wa-mode-btn { padding: 6px 12px; }
.wa-mode-swatch {
  width: 15px;
  height: 15px;
  border: 2px solid var(--edge);
  border-radius: 50%;
  background-size: cover;
  background-position: center top;
  image-rendering: pixelated;
}

/* The mode toggle reuses Dream World's, so it needs the same swatch sizing. */
.pc-mode-btn { display: flex; align-items: center; gap: 7px; }
.px-btn.pc-mode-btn { padding: 6px 12px; }
.pc-mode-swatch {
  width: 15px;
  height: 15px;
  border: 2px solid var(--edge);
  border-radius: 50%;
  background-size: cover;
  background-position: center top;
  image-rendering: pixelated;
}

/* Mode toggle: the current buddy's portrait + name, like the wallpaper button. */
/* Companion dialogue mute. Square, sized to the 33px the sibling .px-btns
   resolve to so it sits level in the row. Needs the .px-btn double-class
   specificity to zero the padding, same as the sprite and wallpaper buttons.
   A struck-through bubble means muted; .on also gives it the usual gold. */
.px-btn.dlg-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 33px;
  height: 33px;
  padding: 0;
  flex: 0 0 auto;
}
/* Carries the wallpaper's accent, the same as the dark-mode toggle beside it,
   so the two square icon buttons read as one set. 18x16 holds the bubble's
   1.11 ratio at the 18px the sun and moon use. */
.dlg-toggle svg { width: 18px; height: 16px; display: block; fill: var(--accent); }

/* Two icons, one shown. The same pattern as the dark toggle's sun and moon,
   and for the same reason. This button used to be one bubble that filled solid
   when muted, plus a diagonal bar struck through it. Filling a shape is a weak
   signal: it reads as "selected", and that is what every other toggle on the site
   means by it, so the button looked switched ON at exactly the moment it had
   switched the dialogue OFF. An X inside the bubble says muted without relying
   on the viewer having seen the other state to compare against.

   .on means MUTED here (set by each page's paint()), so it is the X that shows
   under it and the plain bubble the rest of the time. */
.dlg-toggle .dlg-mute { display: none; }
.dlg-toggle.on .dlg-speak { display: none; }
.dlg-toggle.on .dlg-mute { display: block; }

/* And the button itself must stop filling. `.px-btn.on` paints every toggled
   button with `background: var(--accent)`, and this icon is `fill:
   var(--accent)`, so the moment it went muted the bubble became accent on
   accent and disappeared, leaving one solid block of colour. Swapping the icon
   underneath changed nothing anyone could see.

   The dark toggle never hits this because it has no `.on` at all: it keys off
   `.dex-dark` on <html>. This button's `.on` is set by seven page scripts, so
   the fill is cancelled here instead. Values restated from `.px-btn` rather
   than `revert`, which would go to the UA sheet, not to the base rule.

   The state now lives ENTIRELY in the glyph, which is the point. A filled
   square reads as "selected" everywhere else on this site, so on a mute button
   it said the opposite of what was true. */
.px-btn.dlg-toggle.on {
  color: var(--ink);
  background: var(--panel);
  box-shadow: none;
}

.dw-mode-btn { display: flex; align-items: center; gap: 7px; }
.px-btn.dw-mode-btn { padding: 6px 12px; }
.dw-mode-swatch {
  /* 15px, where the wallpaper swatch is 18px: with the button's 6px padding
     and 3px borders that lands it at the same 33px the sibling .px-btns
     resolve to. The wallpaper button on the dex solves its identical problem
     the same way. */
  width: 15px;
  height: 15px;
  border: 2px solid var(--edge);
  border-radius: 50%;
  background-size: cover;
  background-position: center top;
  image-rendering: pixelated;
}

.beta-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* Same vertical rhythm as the dex header's brand row. Horizontal stays at
     20px, which is this page's own gutter (its main uses 20px, not 24px). */
  padding: 22px 20px;
}
/* No .beta-topbar .brand rule: it used to restate .brand's flex/gap verbatim,
   which meant the lockup spacing had two homes and only one of them got
   updated. It inherits the shared rule now. */
/* No per-page logo sizing any more: every wordmark is Basset at one size, so
   the letters match across pages by construction. No more rescaling
   differently-cropped SVGs to agree. */
.beta-topbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }

.beta-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 0 20px 12px;
}

/* Desktop only: the collection tabs sit in their own band, divided from the
   title row above. This existed before Dream World became a standalone page
   (the tabs were a separate sticky bar with their own border) and was lost in
   that conversion, leaving the two groups as one undifferentiated block.

   Scoped to match the Pokedex header, and because on a phone the header is
   compact enough that a second tone plus a divider looks like a mismatched
   seam, not two zones. Mobile keeps the seamless header it had. */
@media (min-width: 721px) {
  .beta-toolbar {
    padding: 18px 20px;
    background: var(--panel);
    border-top: 3px solid var(--edge);
  }
}
.beta-tabs { display: flex; gap: 6px; flex-wrap: wrap; }

/* Mobile-only "Dreams" picker: on desktop the three subcategories stay visible
   as the tab strip, so this stays hidden. Styled after the dex's Other menu. */
.dreams-wrap { display: none; position: relative; }
.dreams-pop[hidden] { display: none; }
.dreams-pop {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 120;
  width: max(220px, 100%);
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 6px 6px 0 rgba(0, 0, 0, 0.4);
  animation: pop-in 0.18s ease-out both;
}
.dreams-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 10px 8px;
  font-family: var(--px);
  /* The dense size: the names are long ("Spaceworld Demo Gen 1") and this keeps
     them on one line at phone widths, with the line-height stopping any wrap
     from colliding. */
  font-size: var(--btn-fs-sm);
  line-height: 1.5;
  text-align: left;
  /* Full ink: a menu row is something you press; --ink-dim is reserved for a
     toggle that is currently off. */
  color: var(--ink);
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: color 0.12s, background 0.12s;
}
.dreams-opt:hover { color: var(--ink); }
.dreams-opt.on { color: var(--accent-ink); background: var(--accent); }
.dreams-opt .do-count { opacity: 0.75; flex: 0 0 auto; }
.beta-tab {
  font-family: var(--px);
  font-size: var(--btn-fs);
  padding: 9px 12px 8px;
  color: var(--ink-dim);
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.12s ease-out, color 0.12s;
}
.beta-tab:hover { color: var(--ink); transform: translateY(-1px); }
.beta-tab.on { color: var(--accent-ink); background: var(--accent); }
.beta-count {
  margin-left: auto;
  font-family: var(--px);
  font-size: 8px;
  color: var(--ink-dim);
  white-space: nowrap;
}

.beta-scroll {
  display: block;
  max-width: 1280px;
  margin: 0 auto;
  padding: 20px;
}

/* Discovery-history blurb above each generation's grid. */
.beta-info {
  max-width: 900px;
  margin: 0 auto 20px;
  padding: 14px 16px;
  font-size: 18px;
  line-height: 1.55;
  color: var(--paper-ink);
  background: var(--paper);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.3);
}
.beta-info[hidden] { display: none; }

/* Names the collection you are looking at. Desktop has the tab strip for that,
   so this only appears at the mobile breakpoint, where the strip collapses into
   a single picker button. */
.beta-title { display: none; }

/* Slightly denser than the main grid since the cards carry less. */
.beta-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }

.beta-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px 10px 12px;
  font-family: inherit;
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 4px 4px 0 rgba(0, 0, 0, 0.35);
  transition: transform 0.13s ease-out, box-shadow 0.13s ease-out;
  animation: pop-in 0.28s cubic-bezier(0.25, 1.4, 0.5, 1) both;
}
.beta-card.flippable { cursor: pointer; }
.beta-card:not(.flippable) { cursor: default; }
.beta-card:hover {
  transform: translate(-3px, -3px);
  box-shadow: inset 0 0 0 2px var(--edge-hi), 8px 8px 0 rgba(0, 0, 0, 0.35);
}

/* Sprite frame. Front and back are stacked; the back is revealed on hover
   (desktop) or when the card carries .flipped (tap, set in dreamworld.js). */
.beta-art {
  position: relative;
  height: 104px;
  width: 100%;
  display: grid;
  place-items: center;
}
.beta-sprite {
  grid-area: 1 / 1;
  max-height: 96px;
  max-width: 96px;
  object-fit: contain;
  image-rendering: pixelated;
  transition: opacity 0.15s ease;
}
.beta-sprite.beta-back { opacity: 0; }
/* Collections that are illustration instead of a sprite rip. `pixelated` above
   is right for every other set on this page, but it resamples by nearest
   neighbour, which on smooth art drops rows out of the curves and leaves the
   outlines ragged. See SMOOTH_ART in porygon.js. */
.beta-sprite.beta-smooth,
.beta-modal .beta-fig img.beta-smooth { image-rendering: auto; }
/* Drawn smaller than the sprite collections, 78px against 96px. The source has
   no anti-aliasing, so its outlines are stair-stepped; the art is stored at 2x
   and drawn well under that, which leaves the browser downscaling and takes the
   hard steps out of the edge. */
.beta-sprite.beta-smooth { max-height: 78px; max-width: 78px; }
/* Hover-flip is for real pointers only. On a touchscreen :hover can stick after
   a tap, leaving the card showing its back sprite once the expanded view
   closes; touch users get that front+back view on tap instead. */
@media (hover: hover) {
  .flippable:hover .beta-front { opacity: 0; }
  .flippable:hover .beta-back { opacity: 1; }
}

/* Concept-art fallback (the two Gen 3 mons with no sprite) is a photo, so it
   renders smoothly, not pixelated. */
.beta-concept {
  max-height: 100px;
  max-width: 100px;
  object-fit: contain;
  border-radius: 3px;
}
.beta-nosprite { font-family: var(--px); font-size: 22px; color: var(--ink-dim); }

/* Card-art collections (Pokemon Card GB2). The art is a 64x48 painting and the
   page exists to show it, so the frame grows to give it a whole 2x. A round
   multiple keeps the pixels crisp; anything else makes them uneven.
   A max-width cannot do this: the sprite rules only CAP a sprite, and every
   other collection's art is larger than the cap, so nothing there ever had to
   be enlarged. 64x48 needs the size set outright. */
.beta-grid-art .beta-art { height: 112px; }
.beta-grid-art .beta-sprite {
  width: 128px;
  height: 96px;
  max-width: none;
  max-height: none;
}

/* The glitch sprites ship at 2x native (96 to 112px), so the box grows to
   hold them at natural size: any cap under 112 would rescale by a non-integer
   factor and smear the pixels. */
.beta-grid-glitch .beta-art { height: 128px; }
.beta-grid-glitch .beta-sprite { max-height: 112px; max-width: 112px; }

/* Place collections (Pokemon Center and Poke Mart Designs). These are pictures
   of buildings, and they arrive in wildly different shapes: a 64x64 Game Boy
   tile, a 400x240 DS capture, a 1920x1080 screenshot. Contained inside the
   ordinary sprite box they each sat at their own size, so a row of them looked
   like a pile of loose images, not one set.

   So every one gets the SAME plate: a fixed 4:3 frame with the picture
   contained inside it. The frame is uniform; the picture keeps its own aspect
   within it, which makes sense, since a floor plan really is a different
   shape from a shopfront.

   The black edge is the same idea as the Pokemon Card GB2 cards, except those
   carry their own border in the artwork and these have none, so it is drawn
   here. Literal near-black instead of var(--edge): the frame should look the
   same against all three of this page's palettes, and --edge is a different
   colour in each. */
.beta-grid-place .beta-art { height: 106px; }
.beta-place {
  grid-area: 1 / 1;
  /* 120x90 is measured. The grid's narrowest column is 150px, and a card
     spends 10px of padding, 3px of border and 2px of inset shadow on each
     side, leaving ~124px of content. A wider frame would be SHRUNK by the
     flex column (it wouldn't overflow), so it would be a different size at
     different viewport widths, and the plate only works if every one of them
     matches. flex-shrink:0 holds it. */
  width: 120px;
  height: 90px;
  flex: 0 0 auto;
  max-width: none;
  max-height: none;
  /* COVER, because contain letterboxes: a row of plates each with a different
     band of empty frame inside it is the same raggedness the plate was meant
     to fix, just moved inside the border. Cover crops every capture to the
     one ratio instead. They are all centred on their subject, so the crop
     takes background off the sides. The expanded view shows them whole. */
  object-fit: cover;
  background: #0d0d0d;
  border: 2px solid #0d0d0d;
  border-radius: 2px;
  /* The site's global default is `img { image-rendering: pixelated }` (line
     110), so the 3D-era CAPTURES are the ones that have to opt out: a 1280x720
     screenshot reduced to 120px with nearest-neighbour is a mess of aliased
     edges. Tiles opt back in via .px, which world.js sets from the flag the
     builder records. Same shape as .arch-img / .arch-img.px. */
  image-rendering: auto;
}
.beta-place.px { image-rendering: pixelated; }

/* A CUT-OUT building, not a photograph of one. Same box, so the grid still
   lines up, but no mat and no edge: there is nothing to frame here, just a
   building standing on nothing, and a border round it would be boxing empty
   space. Contain here, since cropping one to the shared ratio would take the
   roof off, and with no background behind it there are no bars to avoid in
   the first place. box-sizing is border-box globally, so dropping the border
   leaves the outer 120x90 unchanged. */
.beta-place.cut {
  object-fit: contain;
  background: none;
  border: 0;
}

/* Same plate in the expanded view, at the size the dialog gives it. The sprite
   frame's padding and panel fill come off: these already sit on their own black
   mat and do not want a second one around it. */
.beta-modal.place-art .beta-fig img {
  width: 232px;
  height: 174px;
  /* Cover, same as the grid: every capture is cropped to the one 4:3 plate in
     the same thin black frame. Contain drew letterbox bars down both sides of
     every screenshot whose ratio was not exactly 4:3, which was most of
     them. */
  object-fit: cover;
  image-rendering: auto;
  padding: 0;
  background: #0d0d0d;
  border: 2px solid #0d0d0d;
  border-radius: 2px;
}
.beta-modal.place-art .beta-fig img.px { image-rendering: pixelated; }
/* Cut-out buildings keep their own shape: no frame to letterbox inside, and
   cropping one takes its roof off. Same rule the grid follows. */
.beta-modal.place-art .beta-fig img.cut { background: none; border: 0; object-fit: contain; }

/* Concept-art collections (Gen 3 Concept Art). Nothing is known about these
   pieces beyond the drawing itself, so the card has no name line and no type
   row. The image IS the card, and it gets both the extra column width and the
   whole card height that a painting needs to be legible at grid size.
   The white fill is the art's own background, so `contain` letterboxing is
   seamless and doesn't show up as a mat. */
.beta-grid-concept { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
.beta-card-art { padding: 10px; }
.beta-grid-concept .beta-art { height: 190px; }
/* Absolutely positioned, not `height: 100%`: .beta-art is a grid box, and a
   percentage height against it resolves to auto for these, so the portrait
   pieces grew to their own aspect ratio and burst out of the card. inset:0
   gives the image a definite box to be contained inside. */
.beta-card-art .beta-concept {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  object-fit: contain;
  /* Padding insets the artwork without shrinking the plate: border-box keeps the
     element the full size of the frame and the white background paints under the
     padding, so the drawing gets a margin instead of touching the edges. */
  padding: 14px;
  background: #fff;
  border-radius: 2px;
}

.beta-name {
  margin-top: 8px;
  font-family: var(--px);
  font-size: 9px;
  line-height: 1.5;
  text-align: center;
  color: var(--ink);
  /* Some names are a single unbroken token with no spaces to wrap at
     ("Calico Orange/White/Black"), which pushed the text past both card edges.
     max-width keeps it inside the padding; anywhere lets it break mid-token. */
  max-width: 100%;
  overflow-wrap: anywhere;
}
.beta-final {
  margin-top: 4px;
  font-size: 15px;
  color: var(--ink-dim);
  text-align: center;
}
.beta-final.scrapped,
.beta-final.gb-only { font-style: italic; }
.beta-card .num {
  position: absolute;
  top: 8px;
  right: 10px;
  font-family: var(--px);
  font-size: 8px;
  color: var(--ink-dim);
}
.beta-card .types { display: flex; gap: 4px; margin-top: 8px; flex-wrap: wrap; justify-content: center; }

/* "Type Unknown" reuses the badge shell in a neutral grey. These are the only
   badges whose text wraps (the game lists on the Centers and Marts cards run
   to two lines), and the pixel font's tight inherited leading stacked those
   lines on top of each other. The other badges are single-line and keep their
   height. */
.type-badge.type-unknown {
  --tc: var(--edge-hi);
  color: var(--ink-dim);
  text-transform: none;
  line-height: 1.6;
}

/* ---------- Leaders: game strip, tier colour, teams ---------- */

/* A second strip under the blurb, narrowing 249 characters to one game at a
   time. NOT the .beta-tab collection strip restyled: that one changes which
   collection you are in and owns the URL, this one filters inside a
   collection and does not, so the two shouldn't look identical. Smaller,
   subtler, and it wraps. */
/* `display: flex` beats the UA sheet's `[hidden] { display: none }`, so
   world.js setting `hidden` left an empty flex box behind, still spending its
   18px bottom margin. That put 38px under the blurb here against 20px on Dream
   World and the Cache. Same guard the popups on this page already use. */
.wa-games[hidden] { display: none; }
.wa-games {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px;
  margin: 0 0 18px;
}
/* The game picker. It was a row of nineteen buttons; at that count the row
   wrapped three deep on desktop and seven on a phone, so it is a select at
   every width now. Sized like the dex's own game/region fields; stretched
   across the grid, a 1240px-wide dropdown looks like a mistake. */
.wa-game-select { width: 100%; max-width: 320px; }

/* The three tiers, as a colour the whole card carries. The badge already says
   the words; this exists so you can tell them apart without reading. Gold for
   a Champion, deep red for the Elite Four, and the ordinary panel for a Gym
   Leader, which is the right way round: Leaders are the bulk of the set, so
   they stay the neutral case and the two rarer tiers catch the eye.

   Alola's Island Kahunas sit with the Elite Four and its Trial Captains with
   the Gym Leaders, which is the closest the mainland structure gets to them.

   Tinting `background` would fight the wallpaper the page already runs, so
   this rides the border and the inset highlight, which is where the card's
   own edge already is. */
.beta-card.tier-champion {
  border-color: #a5822a;
  box-shadow: inset 0 0 0 2px #f0d98a, 4px 4px 0 rgba(0, 0, 0, 0.35);
}
.beta-card.tier-elite {
  border-color: #7b2f3c;
  box-shadow: inset 0 0 0 2px #e2a3ad, 4px 4px 0 rgba(0, 0, 0, 0.35);
}

/* The team, in the expanded view. Species and level and nothing else: moves,
   items and natures are all in the source and all left there, because the
   only question this answers is "what did they field". */
.wa-team { margin-top: 14px; }
.wa-team-h {
  font-family: var(--px);
  font-size: 9px;
  color: var(--ink-dim);
  margin-bottom: 8px;
}
.wa-team-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: center;
}
.wa-mon {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 9px 5px;
  font-family: var(--px);
  font-size: var(--btn-fs-sm);
  color: var(--paper-ink);
  background: var(--paper-2);
  border: 2px solid var(--edge);
  border-radius: 3px;
}
.wa-mon-lv { color: var(--ink-dim); }
/* The species' Black/White sprite. assets/bw covers 1-1025 with no gaps, so
   every team member on this page has one, and it is the pixel set the rest of
   the site leads with. */
.wa-mon img {
  width: 32px;
  height: 32px;
  object-fit: contain;
  image-rendering: pixelated;
  flex: 0 0 auto;
}

/* ---------- Beta Dex expanded view ---------- */

.beta-modal {
  width: min(560px, calc(100vw - 32px));
  padding: 0;
  border: 4px solid var(--edge);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink);
  box-shadow: inset 0 0 0 3px var(--edge-hi), 10px 10px 0 rgba(0, 0, 0, 0.45);
  overflow: visible;
}
.beta-modal::backdrop { background: rgba(8, 6, 4, 0.65); }
.beta-modal[open] { animation: dialog-pop 0.24s cubic-bezier(0.25, 1.4, 0.5, 1) both; }
.beta-modal[open]::backdrop { animation: fade-in 0.2s ease both; }

.beta-modal-body {
  padding: 26px 24px 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.beta-modal-name { margin: 0; font-family: var(--px); font-size: 14px; text-align: center; }
.beta-modal-sub { font-size: 17px; color: var(--ink-dim); }
.beta-modal-sub.scrapped,
.beta-modal-sub.gb-only { font-style: italic; }
.beta-modal-types { margin-top: 2px; }

/* Front + back side by side on desktop, stacked on mobile. */
.beta-modal-sprites { display: flex; flex-direction: row; gap: 20px; margin-top: 10px; }
.beta-fig { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.beta-fig img {
  width: 132px;
  height: 132px;
  object-fit: contain;
  image-rendering: pixelated;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 4px;
  padding: 10px;
}
/* Same reasoning in the expanded view: a flat 4x. The square sprite box would
   letterbox a landscape painting at 1.75x. The sprite frame's padding, panel
   fill and border all come off too: a sprite needs a mat to sit on, but a
   full-bleed painting already carries its own black edge. */
.beta-modal.card-art .beta-fig img {
  width: 256px;
  height: 192px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}

/* Expanded concept art: a wider dialog holding the piece and nothing else, so
   the padding drops to a thin frame and the image takes the space. */
.beta-modal.concept-art { width: min(760px, calc(100vw - 32px)); }
.beta-modal.concept-art .beta-modal-body { padding: 14px; }
.beta-concept-full {
  display: block;
  width: 100%;
  max-height: min(74vh, 620px);
  object-fit: contain;
  /* Same inset as the grid plate, scaled to the size the piece is shown at. */
  padding: 28px;
  background: #fff;
  border-radius: 2px;
}

/* A whole wardrobe, no front/back pair: Cafe ReMix cards open onto every
   outfit at once, so the row becomes a scrolling grid. Sized to the biggest
   set (Pikachu's 44) without letting the dialog run off the screen. */
.beta-modal.look-grid { width: min(860px, calc(100vw - 32px)); }
.beta-modal.look-grid .beta-modal-sprites {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(124px, 1fr));
  gap: 14px;
  width: 100%;
  max-height: min(62vh, 560px);
  overflow-y: auto;
  padding-right: 4px;
}
/* Buildings: sized by what is in them. A Mart with two views was getting the
   same 860px box as a Cafe Pokemon with thirty outfits, and looked like two
   small pictures adrift in an empty panel. fit-content needs the wrapping
   flex row: an auto-fill GRID collapses to a single column under intrinsic
   sizing, so the fixed-width dialog stays for Cafe above and the shrink-wrap
   applies only here, where every plate is the same 232x174. */
.beta-modal.place-art.look-grid {
  width: fit-content;
  min-width: min(328px, calc(100vw - 32px));
  max-width: min(860px, calc(100vw - 32px));
}
.beta-modal.place-art.look-grid .beta-modal-sprites {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}
.beta-modal.place-art.look-grid .beta-fig img { width: 232px; height: 174px; }
/* These are painted illustrations, so they must not be forced through the
   nearest-neighbour scaling the sprite frame applies. */
.beta-modal.look-grid .beta-fig img {
  width: 100%;
  height: 105px;   /* 140x105: the same 4:3 as the full-size plate */
  image-rendering: auto;
}
.beta-modal.look-grid .beta-fig figcaption {
  text-align: center;
  line-height: 1.5;
  overflow-wrap: anywhere;
}

.beta-modal-note {
  max-width: 380px;
  margin: 14px 0 0;
  font-size: 17px;
  line-height: 1.5;
  text-align: center;
  color: var(--ink-dim);
}

.beta-fig figcaption {
  font-family: var(--px);
  font-size: 8px;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* ---------- The tablet band ----------

   721 to 1180, the widths where the desktop layout still runs but no longer
   fits on one line. Left alone, every header degraded the same two ways: the
   action buttons wrapped into ragged right-aligned stacks two to four rows
   deep, and the tab strips wrapped into lines that stopped wherever their
   labels ran out. The principles at work here are the site's own: menu items
   sit in rectangular blocks with even padding on every side, buttons sharing
   a row share a height, and nothing overlaps anything.

   The recurring device is flex-grow on every item in a wrapping row: each
   wrapped line then stretches edge to edge, the same way the dex's type chips
   have always done it, and any number of items reads as a solid slab rather
   than a torn edge. */
@media (min-width: 721px) and (max-width: 1180px) {

  /* Archive pages and the Collector's Corner: the brand and its actions no
     longer fight for one row. Brand above, actions in a left-aligned row
     below -- every action is already 33px tall, so the row is uniform by
     construction. The 8px gap is what lets the widest set (the TCG page's
     seven controls, 679px) fit at exactly 721. */
  .beta-topbar {
    flex-direction: column;
    align-items: stretch;
    gap: 14px;
  }
  .beta-topbar-actions {
    justify-content: flex-start;
    gap: 8px;
  }

  /* Collection tabs fill each line they wrap onto. The strip itself grows
     too: on the pages where it shares .beta-toolbar with another control it
     was content-sized, and its tabs stretched only to their own edge. */
  .beta-toolbar .beta-tabs { flex: 1 1 auto; }
  .beta-tabs .beta-tab { flex: 1 1 auto; }

  /* The Collector's Corner bar: tabs as one full-width slab, then the tools
     as one row -- the search takes whatever the pill and the sort leave. */
  .cc-tabs { flex: 1 1 100%; }
  .cc-tab { flex: 1 1 auto; }
  .cc-tools { flex: 1 1 100%; flex-wrap: nowrap; }
  .cc-tools .cc-search { flex: 1 1 auto; }

  /* The dex's tab band holds ten things at this width: four dex tabs and the
     six display controls app.js parks here in desktop mode. Everything grows,
     so the wrap lands as two solid rows; the wrappers pass the stretch down
     to the button inside them, or the cell would grow and its button would
     not. The dex tabs are content-sized to 32px against the controls' 33, so
     the row height is said once, out loud. */
  .filters .dex-tabs > * { flex: 1 1 auto; }
  .filters .dex-tabs > .spr-toggle { margin-left: 0; }
  .filters .dex-tabs > .other-wrap,
  .filters .dex-tabs > .wp-wrap,
  .filters .dex-tabs > .fol-wrap { display: flex; }
  .filters .dex-tabs > .other-wrap > .other-tab,
  .filters .dex-tabs > .wp-wrap > .wp-btn,
  .filters .dex-tabs > .fol-wrap > .fol-btn { width: 100%; justify-content: center; }
  .filters .dex-tabs .dex-tab,
  .filters .dex-tabs .px-btn {
    height: 33px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
  }

  /* The selects: a two-by-two grid instead of three on a line and Sort alone
     on the next. The search keeps its icon geometry; everything stretches to
     its cell so the block's outline is a rectangle.

     The dex only. The Game Library reuses .controls and .dex-tabs for its own
     one-row layout, where a blanket width:100% handed everything full width
     and the one child allowed to shrink -- the overflow:auto chip strip --
     shrank to zero and took the category buttons with it. */
  body[data-credits="dex"] .filters .controls {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
  body[data-credits="dex"] .filters .controls > * { width: 100%; min-width: 0; }
  body[data-credits="dex"] .filters .controls .select { width: 100%; }

}

/* ---------- The squeezed dialog band ----------

   721 to 911: the detail dialog is 100vw - 32px here (its 880px cap only
   engages above 912), so the Pokedex/TCG flipper hanging off its left edge
   lands outside the viewport, and the right-hand column gets narrow enough
   that the four detail tabs run under the close button. Both get the phone
   dialog's answer, at tablet size: the flipper becomes a segmented bar across
   the top that ends before the close button, and the tabs take the two-column
   grid so no tab can reach the corner. */
@media (min-width: 721px) and (max-width: 911px) {
  /* A header band of its own: even 12px padding on every side so neither
     button is shaved by an edge, the close button's lane reserved on the
     right, and the dialog's own bright trim line drawn underneath to fence
     the band off from the body. */
  .dmodes {
    position: static;
    transform: none;
    flex-direction: row;
    gap: 0;
    padding: 12px 58px 12px 12px;
    border-bottom: 3px solid var(--edge-hi);
  }
  .dmode {
    flex: 1 1 0;
    width: auto;
    height: 38px;
    flex-direction: row;
    gap: 7px;
    border: 3px solid var(--paper-edge);
    border-radius: 3px;
    box-shadow: none;
    background: var(--paper-2);
    color: var(--paper-dim);
  }
  .dmode + .dmode { margin-left: 8px; }
  .dmode:hover { transform: none; }
  .dmode.on {
    color: var(--paper);
    background: #c23a2f;
    --ico-face: #c23a2f;
  }
  .dmode-ico { height: 18px; }
  .dmode-label { display: block; font-family: var(--px); font-size: 9px; }
  .dclose {
    width: 38px;
    height: 38px;
    top: 12px;
  }
  /* Scoped to [open] for the same reason as the phone block: a bare display
     rule would beat the UA's not-open display:none. */
  .detail[open] { display: flex; flex-direction: column; }

  .d-tabs { display: grid; grid-template-columns: 1fr 1fr; }
  .d-tabs > .d-tab:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

/* ---------- Responsive ---------- */

@media (max-width: 720px) {
  /* The stacked detail column was sizing to its CONTENT's min-content width:
     a `1fr` track's automatic minimum is min-content, and the four never-wrapping
     .d-tabs are ~397px wide, so the column blew out ~100px past the dialog and
     pushed every row off both edges. minmax(0, 1fr) + min-width:0 pins the
     column to the dialog instead, and the tab strip wraps to fit. */
  /* Full width on a phone: there is nothing to sit beside it. */
  .wa-game-select { max-width: none; }

  .detail { width: calc(100vw - 16px); max-width: none; }
  .detail-body { grid-template-columns: minmax(0, 1fr); }
  .d-left, .d-right { min-width: 0; }
  .d-left { position: static; padding: 20px 14px 16px; }
  .d-right {
    border-left: none;
    border-top: 3px solid var(--edge);
    padding: 14px 14px 18px;
  }
  /* Four tabs on one row would need the 9px label dropped to 7px and would
     still overflow at 320px, so use an even 2x2 instead: equal buttons, full
     size type at every phone width. */
  .d-tabs { display: grid; grid-template-columns: 1fr 1fr; }
  /* An odd number of tabs would leave the last one alone in a half-width cell,
     so it takes the whole row instead. A no-op at four tabs. */
  .d-tabs > .d-tab:last-child:nth-child(odd) { grid-column: 1 / -1; }

  /* There is no edge to hang a tab off here: the dialog is full-bleed, and an
     element translated past its left edge lands off-screen (the Other popover
     hit the same problem). So the two tabs become a segmented bar across the
     top of the dialog instead, which still works as the same control. */
  .dmodes {
    position: static;
    transform: none;
    flex-direction: row;
    gap: 0;
    /* The close button is absolutely positioned over this row (38px wide at
       right: 12px), so the bar has to end before it. Otherwise the right end
       of the second tab is covered and taps there close the dialog instead. */
    padding: 10px 58px 0 12px;
  }

  /* Square, and the same height as the two mode buttons it sits beside, so
     the three line up as one row with nothing smaller stuck on the end. */
  .dclose {
    width: 38px;
    height: 38px;
    top: 10px;
  }

  /* Plain segmented buttons, styled as the .d-tab controls they sit directly
     above: same 3px edge, same 3px radius, all four sides, and no bevel. The
     inset highlight is a desktop-only flourish: on a flat button it looks
     like a shadow pressed into the face, not a raised edge. */
  .dmode {
    flex: 1 1 0;
    width: auto;
    height: 38px;
    flex-direction: row;
    gap: 7px;
    border: 3px solid var(--paper-edge);
    border-radius: 3px;
    box-shadow: none;
    background: var(--paper-2);
    color: var(--paper-dim);
  }
  .dmode + .dmode { margin-left: 8px; }
  .dmode:hover { transform: none; }
  /* Exactly what .d-tab.on does, so the two rows of tabs agree with each other. */
  .dmode.on {
    color: var(--paper);
    background: #c23a2f;
    --ico-face: #c23a2f;
  }
  .dmode-ico { height: 18px; }   /* +1 for the viewBox padding, as above */

  .dmode-label { display: block; font-family: var(--px); font-size: 9px; }

  /* The bar sits in the dialog's flow here instead of overlaying it.
     MUST be scoped to [open]: a bare `.detail { display: flex }` beats the UA's
     own `dialog:not([open]) { display: none }` at any specificity, which laid
     the closed dialog out at the bottom of the page and left a dead mode bar
     sitting under the grid. */
  .detail[open] { display: flex; flex-direction: column; }

  .detail-body.is-tcg { padding: 14px 12px 18px; }

  /* Width is the scarce dimension here (on desktop it's height), so the sizing
     flips: a card scaled off viewport height would be wider than the dialog,
     leaving no room either side for the fade or for the neighbouring cards to
     show through. */
  .tcg {
    --card-w: min(62vw, 230px);
    --card-h: calc(var(--card-w) * 11 / 8);
  }
  .tcg-strip { gap: 14px; }

  /* Same auto-minimum trap one level down, on the narrowest phones: the move
     row's 1fr name column and the evolution row's mon buttons wouldn't shrink
     below their text. Both are no-ops once there's room. */
  .move { grid-template-columns: 52px minmax(0, 1fr) auto auto 34px; }
  .move-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .evo-step-mons { flex-wrap: wrap; justify-content: center; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); }
  .count { display: none; }
  /* Stacked, so the single capped row above becomes two: the preview sized to
     its own height, the controls taking what is left and scrolling inside it.
     Left as an implicit `auto` row, the controls would grow past the modal and
     hide Export PNG again, which is the bug the base rule exists to prevent.
     This is below the width where the dock and its Trainer Card button are
     shown, so it only bites on a resize with the editor already open. */
  .card-modal-body { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); }
  .card-controls { border-left: none; border-top: 3px solid var(--edge); }
  .team-label { display: none; }

  /* Mobile is a sleek pokedex scroller: brand alone, then a wallpaper+shiny row
     and a search+filters row. Mew, Guide and all team-building are desktop-only;
     everything else drops into the Advanced-filters overlay. */
  /* The desktop header's rhythm, scaled down: room above and below the brand
     row, then a full-bleed rule between it and the collapsible menu -- the
     same line the desktop draws under its header. Negative margins run the
     rule through the padding so it spans edge to edge like that one does.
     With the menu collapsed the rule stays as the header's own bottom edge:
     .topbar's border is what separates header from grid either way. */
  .topbar-inner { padding: 16px 16px 16px; }
  .brand-row {
    /* 6, not 16: .topbar-inner is a flex column with a 10px row gap on the
       phone, and the gap and this margin add. Together they are the 16 that
       matches the menu's other three sides. */
    margin: 0 -16px 6px;
    padding: 0 16px 14px;
    border-bottom: 3px solid var(--edge);
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
  }
  body.filters-collapsed .brand-row {
    margin-bottom: 0;
    padding-bottom: 2px;
    border-bottom: none;
    box-shadow: none;
  }
  .hide-mobile { display: none !important; }
  .mobile-only { display: inline-block; }
  /* The Filters button pairs an icon with a label, so it needs a flex row;
     the other .mobile-only elements get inline-block. */
  .adv-toggle.mobile-only { display: inline-flex; }
  .d-team-add { display: none; }
  .team-dock { display: none !important; }
  .tier-page { display: none !important; }

  /* Dream World and Porygon's Cache stay available on mobile (unlike
     Team/Tier). Both share .beta-topbar, so this tightens each. */
  .beta-topbar {
    padding: 16px 14px 14px;
    border-bottom: 3px solid var(--edge);
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
  }
  .beta-toolbar { padding: 14px; gap: var(--mp-gap); }
  /* Collapsed, the toolbar is gone and the header's own bottom border is the
     closing line, same as the dex. The TCG page is the exception: its tab row
     lives outside the collapsible toolbar and stays on screen, so the divider
     keeps meaning something there. */
  body.filters-collapsed .beta-topbar {
    border-bottom: none;
    box-shadow: none;
  }
  body.filters-collapsed .tcg-topbar-wrap .beta-topbar {
    border-bottom: 3px solid var(--edge);
    box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
  }
  /* Same control height as the dex's mobile bar. These are .px-btn elements
     sized by their padding, so they need the height set outright plus flex
     centring, or the label sits off-centre inside the taller box. */
  .beta-toolbar > *,
  .beta-toolbar .px-btn {
    height: var(--mp-h);
  }
  .beta-toolbar .px-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* The page runs under Safari's toolbar (100lvh), so pad the scroll area past
     it and the home indicator or the last row of cards can't be reached. */
  .beta-scroll { padding: 14px; padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px)); }
  .beta-info { font-size: 16px; padding: 12px 14px; margin-bottom: 16px; }
  /* Sits directly on top of the blurb as one unit: square inner corners and a
     shared edge, so the pair looks like one titled panel, not two boxes. */
  .beta-title {
    display: block;
    max-width: 900px;
    margin: 0 auto;
    padding: 11px 14px 9px;
    font-family: var(--px);
    font-size: var(--btn-fs);
    line-height: 1.5;
    color: var(--paper);
    background: var(--edge);
    border: 3px solid var(--edge);
    border-radius: 4px 4px 0 0;
    box-shadow: 4px 0 0 rgba(0, 0, 0, 0.3);
  }
  .beta-title + .beta-info {
    border-top-width: 0;
    border-radius: 0 0 4px 4px;
  }
  .beta-grid { grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 12px; }
  /* Two columns of art on a phone; three would shrink them to stamps. The
     pieces are drawings with no label to fall back on, so they have to stay
     readable. */
  .beta-grid-concept { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
  .beta-grid-concept .beta-art { height: 140px; }
  .beta-card-art .beta-concept { padding: 10px; }

  /* Mobile trades the tab strip for one row: [buddy] [Dreams] [Back].
     dreamworld.js moves the buddy toggle and Back down into this row. */
  .beta-tabs { display: none; }
  .beta-count { display: none; }
  .beta-toolbar { flex-wrap: nowrap; align-items: stretch; }
  .beta-toolbar > #dwModeBtn,
  .beta-toolbar > #betaBack { flex: 0 0 auto; }
  /* The toolbar stretches its items to the tallest (the buddy toggle, whose
     swatch makes it 36px). The wrap stretches with them, but a block child
     wouldn't, so make it flex and let the button fill it. Otherwise Dreams
     sits ~3px shorter than the buttons either side. */
  .dreams-wrap { display: flex; flex: 1 1 auto; }
  .dreams-btn {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
  }

  /* A caret, so the label reads as a control, not a heading. This button
     REPLACES the tab strip on phones, and with nothing but centred text on it
     there was no sign it opened anything.

     Literally the same arrow as the UI hider: same 8x5 viewBox, same four
     rects, same 12x8 box and crispEdges, so the two read as one control type.
     It is authored pointing UP (the hider's resting state), so a dropdown flips
     it 180 by default and back to 0 when the menu is open. Always pointing at
     where the list will appear. */
  .dreams-caret {
    flex: 0 0 auto;
    width: 12px;
    height: 8px;
    fill: currentColor;
    rotate: 180deg;
    transition: rotate 0.18s ease-out;
  }
  /* porygon.js / dreamworld.js / world.js already keep aria-expanded in step,
     so the flip needed no JS change. */
  .dreams-btn[aria-expanded="true"] .dreams-caret { rotate: 0deg; }

  /* The picker's menu spans the toolbar, matching the dex's Asset Archives
     dropdown. Hung off the button it was 251px of a 402px screen and shoved to
     one side, because the button is the middle item of the row.

     Anchored on .beta-toolbar, so the wrapper gives up its positioning context
     here. The offsets are that toolbar's own padding: 14px at the sides, and
     a `top` that adds back the 10px of bottom padding before the shared gap,
     so the menu clears the BUTTON by --mp-gap, not just the bar. */
  .beta-toolbar { position: relative; }
  .beta-toolbar .dreams-wrap { position: static; }
  .dreams-pop {
    top: calc(100% - 10px + var(--mp-gap));
    left: 14px;
    right: 14px;
    width: auto;
  }

  /* The mobile menu keeps just the logo and a compact "Back" (which is centered
     against the logo by the topbar's align-items:center). Donate and wallpaper
     live on the main mobile bar instead. */
  /* Mobile Dream World topbar: logo, the day/night toggle, and a compact Back. */
  /* By class, not by id: Donate now sits on every archive page and each one
     had its own id for it, so an id selector here covered three of six. */
  .beta-topbar-actions .donate-btn { display: none; }

  /* Asset Archives and Donate are DESKTOP-ONLY on the archive pages. Both were
     added to those headers for the wider layout, where there is room for them;
     a phone header is meant to be the logo and the collapse arrow and nothing
     else, and the toolbar below it is already carrying the page's own controls.

     :not([data-credits="dex"]) keeps this off the Pokedex, whose mobile
     bar has had its own full-width Archives button all along and is not part of
     this change. Its wrapper carries the same class, so an unscoped rule would
     have taken that button away too. */
  body:not([data-credits="dex"]) .archives-wrap,
  body:not([data-credits="dex"]) .donate-btn { display: none; }
  .beta-back-full { display: none; }
  .beta-back-short { display: inline; }
  .dw-mode-btn .dw-mode-name,
  .pc-mode-btn .pc-mode-name,
  .wa-mode-btn .wa-mode-name { display: none; }

  /* Expanded view stacks the two sprites on mobile. */
  .beta-modal-sprites { flex-direction: column; gap: 14px; }
  body.adv-open .adv-toggle {
    color: var(--accent-ink); background: var(--accent);
  }

  /* ONE control height and ONE gap for every mobile bar on the site: the dex's
     bar and its Filters panel, Dream World, Porygon's Cache and Trading Cards.
     Each used to size its own row (38/33/33/36px tall, gaps of 8/8/6/10),
     which looked like four different designs across four pages that share a
     header. Change these two and every bar follows.

     On :root, not on a page's own wrapper, because those pages are separate
     documents that share this stylesheet. */
  :root {
    --mp-h: 38px;
    --mp-gap: 8px;
    /* One step down for every control, so a phone gets smaller type without
       any single control choosing its own size. --field-fs stays at 16px and
       no lower: below that iOS zooms the page when a field takes focus. */
    --btn-fs: 8px;
    --btn-fs-sm: 7px;
    --field-fs: 16px;
  }

  /* Three-row mobile bar: [sprite wallpaper shiny], [Asset Archives], then
     [search filters]. */
  .mobile-primary {
    display: flex;
    flex-direction: column;
    gap: var(--mp-gap);
  }
  /* The collapse arrow is the one header control that stays on the brand row
     on a phone, at the far right against the logo. Collapsed, the whole
     control bar goes and the header is just those two things.

     .filters needs no rule here: on mobile it is already an overlay that only
     appears while .adv-open is set, and collapsing clears that. */
  body.filters-collapsed .mobile-primary { display: none; }

  .mp-row { display: flex; gap: var(--mp-gap); align-items: stretch; }
  /* The toggles grow to equal width so the only gaps left are the shared ones.
     white-space:nowrap keeps each label on one line and makes the equal-width
     split deterministic (without it the wallpaper button gets starved and wraps). */
  #mpTop { gap: var(--mp-gap); flex-wrap: wrap; }
  #mpTop > * { flex: 1 1 0; }
  #mpTop .spr-toggle { flex: 0 0 var(--mp-h); width: var(--mp-h); }
  #mpTop .wp-wrap { display: flex; }
  #mpTop .wp-btn { flex: 1 1 auto; justify-content: center; }
  #mpTop .wp-btn, #mpTop .px-btn, #mpTop .dex-tab { white-space: nowrap; }
  /* The labels no longer fit at the base 9px/12px size on narrow phones, so
     trim both within this row only and nothing spills past the viewport edge. */
  #mpTop .wp-btn, #mpTop .px-btn, #mpTop .dex-tab { padding-left: 8px; padding-right: 8px; }
  #mpTop .wp-btn { gap: 5px; }
  /* Everything in the bar shares the one height: the toggles, the archives
     button, the search field and Filters. Without the search input listed the
     row below sat a few px taller and looked misaligned. */
  #mpTop > *, #mpTop .wp-btn, .archives-wrap, .archives-btn,
  .mp-row #search, .mp-row .adv-toggle { height: var(--mp-h); }
  .mp-row .search-wrap { flex: 1 1 auto; }
  .mp-row .shiny-tab { margin-left: 0; }

  /* Asset Archives: one full-width button on its own line. flex-basis 100%
     puts it there: an item that wide cannot share a line, so no spacer
     element is needed to force the break. */
  /* One full-width button on its own row. Keep the `#mpTop` prefix:
     `#mpTop > *` above sets `flex: 1 1 0` and an ID beats a class, so as a
     bare class this basis was discarded and the button only took its own row
     when the four controls happened not to fit (true at 390px, false on a
     402px iPhone, where it joined the top row and dragged its dropdown into
     the corner at button width). */
  #mpTop .archives-wrap { flex: 1 0 100%; }
  /* The menu spans the bar here; it doesn't hang off the button. */
  .archives-pop {
    top: calc(100% + var(--mp-gap));
    left: 0;
    right: 0;
    width: auto;
  }

  .filters {
    display: flex;
    flex-direction: column;
    gap: 10px;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    padding: 12px 16px 16px;
    background: var(--panel-2);
    border-bottom: 3px solid var(--edge);
    box-shadow: 0 8px 0 rgba(0, 0, 0, 0.25);
    max-height: 78vh;
    overflow-y: auto;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-8px);
    pointer-events: none;
    transition: opacity 0.22s ease, transform 0.22s ease, visibility 0.22s;
    z-index: 40;
  }
  body.adv-open .filters {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    pointer-events: auto;
  }
  /* A divider so the menu looks like its own panel and doesn't run on from
     the header above it. */
  .filters {
    border-top: 3px solid var(--edge);
    /* One gap value for the whole panel and equal space above and below the
       set, so the eight controls sit as a single grid, not three stacked
       groups with their own rhythms. */
    gap: 8px;
    padding: 16px;
  }
  .filters .dex-tabs,
  .filters .controls { gap: 8px; }

  /* Every control is half a row wide, less half the shared 8px gap, so all
     eight land on the same two columns. */
  .filters .dex-tabs > .dex-tab,
  .filters .dex-tabs > .other-wrap,
  .filters .controls .select,
  .filters .controls .clear-btn { flex: 1 1 calc(50% - 4px); }
  .filters .dex-tabs > .other-wrap > .other-tab { width: 100%; }
  /* The popover cannot fit beside a control that starts past the middle of the
     screen, so the select takes over here and the button and popover go away.
     Matched to the same depth as the rule that lays the dex tabs out, or that
     rule's display:flex keeps the button visible underneath the select. */
  .filters .dex-tabs > .other-wrap > .other-tab,
  .filters .dex-tabs > .other-wrap > .other-pop { display: none; }
  .filters .dex-tabs > .other-wrap > .other-select {
    display: block;
    width: 100%;
    height: 100%;
    padding-left: 10px;
    padding-right: 26px;
  }

  /* Filled once a category is chosen, matching Pokedex/Megadex/Gigantamax
     beside it. Otherwise it is the one control in the row that gives no sign
     it is doing anything. The arrow is a background image, so it has to be
     re-drawn in the knockout colour or it disappears into the fill. */
  .filters .dex-tabs > .other-wrap > .other-select.on {
    color: var(--accent-ink);
    background-color: var(--accent);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7'%3E%3Cpath d='M0 0h12L6 7z' fill='%23fff'/%3E%3C/svg%3E");
    box-shadow: inset 2px 2px 0 rgba(255, 255, 255, 0.25), 3px 3px 0 rgba(0, 0, 0, 0.3);
  }

  /* And the same height. The dex tabs and the selects use different fonts and
     paddings, which left them a few pixels apart; fixing the box height and
     zeroing the vertical padding makes them uniform regardless. The height is
     the shared one, so the panel matches the bar that opens it. It used to be
     42px against the bar's 38px, which showed as soon as the panel dropped. */
  .filters .dex-tabs > .dex-tab,
  .filters .dex-tabs > .other-wrap,
  .filters .dex-tabs > .other-wrap > .other-tab,
  .filters .controls .select,
  .filters .controls .clear-btn {
    height: var(--mp-h);
    padding-top: 0;
    padding-bottom: 0;
  }
  /* Buttons need their label centred once the padding is gone; a select
     centres its own text. */
  /* Keep .other-tab out of this list: it is hidden at this breakpoint in
     favour of the select, and listing it here would re-show it, since this rule
     ties on specificity with the one that hides it and wins on order. */
  .filters .dex-tabs > .dex-tab,
  .filters .controls .clear-btn:not([hidden]) {
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .filters .controls .select {
    min-width: 0;
    /* At half a phone's width the longest option, "Number: low to high",
       overflows its field by 24px. Most of that comes back out of the padding,
       not the text: the arrow does not need 30px of clearance, and a 17px
       face still matches the fields around it far better than the 15.8px it
       would take to fit at the original padding. The size itself now comes
       from --field-fs; only the padding is reclaimed here. */
    padding-left: 10px;
    /* The chevron is drawn 10px from the right edge and is 12px wide, so the
       text needs to stop before 22px or it runs straight into it. */
    padding-right: 26px;
  }
  /* The Types control is a button, which centres its text; the selects beside
     it start theirs at the left edge. */
  .type-toggle { justify-content: flex-start; }

  /* Type chips live behind the Types control here and expand beneath it.

     A grid, not a wrapping flex row. Stretching chips to fill each line
     meant a short final row absorbed all the leftover space, so dark, steel
     and fairy ended up with 37px either side of the word while a full row sat
     at 9px. Equal columns give every chip the same box, and a short last row
     leaves its remaining columns empty instead of inflating. */
  .filters .type-row { display: none; }
  body.types-open .filters .type-row {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
    gap: 6px;
  }
  .filters .type-row .type-chip { flex: none; width: auto; }

  .other-pop { width: 200px; }

  /* The wallpaper popover spans the bar and sits under the row that opens it,
     matching the Asset Archives dropdown; it doesn't hang off the button
     itself. Anchored on .mobile-primary instead of .wp-wrap: the button is
     the second item in its row, so anchoring to it put the popover off-centre
     and, at 340px wide, past the right edge of a 390px phone.

     `top` is the first row's height plus one gap, which is exactly the space
     the archives dropdown leaves below its own button, and it stays correct if
     either variable changes. */
  .mobile-primary { position: relative; }
  /* The wallpaper button lives on the brand row now. Its wrapper goes static
     so the popover resolves against .topbar -- sticky, and therefore already
     a containing block -- and spans the bar under the whole header, the same
     anchor trick the TCG page uses. */
  #hdrActions .wp-wrap { position: static; }
  .wp-pop {
    top: calc(100% + 6px);
    left: 12px;
    right: 12px;
    width: auto;
    max-height: 72vh;
    overflow-y: auto;
  }

  /* ---------- The brand-row squares ----------

     Wallpaper and dark mode (or the companion mode toggle on the pages whose
     palette rides the companion) sit left of the collapse arrow on every
     page, as squares of exactly its 33px, so the two most-used switches stay
     reachable with the menu collapsed. The wallpaper button drops its label
     and keeps the swatch; the mode buttons already dropped their names on the
     phone and keep their swatches too. */
  #hdrActions .wp-btn,
  .beta-topbar-actions .wp-btn,
  .tcg-topbar-wrap .beta-topbar-actions .wp-btn,
  .px-btn.dw-mode-btn,
  .px-btn.pc-mode-btn,
  .px-btn.wa-mode-btn {
    width: 33px;
    height: 33px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
  }
  #hdrActions .wp-btn #wpName,
  .beta-topbar-actions .wp-btn #wpName { display: none; }

  /* The Game Library's category dropdown on the mobile bar. Same static-wrap
     trick as the wallpaper popover: the popup resolves against
     .mobile-primary and spans the bar, instead of hanging off a half-width
     button. */
  #mpTop .dreams-wrap { position: static; }
  #mpTop .dreams-pop {
    top: calc(var(--mp-h) + var(--mp-gap));
    left: 0;
    right: 0;
    width: auto;
    max-height: 60vh;
    overflow-y: auto;
  }
}

/* ==================================================================
   Trading Cards page (/tcg)
   ==================================================================
   Runs on the dex's wallpaper palette, with no page palette of its own.
   Unlike Dream World, this is the same catalogue seen a different way, so it
   should feel like the same room. */

/* Parked with the Trading Cards page; see the note in index.html. */

/* This page used to be the odd one out: every other header held a wordmark SVG
   and this one had no artwork, so its title was set in the pixel face instead.
   Now that all of them are live text in Basset it just uses .brand-logo, and
   the only thing left to say is the phone size below. */

/* Same sticky unit as .dw-topbar-wrap: title row and searches pin together,
   with the safe-area padding on the wrapper so its fill reaches the notch. */
.tcg-topbar-wrap {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--panel-2);
  border-bottom: 3px solid var(--edge);
  box-shadow: 0 3px 0 rgba(0, 0, 0, 0.3);
  padding-top: env(safe-area-inset-top, 0px);
}

/* The coloured band runs edge to edge; the content inside it is capped to the
   same 1280 column as <main>, so the searches line up with the cards under
   them. Same setup as .topbar / .topbar-inner on the dex. Without it the
   header ran full width over a 1280 body and the page looked inset. */
.tcg-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 0 0 8px;
}
.tcg-topbar-wrap .beta-topbar,
.cc-bar {
  width: 100%;
  max-width: 1280px;
  margin-inline: auto;
}
/* Border-box, so the 20px lands inside the 1280 and the content edge sits at
   exactly the same place as .tcg-scroll's. */
.cc-bar { padding-inline: 20px; }

/* Desktop gives the searches their own band, the way Dream World's tab strip
   gets one: they are the page's controls, so they sit apart from its title. */
@media (min-width: 721px) {
  .tcg-toolbar {
    /* Horizontal padding lives on the capped children, not here, so the band
       itself keeps running the full width of the window. 10px, down from 18:
       every control in the row is now a fixed 32px, so the band no longer has
       to leave slack for buttons of different heights. */
    padding: 10px 0;
    background: var(--panel);
    border-top: 3px solid var(--edge);
  }
}

.tcg-toolbar { flex-direction: column; align-items: stretch; }
.tcg-lens-label {
  font-family: var(--px);
  font-size: 7px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* ---------- The always-open bar: tabs, count, search, two pills ----------

   One row on a desktop, wrapping only on a phone. Every control in it is
   --cc-h tall and vertically centred, so the row reads as one band instead of
   a set of differently-sized buttons that happen to sit together. Any new
   control added here has to take that height too. */
.cc-bar {
  --cc-h: 32px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
}
/* Both groups wrap. On a desktop .cc-bar is nowrap and there is room, so they
   never do; on a phone the five tabs are 401px against a 375px screen and
   without this they push the whole page sideways. */
.cc-tabs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
.cc-tab {
  display: inline-flex;
  align-items: center;
  height: var(--cc-h);
  font-family: var(--px);
  font-size: var(--btn-fs-sm);
  /* No vertical padding: the height sets the box and the flex centring sets
     the text, so the pixel font's odd baseline cannot push it off centre. */
  padding: 0 13px;
  color: var(--ink-dim);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.12s, background 0.12s;
}
.cc-tab:hover { color: var(--ink); }
.cc-tab.on { color: var(--accent-ink); background: var(--accent); }
.cc-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Sits immediately right of the last tab, so it reads as the size of what the
   tabs point at. .beta-count's own margin-left:auto has to be undone or the
   two autos split the slack and the count floats mid-row; margin-right:auto is
   then what pushes the tools to the far end. */
.cc-count {
  margin-left: 0;
  margin-right: auto;
  white-space: nowrap;
}

/* Clear inherits .px-btn's padding, which is taller than the row. */
.cc-tools .tcg-clear {
  height: var(--cc-h);
  padding-block: 0;
  line-height: 1;
}

.cc-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.cc-search {
  position: relative;
  flex: 0 1 300px;
  min-width: 170px;
}

/* One row once there is room for all of it. Measured: five tabs, the count,
   the search at its 170px floor and both pills need about 975px, so anything
   narrower wraps into two rows instead of overflowing.

   THIS MUST STAY BELOW the .cc-bar / .cc-tabs / .cc-tools base rules. Same
   specificity, so source order decides, and placed above them the base
   flex-wrap:wrap wins and the row silently breaks into three. That has now
   bitten twice.

   All three selectors, not just .cc-bar: with only the bar set to nowrap its
   two children shrink under their content and wrap internally instead. */
@media (min-width: 1100px) {
  .cc-bar, .cc-tabs, .cc-tools { flex-wrap: nowrap; }
}

/* Matches the pills and tabs exactly. .tcg-input's own padding is overridden
   here so the height comes from one place. */
.cc-search .tcg-input {
  height: var(--cc-h);
  padding-block: 0;
}
/* Wider than the old per-kind popovers: rows here carry a name, a note and a
   count, and they are grouped by what the thing is. */
.cc-pop { max-height: min(560px, 70vh); }
.cc-pop .to-note {
  flex: 0 0 auto;
  font-size: 6px;
  color: inherit;
  opacity: 0.7;
}

/* A labelled pill, not an icon button. It changes every count on the page, so
   it has to say what it is; the dot is the on/off tell at a glance. */
.cc-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: var(--cc-h);
  font-family: var(--px);
  font-size: var(--btn-fs-sm);
  padding: 0 12px;
  color: var(--ink-dim);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.12s, background 0.12s;
}
.cc-pill:hover { color: var(--ink); }
.cc-pill.on { color: var(--accent-ink); background: var(--accent); }
.cc-pill-dot {
  width: 9px;
  height: 9px;
  border-radius: 999px;
  background: var(--panel);
  border: 2px solid var(--edge);
}
.cc-pill.on .cc-pill-dot { background: var(--accent-ink); border-color: var(--accent-ink); }
.cc-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The sort pill is a two-way switch, so it has no on state; the label is what
   tells you where it is. The arrow flips to back the label up at a glance. */
.cc-sort-pill { color: var(--ink); }
.cc-sort-arrow {
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 6px solid currentColor;
  transition: transform 0.14s;
}
.cc-sort-pill.oldest .cc-sort-arrow { transform: rotate(180deg); }

/* One connected strip, so its options come across as a single control with a
   single answer, not a set of independent buttons. */
.tcg-seg {
  display: flex;
  border: 3px solid var(--edge);
  border-radius: 3px;
  overflow: hidden;
}
.tcg-segb {
  font-family: var(--px);
  font-size: var(--btn-fs-sm);
  padding: 8px 11px 7px;
  color: var(--ink-dim);
  background: var(--panel-2);
  border: none;
  /* The strip owns the divider, so the outer ends stay flush against the
     border. */
  border-left: 3px solid var(--edge);
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.12s, background 0.12s;
}
.tcg-segb:first-child { border-left: none; }
.tcg-segb:hover { color: var(--ink); }
.tcg-segb.on { color: var(--accent-ink); background: var(--accent); }
.tcg-segb:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; }

.tcg-input {
  width: 100%;
  padding: 7px 12px 6px 32px;
  font-family: var(--vt);
  font-size: var(--field-fs);
  color: var(--ink);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  outline: none;
  transition: box-shadow 0.12s;
}
.tcg-input::placeholder { color: var(--ink-dim); }
.tcg-input:focus { box-shadow: inset 0 0 0 2px var(--accent); }
/* Safari draws its own clear affordance on type=search, which collides with
   the popover's own way out. */
.tcg-input::-webkit-search-cancel-button { display: none; }

/* Suggestion list. Styled after the dex's Other menu so the two feel like the
   same kind of control. */
.tcg-pop[hidden] { display: none; }
.tcg-pop {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 120;
  width: max(240px, 100%);
  max-height: min(420px, 60vh);
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 6px 6px 0 rgba(0, 0, 0, 0.4);
  animation: pop-in 0.18s ease-out both;
}
.tcg-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 10px 8px;
  font-family: var(--px);
  font-size: 7px;
  line-height: 1.5;
  text-align: left;
  color: var(--ink-dim);
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: color 0.12s, background 0.12s;
}
.tcg-opt:hover, .tcg-opt.on { color: var(--accent-ink); background: var(--accent); }
.tcg-opt .to-count { opacity: 0.75; flex: 0 0 auto; }
.tcg-opt-none {
  padding: 10px;
  font-family: var(--px);
  font-size: 7px;
  color: var(--ink-dim);
}

/* One wrapper per era, so its heading is pushed out by the next era. Without
   the wrappers every heading pins to the same line and they pile up. */
.tcg-optgroup { display: flex; flex-direction: column; gap: 4px; }
.tcg-opt-era {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding: 12px 4px 5px;
  font-family: var(--px);
  font-size: 7px;
  letter-spacing: 0.05em;
  color: var(--ink);
  border-bottom: 2px solid var(--edge);
  /* Sticky so you always know which era you are scrolling through. */
  position: sticky;
  top: -8px;
  background: var(--panel-2);
  z-index: 1;
}
.tcg-opt-era:first-child { padding-top: 4px; }
.toe-note { font-weight: normal; color: var(--ink-dim); }

.tcg-scroll {
  display: block;
  max-width: 1280px;
  margin: 0 auto;
  padding: 20px;
}

.tcg-loading, .tcg-empty {
  padding: 40px 0;
  text-align: center;
  font-family: var(--px);
  font-size: 9px;
  line-height: 1.8;
  color: var(--ink-dim);
}
.tcg-loading[hidden], .tcg-empty[hidden] { display: none; }
.tcg-featured[hidden], .tcg-results[hidden] { display: none; }
.cc-browse[hidden] { display: none; }

/* ==================================================================
   The four browse indexes
   ==================================================================
   Sets, artists, Pokemon and categories are four grids of the same tile with
   different art in it, so they share everything below and differ only in
   column width. */

.cc-browse { display: block; }

/* A title over an index that needs naming but no explanation. Same plate as
   .tcg-feat-label on the landing view, so the two pages match. */
.cc-badge {
  display: inline-block;
  margin: 0 0 18px;
  padding: 8px 11px 7px;
  font-family: var(--px);
  font-size: 9px;
  letter-spacing: 0.05em;
  color: var(--ink);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3);
}


/* Jump-to row for the Set index. Sixteen eras is a lot of scrolling, and the
   arrows only help once you are already at the right one. */
.cc-jump {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 9px;
  margin-bottom: 26px;      /* the page's rhythm, same as .cc-sec */
  padding: 10px 12px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3);
}
.cc-jump-label {
  font-family: var(--px);
  font-size: 7px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
/* Equal columns, so the chips line up in a true rectangle instead of each row
   ending wherever its labels happened to run out. 178px is the widest label
   ("HeartGold & SoulSilver" measures 176), so nothing wraps to a second line
   and every cell is the same height. */
/* The phone's Jump To. Hidden on a desktop, where the grid of chips is
   better: a select would be one tap to open plus a scroll to choose, against
   a grid that shows every era at once. */
.cc-jump-select {
  display: none;
  width: 100%;
  height: var(--cc-h);
  padding: 0 10px;
  font-family: var(--px);
  font-size: var(--btn-fs-sm);
  color: var(--ink);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
}

.cc-jump-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(178px, 1fr));
  gap: 6px;
}
.cc-jump-b {
  font-family: var(--px);
  font-size: 7px;
  padding: 7px 9px 6px;
  text-align: center;
  color: var(--ink-dim);
  background: var(--panel);
  border: 2px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: color 0.12s, background 0.12s;
}
.cc-jump-b:hover { color: var(--accent-ink); background: var(--accent); }

/* ---------- Collapsible sections ---------- */

.cc-sec { margin-bottom: 26px; }
/* The whole heading is the target, so the arrow is never a tiny hit area. */
.cc-sec-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 10px 14px 9px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3);
  cursor: pointer;
  transition: background 0.12s;
}
.cc-sec-head:hover { background: var(--panel); }
.cc-sec-title {
  font-family: var(--px);
  font-size: 10px;
  line-height: 1.4;
  color: var(--ink);
}
.cc-sec-note {
  font-family: var(--px);
  font-size: 7px;
  line-height: 1.6;
  color: var(--ink-dim);
}
/* Drawn in CSS so it matches the pixel furniture and needs no SVG per section.
   Points down when open, right when shut. */
.cc-arrow {
  flex: 0 0 auto;
  align-self: center;
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 7px solid var(--ink);
  transition: transform 0.14s;
}
.cc-sec.shut .cc-arrow { transform: rotate(-90deg); }
.cc-sec-body { padding-top: 16px; }
.cc-sec.shut .cc-sec-body { display: none; }

/* ---------- Tiles ---------- */

.cc-grid {
  display: grid;
  gap: 14px;
}
/* Set logos are wide and short, so they get a wider column than a card does. */
.cc-grid-set { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
/* Artist and category tiles show a card, which is portrait. */
.cc-grid-artist,
.cc-grid-cat { grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); }

.cc-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 12px 10px 11px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3);
  cursor: pointer;
  transition: transform 0.12s, box-shadow 0.12s, background 0.12s;
}
.cc-tile:hover {
  transform: translateY(-3px);
  box-shadow: 3px 6px 0 rgba(0, 0, 0, 0.35);
  background: var(--panel);
}
.cc-tile:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* Fixed height so a row of logos lines up whatever their aspect ratio. The
   logos are trimmed to their ink by fetch_set_logos.py, so contain gives every
   one the same optical weight. */
.cc-tile-art {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 74px;
}
.cc-tile-art img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}
/* Artist and category tiles show a whole card, which needs more room. */
.cc-art-card { height: 168px; }
.cc-art-card img {
  height: 100%;
  width: auto;
  border-radius: 4px;
  box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.3);
}
/* The three Pocket sets with no logo published yet. The name carries the tile. */
.cc-tile.no-logo .cc-tile-art::after {
  content: "";
  width: 46px;
  height: 46px;
  background: var(--panel);
  border: 3px dashed var(--edge);
  border-radius: 3px;
}
.cc-tile-name {
  font-family: var(--px);
  font-size: 8px;
  line-height: 1.55;
  text-align: center;
  color: var(--ink);
}
/* Pinned to the bottom so the year-and-count line sits level across a row,
   whether the set name above it wrapped to one line or three. */
.cc-tile-note {
  margin-top: auto;
  font-family: var(--px);
  font-size: 6px;
  line-height: 1.5;
  text-align: center;
  color: var(--ink-dim);
}

/* ---------- The Pokemon index ---------- */

/* Small on purpose. 1,025 sprites only work as an index if a generation fits
   on a screen or two, and a B/W sprite is readable well below its native 96. */
.cc-mon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(76px, 1fr));
  gap: 6px;
}
.cc-mon {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 6px 2px 5px;
  background: var(--panel-2);
  border: 2px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
  transition: transform 0.1s, background 0.1s;
}
.cc-mon:hover { transform: translateY(-2px); background: var(--accent); }
.cc-mon:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cc-mon img {
  width: 56px;
  height: 56px;
  object-fit: contain;
  image-rendering: pixelated;
}
.cc-mon-name {
  font-family: var(--px);
  font-size: 5px;
  line-height: 1.5;
  text-align: center;
  color: var(--ink-dim);
  /* Long names get one line and an ellipsis; the title attribute has the rest. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cc-mon:hover .cc-mon-name { color: var(--accent-ink); }

/* ---------- Cameos, under a Pokemon's own gallery ---------- */

.cc-cameos { display: block; margin-top: 40px; }
.cc-cameos[hidden] { display: none; }
/* A rule above it, so it reads as a second section and not more of the grid it
   sits under. */
.cc-cameo-head {
  padding-top: 26px;
  border-top: 3px solid var(--edge);
  margin-bottom: 16px;
}
.cc-cameo-head .cc-badge { margin-bottom: 10px; }
.cc-cameo-note {
  max-width: 62ch;
  margin: 0;
  font-family: var(--vt);
  font-size: 17px;
  line-height: 1.45;
  color: var(--ink);
  /* Sits straight on the wallpaper, like the featured headings. */
  text-shadow: 1px 1px 0 var(--panel-2), -1px 1px 0 var(--panel-2),
               1px -1px 0 var(--panel-2), -1px -1px 0 var(--panel-2);
}

/* The cameos with no card in the archive. Host card on the left, where to find
   it on the right. */

@media (max-width: 720px) {
  .cc-cameo-note { font-size: 16px; }

  /* A 178px grid on a 335px screen is one chip per row, seventeen rows deep,
     which pushes the first set most of a screen down. A swipeable row fixed
     the height and hid most of the eras behind a gesture with nothing on
     screen to suggest it, so the phone gets a real dropdown: every era in one
     list, one tap, no horizontal scrolling. The grid is what the select
     replaces, so only one of the two is ever present. */
  .cc-jump-grid { display: none; }
  /* Taller than the row's controls: it is the one thing on the page a thumb
     has to hit precisely to open a list, and at 32px it read as a hairline
     against the panel it sits in. */
  .cc-jump-select {
    display: block;
    height: 44px;
    border-radius: 0;
  }

  /* Two columns on a phone. At the desktop 190px minimum a 375px screen gets
     one set per row, and a logo does not need that much space. */
  .cc-grid-set { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); }
  .cc-grid-artist,
  .cc-grid-cat { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
  .cc-tile-art { height: 56px; }
  .cc-art-card { height: 132px; }

  .cc-mon-grid { grid-template-columns: repeat(auto-fill, minmax(62px, 1fr)); }
  .cc-mon img { width: 46px; height: 46px; }

  /* The search takes the full width of its own line; the pill sits under it
     with the count, so neither gets squeezed to an unreadable width. */
  .cc-search { flex: 1 1 100%; max-width: none; }
}

/* ---------- Featured headings ---------- */

/* A column: the badge sits on its own line above the name, so the two never
   compete for the same baseline. */
.tcg-feat-head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 9px;
  margin-bottom: 14px;
  /* Indented past the arrows to start where the cards start. The arrows are
     controls sitting in the margin, not part of the column, so hanging the
     badge and the name off them put the heading a step left of everything it
     labels. Same inset the Featured Pokemon grid uses, and it is already 0 on
     mobile, where there are no arrows to clear. */
  margin-left: var(--cc-inset, 0px);
  margin-right: var(--cc-inset, 0px);
}
.tcg-feat-head-2 { margin-top: 34px; }
/* Name and its counts share a line, centred against each other so the small
   count sits mid-height of the big name instead of on its baseline. */
.tcg-feat-line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
}
.tcg-feat-label {
  font-family: var(--px);
  font-size: 7px;
  letter-spacing: 0.05em;
  color: var(--ink-dim);
  /* The label sits straight on the wallpaper, so it needs its own backing to
     stay legible across all eighteen of them. */
  padding: 5px 7px 4px;
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3);
}
/* The name is the link to that artist's or Pokemon's full grid. */
.tcg-feat-name {
  font-family: var(--vt);
  font-size: 30px;
  line-height: 1.1;
  color: var(--ink);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  transition: color 0.12s;
}
.tcg-feat-name:hover { color: var(--accent); }
.tcg-feat-count {
  font-family: var(--px);
  font-size: 9px;
  color: var(--ink);
}

/* ---------- Featured artist rail ---------- */

/* The rail and the Featured Pokemon grid under it share one column track, so
   the seven cards in the rail sit directly above the seven in the grid. Both
   read off these, and the arrows are pushed outside the track rather than
   over it: the grid is inset by an arrow plus its gap on each side, which is
   exactly the room the flex row gives the rail.

   Seven is a deliberate count, not auto-fill. At the 1280px page width the
   grid was landing on seven anyway, but only by arithmetic -- any change to
   the minimum column width silently moved it, and the rail, fixed at 168px
   cards, never agreed with it at any width. */
.tcg-featured {
  --cc-cols: 7;
  --cc-gap: 16px;
  --cc-arrow: 34px;
  --cc-arrow-gap: 10px;
  --cc-inset: calc(var(--cc-arrow) + var(--cc-arrow-gap));
}

.tcg-rail-wrap {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--cc-arrow-gap);
}
.px-btn.tcg-rail-arrow {
  flex: 0 0 auto;
  width: var(--cc-arrow, 34px);
  height: var(--cc-arrow, 34px);
  padding: 0;
  /* An icon button: the size belongs to the arrow glyph, so it sits outside
     the --btn-fs scale. */
  font-size: 17px;
  line-height: 1;
}
.tcg-rail {
  flex: 1 1 auto;
  min-width: 0;
  /* A single grid row rather than flex, so a card's width is a share of the
     track instead of a fixed pixel count. That is what keeps it equal to a
     grid column below as the page resizes. */
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: calc(
    (100% - (var(--cc-cols) - 1) * var(--cc-gap)) / var(--cc-cols));
  /* Same reason as the grid: art shapes differ, so top-align; stretching
     would size every card to the tallest one in the rail. */
  align-items: start;
  gap: var(--cc-gap);
  scroll-snap-type: x proximity;
  overflow-x: auto;
  scrollbar-width: none;
  /* pan-y here for years, which is backwards: it hands VERTICAL gestures to
     the browser and leaves the rail nothing, so a sideways swipe scrolled the
     page instead of the rail. On a phone the arrows are hidden, so swiping is
     the only way through and there was no way through. pan-x lets the rail
     take the horizontal drag while the page keeps the vertical one. */
  touch-action: pan-x;
  /* No horizontal padding: the column width is a share of this box, so 2px
     here makes every rail card 1px narrower than the grid column below and
     the two stop lining up. The vertical padding stays -- it is what the
     cards' drop shadow needs. */
  padding: 4px 0 10px;
  scroll-behavior: smooth;
  cursor: grab;
}
.tcg-rail::-webkit-scrollbar { display: none; }
.tcg-rail.dragging { cursor: grabbing; scroll-behavior: auto; }
.tcg-rail.dragging .tcg-card { cursor: grabbing; }
.tcg-rail .tcg-card {
  width: auto;              /* the track sizes it */
  scroll-snap-align: start;
}

/* The Featured Pokemon grid, pulled in to the rail's width so the two line up.
   Only this grid: .tcg-cards is also the results grid and the cameo grid, and
   neither of those sits under a rail. */
#featMonGrid {
  grid-template-columns: repeat(var(--cc-cols), minmax(0, 1fr));
  gap: var(--cc-gap);
  margin-left: var(--cc-inset);
  margin-right: var(--cc-inset);
}

/* Fewer columns as the page narrows, so a card never shrinks below the width
   its art needs to read. The rail and the grid step together, since they share
   --cc-cols, and the alignment holds at every step. */
@media (max-width: 1180px) { .tcg-featured { --cc-cols: 6; } }
@media (max-width: 1020px) { .tcg-featured { --cc-cols: 5; } }
@media (max-width: 860px)  { .tcg-featured { --cc-cols: 4; } }

/* ---------- The result header ---------- */

/* Says in words what is being shown. It also answers "do the three searches
   combine?": they visibly add up here, joined by a +. */
/* The header names what is on show on the left and carries the sort on the
   right. They wrap onto separate lines before the sort ever squeezes. */
.tcg-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px 20px;
  margin-bottom: 18px;
}
.th-main { flex: 1 1 320px; min-width: 0; }
.th-parts { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.th-part {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 8px 6px 11px;
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3);
}
/* The collection-stepping arrows, dressed as the badge beside them. */
.th-step { display: inline-flex; gap: 6px; align-self: stretch; }
.th-step-b {
  display: grid;
  place-items: center;
  /* As tall as the badge beside them, and exactly as wide as tall: the badge
     sets the row's height, stretch hands it to the buttons, and the aspect
     ratio keeps them square at whatever size that turns out to be. */
  height: auto;
  aspect-ratio: 1;
  /* The floor is the badge's own one-line height. On a wide screen the
     stretch rule above makes the floor irrelevant; it exists for the narrow
     screen, where a long collection name fills the row and the arrows wrap to
     a line of their own -- stretch then matches them to that empty line and
     they collapsed to their content. */
  min-height: 41px;
  font-size: 11px;
  color: var(--ink);
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 3px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.3);
  cursor: pointer;
  transition: transform 0.12s ease-out;
}
/* Hover only where hovering exists. A phone applies :hover on tap and leaves
   it applied until the next touch, so the arrow stayed raised after pressing
   it -- the raise is desktop-only and a tap just presses. */
@media (hover: hover) {
  .th-step-b:hover { transform: translateY(-2px); }
}
.th-step-b:active { transform: translateY(0); }
.th-step-b:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

.th-kind {
  font-family: var(--px);
  font-size: 7px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.th-name { font-family: var(--vt); font-size: 22px; line-height: 1; color: var(--ink); }
.th-and {
  font-family: var(--px);
  font-size: 9px;
  color: var(--ink);
  text-shadow: 1px 1px 0 var(--panel-2), -1px 1px 0 var(--panel-2), 1px -1px 0 var(--panel-2), -1px -1px 0 var(--panel-2);
}
.th-drop {
  width: 20px;
  height: 20px;
  padding: 0;
  font-family: var(--vt);
  font-size: 18px;
  line-height: 1;
  color: var(--ink-dim);
  background: var(--panel-2);
  border: 2px solid var(--edge);
  border-radius: 2px;
  cursor: pointer;
}
.th-drop:hover { color: #fff; background: #d3425f; border-color: #47101d; }
.th-notes {
  margin-top: 8px;
  font-family: var(--px);
  font-size: 7px;
  line-height: 1.7;
  color: var(--ink);
  text-shadow: 1px 1px 0 var(--panel-2), -1px 1px 0 var(--panel-2), 1px -1px 0 var(--panel-2), -1px -1px 0 var(--panel-2);
}

/* ---------- Card grids ---------- */

/* No panel behind these: the cards carry their own frame and sit straight on
   the wallpaper. Sam asked for this, and it stops the page looking like one
   giant box. */
.tcg-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
  gap: 16px;
  /* Cards set their own height from their art, and not every set's art is the
     same shape, so hang them from the top of the row instead of stretching
     them all to whichever card in that row happens to be tallest. */
  align-items: start;
}

/* No border: a Pokemon card already carries its own printed frame, and a black
   outline on top of it looks like a second, competing one. The drop shadow
   alone is enough to lift the card off the wallpaper.

   The card art is NOT one shape. Most of it is 600x825 (8/11), but the whole
   of HeartGold & SoulSilver and Call of Legends is 700x990 (0.707), parts of
   Sun & Moon are 734x1024, and parts of EX are 600x835. A box locked to 8/11
   with object-fit:cover shaved a slice off the top and bottom of every one of
   those, about 3% for the 700x990 sets, which is a visible trim through the
   card's own border. So the image sets its own height and nothing is cropped;
   the shadow moves onto the image so it still hugs the card exactly. */
.tcg-card {
  display: block;
  width: 100%;
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  transition: transform 0.13s ease-out;
}
.tcg-card:hover { transform: translate(-3px, -3px); }
.tcg-card:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }
.tcg-card img {
  width: 100%;
  height: auto;
  /* `auto 8/11` means "the image's own ratio once it is known, 8/11 until
     then": space is reserved before it loads and the grid does not jump,
     but the real shape wins the moment it arrives. */
  aspect-ratio: auto 8 / 11;
  object-fit: contain;
  display: block;
  border-radius: 6px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.35);
  /* The card used to carry a pop-in animation and the image was left to
     appear inside it whenever it arrived. Chrome holds a composited layer for
     an element whose animation is sitting in its fill state, and an image that
     loaded after that animation finished painted into a texture nothing
     invalidated: whole screenfuls of cards stayed blank until the cursor
     crossed one and its hover transform forced a repaint.

     So the entrance moved onto the image and runs off its load event, which
     puts the style change that reveals a card after its pixels exist -- a
     repaint the browser cannot skip.

     Whether the card is visible is a plain static style, never something a
     transition or a keyframe has to arrive at. Both need frames to run, and a
     page that is not producing them holds them at their start value -- the
     pixels would be there and the card would still be blank, which is the bug
     this is here to fix. The entrance only moves the card; the worst case is
     one that appears without it. */
  opacity: 0;
  transition: box-shadow 0.13s ease-out;
  /* An <img> is draggable by default, and that native drag beats the rail's
     own. Killing pointer events on it also keeps the click on the button. */
  pointer-events: none;
  -webkit-user-drag: none;
}
.tcg-card img.in {
  opacity: 1;
  animation: card-in 0.22s cubic-bezier(0.25, 1.4, 0.5, 1);
}
/* Transform only. Opacity belongs to .in and stays there, because a frozen
   animation holds its start value: a keyframe that began at opacity 0 would
   hide the card for exactly as long as the page went without a frame. */
@keyframes card-in {
  from { transform: scale(0.93) translateY(7px); }
}
@media (prefers-reduced-motion: reduce) {
  .tcg-card img.in { animation: none; }
}
.tcg-card:hover img { box-shadow: 7px 7px 0 rgba(0, 0, 0, 0.35); }
/* The mirror is missing art for a small number of cards; label the gap. With
   no image there is nothing to set the height, so this one keeps a box of its
   own, and a border, since it has no printed frame either. */
.tcg-card.no-art {
  display: grid;
  place-items: center;
  aspect-ratio: 8 / 11;
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 6px;
  box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.35);
}
.tcg-card.no-art::after {
  content: "No artwork";
  font-family: var(--px);
  font-size: 7px;
  color: var(--ink-dim);
}
.tcg-card.no-art img { display: none; }

/* ---------- Expanded artwork ---------- */

/* A column, not a scroller: the artwork flexes to whatever height is left
   over and the facts keep their place at the bottom. If the dialog itself
   scrolled, the close button (absolutely positioned against it) would scroll
   away with the content. */
.art-modal {
  /* Sized to the artwork: cards are portrait, so a fixed-width dialog is
     mostly empty panel either side of them. */
  width: fit-content;
  min-width: min(340px, calc(100vw - 32px));
  max-width: min(880px, calc(100vw - 32px));
  max-height: calc(100dvh - 48px);
  padding: 0;
  border: 4px solid var(--edge);
  border-radius: 6px;
  background: var(--panel);
  color: var(--ink);
  box-shadow: inset 0 0 0 3px var(--edge-hi), 10px 10px 0 rgba(0, 0, 0, 0.45);
  overflow: hidden;
}
.art-modal[open] { display: flex; flex-direction: column; }
.art-modal::backdrop { background: rgba(8, 6, 4, 0.66); }

.px-btn.art-close, .art-close {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  width: 34px;
  height: 34px;
  padding: 0;
  font-family: var(--vt);
  /* Icon button, same exception as .tcg-rail-arrow: this sizes the glyph. */
  font-size: 24px;
  line-height: 1;
  color: var(--ink);
  background: var(--panel-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  cursor: pointer;
}
.art-close:hover { color: #fff; background: #d3425f; border-color: #47101d; }

.art-frame {
  flex: 1 1 auto;
  /* Without this a flex item refuses to shrink below its content, so the image
     would push the facts off the bottom instead of scaling down. */
  min-height: 0;
  display: grid;
  place-items: center;
  /* Top padding clears the close button (34px tall at top:10, so it ends at
     44). The dialog is sized to the card, so the card reaches both edges and
     anything pinned to the corner lands on the artwork without this. */
  padding: 50px 22px 0;
}
.art-frame img {
  /* Bounded by height as well as width so a tall card still fits above its
     facts on a laptop screen and doesn't push them past the fold. */
  max-width: 100%;
  max-height: min(62dvh, 640px);
  height: 100%;
  object-fit: contain;
  border-radius: 8px;
}

.art-facts { flex: 0 0 auto; padding: 18px 22px 22px; }
.art-title {
  font-family: var(--vt);
  font-size: 26px;
  line-height: 1.2;
  margin-bottom: 12px;
}
.art-meta {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 8px 14px;
  align-items: baseline;
  margin: 0;
}
.art-meta dt {
  font-family: var(--px);
  font-size: 7px;
  color: var(--ink-dim);
  white-space: nowrap;
}
.art-meta dd {
  margin: 0;
  font-family: var(--vt);
  font-size: 20px;
}
.art-link {
  font-family: var(--vt);
  font-size: 20px;
  color: var(--accent);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.art-link:hover { color: var(--ink); }
.art-unknown { color: var(--ink-dim); }
/* What kind of chase card this is, if it is one. */
.art-badge {
  display: inline-block;
  margin-left: 8px;
  padding: 4px 7px 3px;
  font-family: var(--px);
  font-size: 7px;
  vertical-align: middle;
  color: var(--accent-ink);
  background: var(--accent);
  border-radius: 2px;
}

@media (max-width: 720px) {
  .tcg-topbar-wrap .beta-topbar { padding: 16px 14px 14px; }
  /* All of the bar's breathing room in one place: the .tcg-toolbar that holds
     it keeps a 10px bottom of its own on desktop, which on top of the bar's
     14 made the header's bottom 24 against 14 everywhere else. */
  .cc-bar { padding: 14px; }
  .tcg-topbar-wrap .tcg-toolbar { padding-bottom: 0; }
  /* The title spelled out plus a wallpaper button plus Back does not fit a
     phone at full size, and .beta-topbar-actions wraps before it overflows,
     which puts Back on its own line under the wallpaper button. Trimming both
     the title and the two controls keeps the header one row. */
  .tcg-topbar-wrap .brand-logo { font-size: 12px; }
  .tcg-topbar-wrap .wp-btn,
  .tcg-topbar-wrap .beta-back-btn { padding-left: 8px; padding-right: 8px; }
  .tcg-topbar-wrap .wp-btn { gap: 5px; }
  .tcg-toolbar { padding: 0 0 10px; gap: var(--mp-gap); }

  /* Phone layout for the browse bar, in the order a thumb wants it:

       row 1  Pocket toggle . sort . wallpaper . Back
       row 2  the five tabs, stretched edge to edge
       row 3  search, full width

     Ordered with flex `order` rather than moved in the markup, so the desktop
     row and the reading order of the HTML both stay as they were. The tools
     group is split: its two pills go up to row 1 with the header controls,
     its search stays below. */
  .cc-bar { flex-wrap: wrap; row-gap: var(--mp-gap); }

  /* Four equal columns edge to edge, so this row, the tabs under it and the
     search under that all span the same width and read as one block of
     buttons rather than three rows of different lengths. Back is last in the
     markup, which puts it furthest right without needing an auto margin. */
  /* Three text controls share the width; the wallpaper button is a square,
     because all it has to show is its swatch and an equal quarter of the row
     was width the labels needed. Back is last in the markup, which puts it
     furthest right without an auto margin. */
  .cc-mobile-tools {
    display: grid;
    order: 1;
    flex: 1 1 100%;
    grid-template-columns: repeat(3, 1fr);
    /* The row itself is one control tall and every cell stretches to it, so a
       button cannot end up taller than its neighbours because of its own
       padding or border. Setting height per control did not hold: these are
       content-box, so a 3px border made two of them 38 against the pills' 32. */
    grid-auto-rows: var(--cc-h);
    align-items: stretch;
    gap: 4px;
  }
  .cc-mobile-tools > * { min-width: 0; }
  /* Square on a phone: three rows of rounded buttons read as separate
     objects, square ones read as a single slab. One height for every control
     in the row, set here rather than per control, so none of them can drift
     when its own padding changes. */
  .cc-mobile-tools .wp-btn,
  .cc-mobile-tools .beta-back-btn,
  .cc-mobile-tools .cc-pill,
  .cc-tab, .cc-search .tcg-input {
    height: var(--cc-h);
    border-radius: 0;
  }
  .cc-mobile-tools .wp-wrap { display: block; }
  /* Scoped through .cc-bar so this outranks the header's own sizing, which
     still matches these two after they move: .cc-bar sits inside
     .tcg-topbar-wrap, so `.tcg-topbar-wrap .beta-back-btn { height: --mp-h }`
     kept applying and held Back and the wallpaper button at 38 against the
     pills' 32. The vertical padding has to go with it -- it is what made the
     content 38 tall in the first place. */
  .cc-bar .cc-mobile-tools .wp-btn,
  .cc-bar .cc-mobile-tools .beta-back-btn,
  .cc-bar .cc-mobile-tools .cc-pill {
    width: 100%;
    height: var(--cc-h);
    padding-block: 0;
    box-sizing: border-box;
    justify-content: center;
  }
  /* Square: no label inside, so no side padding to make room for one. */
  .cc-mobile-tools .wp-btn { padding: 0; }
  .cc-mobile-tools .beta-back-btn,
  .cc-mobile-tools .wp-btn {
    display: flex;
    align-items: center;
    padding-inline: 4px;
  }
  /* The wallpaper button keeps its swatch and drops its word: four labelled
     controls need 424px of a 335px row, and the swatch already says what the
     button is. Back keeps its short label, which it was built with. */
  .cc-mobile-tools #wpName { display: none; }
  .cc-mobile-tools .wp-btn { gap: 0; }
  /* Nothing in the row may push it wider than the screen; the pills shrink
     their labels before the row overflows. */
  .cc-mobile-tools > * { min-width: 0; }
  /* The two pills carry the row's only long labels -- "TCG Pocket" and
     "Oldest First" -- and at desktop size the four controls want 367px of a
     335px lane. Tightening the pills is what gives Back and the swatch their
     room; they stay readable because they are the only text left. */
  /* No overflow:hidden. Clipping is what silently ate the end of "TCG
     Pocket"; the label has to fit, so the type is sized until it does. */
  .cc-mobile-tools .cc-pill {
    padding-inline: 4px;
    font-size: 6px;
    gap: 3px;
    white-space: nowrap;
  }
  /* The dot is decoration and the label is not; on a phone the label gets the
     room. */
  .cc-mobile-tools .cc-pill-dot { display: none; }
  /* Only the sort pill sheds a word. "TCG Pocket" is the name of a product
     and reads wrong shortened, so it keeps both halves and the tighter type
     below is what buys the room. */
  .cc-mobile-tools #ccSort .cc-pill-long { display: none; }

  /* The five tabs share the full width evenly, so the row lines up with the
     search box under it instead of ending wherever the labels happen to. */
  .cc-tabs {
    order: 2;
    flex: 1 1 100%;
    flex-wrap: nowrap;
    gap: 4px;
  }
  /* "Category" is the widest label and at 8px it filled its box corner to
     corner. Smaller type plus real side padding gives every label air inside
     its own button, which is what makes the row read as buttons rather than
     as a strip of text. */
  .cc-tab {
    flex: 1 1 0;
    justify-content: center;
    min-width: 0;
    padding: 0 6px;
    font-size: 7px;
  }

  .cc-count { order: 3; margin-right: 0; }
  .cc-tools { order: 4; flex: 1 1 100%; }
  .cc-tools .cc-search { flex: 1 1 100%; }
  /* Both pills have moved up to row 1; these are the originals. */
  .cc-tools #ccPocket, .cc-tools #ccSort { display: none; }
  .tcg-lenses, .tcg-searches { padding-inline: 14px; }
  /* Same control height and gap as every other mobile bar. The segmented
     control sizes its own buttons, so the strip takes the height and its
     segments fill it; no need to set each one separately. */
  .tcg-searches { gap: var(--mp-gap); }
  .tcg-lens, .tcg-seg, .tcg-search, .tcg-input,
  .tcg-menu-wrap, .px-btn.tcg-menu-btn,
  /* Back only: the wallpaper button is one of the brand-row squares now and
     takes the collapse arrow's 33px below, not the bar height. */
  .tcg-topbar-wrap .beta-back-btn { height: var(--mp-h); }
  .tcg-segb { height: 100%; }
  /* Three fields on a phone would be ~90px each; let them wrap to two rows
     instead, which keeps every placeholder readable. */
  .tcg-search { flex: 1 1 140px; max-width: none; }
  /* The header is sticky, so every row it keeps costs a row of cards for the
     whole visit. A lens per line came to 260px of a 844px screen; dropping the
     labels puts all three on one line and the controls still say what they are
     ("All / TCG / Pocket", "Oldest / Newest", and a menu button that names the
     current lens). */
  .tcg-lenses { gap: var(--mp-gap); }
  .tcg-lens-label { display: none; }
  .tcg-lens { flex: 1 1 auto; }
  .tcg-seg { flex: 1 1 auto; }
  .tcg-segb { flex: 1 1 0; padding-left: 5px; padding-right: 5px; }
  .tcg-menu-wrap { flex: 1 1 auto; }
  .px-btn.tcg-menu-btn { width: 100%; justify-content: center; padding-left: 6px; padding-right: 6px; }
  .tcg-menu { left: auto; right: 0; width: min(280px, calc(100vw - 40px)); }

  /* The wallpaper popover, on every page whose picker sits in this sticky
     header (Trading Cards and the two placeholders) and not in the dex's
     mobile bar.

     The mobile .wp-pop rule further up stretches the popover to left:0/right:0
     and width:auto. That works on the dex because it also makes .wp-wrap
     static, so the popover anchors to the full-width .mobile-primary. Here
     .wp-wrap keeps position:relative, so left:0/right:0 anchored to the
     BUTTON, an 82px box, and the three columns of swatches spilled straight
     out of it and off the side of the screen.

     Same trick as the dex, different anchor: make the wrapper static so the
     popover resolves against .tcg-topbar-wrap, which is sticky and therefore
     already a containing block, and inset it from both edges. */
  .tcg-topbar-wrap .wp-wrap { position: static; }
  .tcg-topbar-wrap .wp-pop {
    top: calc(100% + 6px);
    left: 12px;
    right: 12px;
    width: auto;
    max-height: 72vh;
    overflow-y: auto;
  }

  .th-name { font-size: 19px; }
  .th-step-b { min-height: 39px; }
  .tcg-input { padding-left: 30px; }
  .tcg-scroll { padding: 14px; padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px)); }

  .tcg-feat-name { font-size: 24px; }
  .tcg-feat-head { margin-bottom: 10px; gap: 8px; }
  .tcg-feat-head-2 { margin-top: 26px; }
  /* The arrows are a mouse affordance; a thumb swipes the rail directly. With
     them gone there is nothing to inset the grid past, and the rail goes back
     to a fixed card width: three columns of a phone's width is narrower than
     the art reads at, and a rail is meant to run off the edge anyway. */
  .tcg-rail-arrow { display: none; }
  .tcg-featured { --cc-gap: 10px; --cc-inset: 0px; }
  .tcg-rail { grid-auto-columns: 132px; }
  #featMonGrid {
    grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
    gap: 12px;
  }
  .tcg-cards { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 12px; }

  .art-modal { width: calc(100vw - 20px); }
  /* Same reason as the desktop rule: the top padding has to clear the close
     button, which sits over the card otherwise. The sides come in to 16. */
  .art-frame { padding: 50px 16px 0; }
  .art-frame img { max-height: 52dvh; }
  .art-facts { padding: 14px 16px 18px; }
  .art-title { font-size: 22px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
  html { scroll-behavior: auto; }
}

/* ---------- "Coming soon" archive pages ----------
   Pokemon Trading Card Game and Smeargle's Art Gallery are in the menu before
   they have any content, so each is one panel sitting on the page's wallpaper.
   They reuse .tcg-topbar-wrap for the header, so the wallpaper picker, the
   companion mute and Back are all in the same places as the real pages. */
.soon-main {
  display: flex;
  justify-content: center;
  align-items: center;
  /* Fill what the sticky header and the credits footer leave, so the one panel
     sits in the middle of the page and not at the top of a tall blank field.
     There is no content below it to hold the layout up. */
  min-height: calc(100vh - 210px);
  padding: 28px 20px;
}
.soon-card {
  max-width: 560px;
  padding: 18px 18px 22px;
  text-align: center;
  background: var(--paper);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.3);
}
/* The art is a scan of printed card stock, halftone dots and all, so it must
   NOT be pixel-snapped: style.css sets image-rendering:pixelated on every
   img globally for the sprites, and that turns the dot screen into moire. */
.soon-art {
  display: block;
  width: 100%;
  height: auto;
  margin: 0 0 16px;
  image-rendering: auto;
  border: 3px solid var(--edge);
  border-radius: 3px;
}
.soon-h {
  margin: 0 0 10px;
  font-family: var(--px);
  font-size: 15px;
  line-height: 1.5;
  color: var(--paper-ink);
}
.soon-sub {
  margin: 0;
  font-size: 17px;
  line-height: 1.55;
  color: var(--paper-ink);
}
.soon-sub a { color: inherit; text-decoration: underline; overflow-wrap: anywhere; }

@media (max-width: 720px) {
  .soon-main { min-height: calc(100vh - 190px); padding: 20px 12px; }
  .soon-h { font-size: 12px; }
  .soon-sub { font-size: 15px; }

  /* These two titles are much longer than the other pages' ("Pokemon Trading
     Card Game" against "Trading Cards"), and at the shared 16px they pushed
     Wallpaper and Back onto a second row. Shrink the wordmark and let it wrap
     instead: the buttons keep their single row, which is the part you tap. */
  .soon-body .brand { min-width: 0; }
  /* The mobile header is the logo and the arrow, nothing else. The title and
     its subheading come off entirely: on a phone the row has to hold the
     wallpaper, Back and the collapse control too, and the page name is already
     in the tab title and the address bar. The filter sections sit underneath,
     which is where they are read from anyway. */
  /* 44px, the same as the dex's own corner logo (.brand-logo-img above, which
     has no mobile override). It used to be 30 here, from when the title and
     subheading still sat beside it. They come off on a phone, so the mark can
     have the room. */
  .brand-mark img { height: 44px; }
  .brand-stack { display: none; }

  /* The wallpaper button keeps its swatch and drops its name, but only in a
     TOPBAR: the dex puts this button in the mobile bar below, where there is
     room for the label, and that one is untouched.

     The bigger mark costs 64px of a 347px row, and these headers carry the
     wallpaper, Back and the collapse arrow. Trading Cards was already breaking
     its buttons across two lines before the mark grew, and went to three after.
     Dropping the name is worth ~45px and gets all three back onto one row, and
     it makes the width fixed, so a long wallpaper name cannot break it again. */
  .beta-topbar .wp-btn #wpName { display: none; }
  .beta-topbar .wp-btn { padding-left: 8px; padding-right: 8px; gap: 0; }
  /* The last 11px Trading Cards needed, taken off the gaps rather than off any
     control. It is the busiest of these headers (wallpaper, Back and the
     arrow); the others have slack and are unaffected. */
  .beta-topbar { gap: 8px; }
  /* nowrap as well as the tighter gap. Trading Cards' three buttons come to
     139px against 140px of room, and a fractional width was enough to break
     one onto a second line even though they fit. */
  .beta-topbar-actions { gap: 6px; flex-wrap: nowrap; }
  /* .brand .brand-logo sets white-space:nowrap for the short titles; these two
     have to be allowed to wrap or "Pokemon Trading Card Game" runs straight
     under the Wallpaper button. Shrinking it to fit one line would need about
     5px. Two lines of 12px is the readable answer. */
  .soon-body .brand .brand-logo {
    font-size: 11px;
    line-height: 1.35;
    white-space: normal;
  }
  .soon-body .beta-topbar-actions { flex-wrap: nowrap; }
}

/* Below 360 the nowrap above has to give. A 199px mark plus Trading Cards'
   three buttons cannot fit a 320px phone, and nowrap turned that into
   horizontal page scroll, which is worse than the wrapped row it replaced. */
@media (max-width: 359px) {
  .beta-topbar-actions,
  .soon-body .beta-topbar-actions { flex-wrap: wrap; }
}

/* ---------- Evil Teams: the boss beside the badge ----------
   Each card opens with a strip in the team's own colour carrying the team
   name (the same shape as the mobile blurb's title bar), then the leader
   and the logo side by side, each captioned. --tac/--tai (accent and its ink)
   are set inline per card by world.js from the build's TEAM_ACCENT table. */
/* Landscape cards: the grid deals these wider columns so the two plates sit
   side by side with air around them instead of being crushed into the 150px
   portrait card the other collections use. */
.beta-grid-team { grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); }
.team-card { padding: 0 0 16px; overflow: hidden; }
.team-strip {
  display: block;
  width: 100%;
  padding: 8px 6px 7px;
  font-family: var(--px);
  font-size: 9px;
  line-height: 1.5;
  text-align: center;
  color: var(--tai, #fff);
  background: var(--tac, var(--edge));
  border-bottom: 3px solid var(--edge);
  margin-bottom: 16px;
  overflow-wrap: anywhere;
}
.team-body {
  display: flex;
  gap: 14px;
  justify-content: center;
  align-items: flex-end;
  width: 100%;
  padding: 0 16px;
}
.team-cell {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
}
/* Each half sits in its own plate, the same framed box the expanded views use,
   so the pair looks like two exhibits, not two floating images. Smooth by
   default and pixelated by exception, the opposite of the global img rule:
   official renders and logos are drawings, only the Game Boy bosses are tiles. */
.team-cell .team-img {
  box-sizing: border-box;
  width: 100%;
  height: 150px;
  padding: 14px;
  object-fit: contain;
  image-rendering: auto;
  background: #fff;
  border: 2px solid var(--edge);
  border-radius: 3px;
}
.team-cell .team-img.px { image-rendering: pixelated; }
/* The caption is the same chip every other collection wears under its cards
   (the grey game chips on Centers and Marts). */
.team-cap {
  font-family: var(--px);
  font-size: 7px;
  line-height: 1.6;
  padding: 4px 6px 3px;
  color: var(--ink-dim);
  background: var(--edge-hi);
  border: 2px solid var(--edge);
  border-radius: 2px;
  white-space: nowrap;
  box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.35);
}

/* The expanded view is the card grown up: the team-name bar becomes the
   dialog's own header strip, and the leader and logo keep their white plates
   and chip captions, just bigger. --tac/--tai are set on the dialog by
   openModal and cleared for every other collection. */
.beta-modal.team-art .beta-modal-body { padding-top: 0; }
.beta-modal.team-art .beta-modal-name {
  width: calc(100% + 48px);        /* Span the body's 24px side padding */
  margin: 0 -24px;
  padding: 13px 40px 12px;         /* The right inset clears the close button */
  background: var(--tac, var(--edge));
  color: var(--tai, #fff);
  border-bottom: 3px solid var(--edge);
}
.beta-modal.team-art .beta-modal-sprites { gap: 20px; margin-top: 6px; }
.beta-modal.team-art .beta-fig { gap: 10px; width: 224px; max-width: 44vw; }
.beta-modal.team-art .beta-fig img {
  box-sizing: border-box;
  width: 100%;
  height: 250px;
  padding: 18px;
  object-fit: contain;
  image-rendering: auto;
  background: #fff;
  border: 2px solid var(--edge);
  border-radius: 3px;
}
.beta-modal.team-art .beta-fig img.px { image-rendering: pixelated; }
/* Same chip the card wears under each plate. */
.beta-modal.team-art .beta-fig figcaption {
  font-family: var(--px);
  font-size: 7px;
  line-height: 1.6;
  padding: 4px 6px 3px;
  color: var(--ink-dim);
  background: var(--edge-hi);
  border: 2px solid var(--edge);
  border-radius: 2px;
  white-space: nowrap;
  box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.35);
}

@media (max-width: 720px) {
  /* One landscape card per row on a phone; two columns would crush them. */
  .beta-grid-team { grid-template-columns: 1fr; }
  .team-cell .team-img { height: 140px; }
  /* Stacked: two 250px plates side by side do not fit a phone. */
  .beta-modal.team-art .beta-modal-sprites { flex-direction: column; align-items: center; }
  .beta-modal.team-art .beta-fig { width: min(260px, 78vw); max-width: none; }
  .beta-modal.team-art .beta-fig img { height: 210px; }
}


/* ---------- Upload a pool: the instructions dialog ---------- */
.tl-upload-modal { width: min(560px, calc(100vw - 32px)); }
.tl-upload-body { padding: 16px 20px 20px; font-size: 16px; line-height: 1.55; color: var(--ink); }
.tl-upload-body p { margin: 0 0 10px; }
.tl-upload-body b { color: var(--ink); font-weight: normal; background: var(--edge-hi); padding: 0 3px; }
.tl-sample {
  margin: 0 0 12px;
  padding: 10px 12px;
  font-family: var(--vt);
  font-size: 15px;
  line-height: 1.6;
  color: var(--paper-ink);
  background: var(--paper);
  border: 2px solid var(--edge);
  border-radius: 3px;
  white-space: pre;
  overflow-x: auto;
}
.tl-tips { margin: 0 0 12px; padding-left: 20px; }
.tl-tips li { margin-bottom: 5px; }
.tl-warn { color: var(--ink-dim); }
.px-btn.tl-choose { display: block; width: 100%; margin-top: 4px; text-align: center; }


/* ---------- Hero sprite: click to view larger ---------- */
.d-hero-btn {
  padding: 0;
  /* The fitted sprite can reach past the 192px box; keep it off the name. */
  overflow: hidden;
  width: 192px;
  height: 192px;
  background: none;
  border: 0;
  cursor: zoom-in;
  display: grid;
  place-items: center;
}
.d-hero-btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 4px; }

.sv-modal {
  padding: 0;
  max-width: min(560px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  border: 4px solid var(--edge);
  border-radius: 4px;
  background: var(--panel);
  color: var(--ink);
  box-shadow: inset 0 0 0 3px var(--edge-hi), 10px 10px 0 rgba(0, 0, 0, 0.45);
  overflow: visible;
}
.sv-modal::backdrop { background: rgba(8, 6, 4, 0.72); }
.sv-modal img {
  display: block;
  width: min(460px, calc(100vw - 72px));
  height: min(460px, calc(100vh - 150px));
  object-fit: contain;
  padding: 22px 22px 8px;
  /* Pixel sprites stay hard-edged at this size; the renders must not be, or
     they gain a jagged staircase from the global img rule. */
  image-rendering: pixelated;
}
.sv-modal img.smooth { image-rendering: auto; }
.sv-cap {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 10px;
  padding: 0 22px 16px;
  font-family: var(--px);
  font-size: 10px;
  line-height: 1.5;
  text-align: center;
}
.sv-cap .sv-label { font-size: 7px; text-transform: uppercase; color: var(--ink-dim); }

@media (max-width: 720px) {
  .sv-modal img { padding: 16px 16px 6px; }
  .sv-cap { font-size: 9px; padding: 0 16px 12px; }
}

/* ==========================================================================
   Game Library (/games)

   The shelf reuses the dex's header, filter band, card frame and dialog
   wholesale; everything here is the part that is genuinely different. A game
   card is box art, which is portrait, landscape or a wide logo depending on
   the era, where a dex card is always a square sprite. So the art box is
   taller than the dex's and every cover is contained inside it instead of
   sized to it.
   ========================================================================== */

/* Wide cards, the Evil Teams shape: a title strip across the top and the art
   side by side underneath. A Pokemon game is very often TWO games sharing one
   entry (Scarlet and Violet, Sword and Shield, Red and Blue), and a portrait
   card had room for one cover, which told half the story. */
.gm-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }

.gm-card {
  padding: 0 0 12px;
  justify-content: flex-start;
  overflow: hidden;
  contain-intrinsic-size: 248px;
}

/* The game's name, in the strip .team-strip draws on the Evil Teams cards.
   NOT tinted per category the way the team strips are tinted per
   team: the wallpaper already colours the page, and three more accents fought
   with it.

   The text colour is a LITERAL, not a variable, and that is deliberate. The
   bar is --edge, which is dark in BOTH palettes, so the text has to be light in
   both, and no palette variable is. --paper is the panel surface and goes
   dark in dark mode (#1b2133); --edge-hi is the inset highlight and goes dark
   too (rgb(58,68,96)). Both were tried and both put near-black text on a
   near-black bar. The team strips on Evil Teams solve it the same way, with a
   literal #fff default; this is the site's cream instead of pure white. */
.gm-strip {
  display: block;
  width: 100%;
  padding: 8px 10px 7px;
  font-family: var(--px);
  font-size: 8px;
  line-height: 1.6;
  text-align: center;
  color: #f8efd8;
  background: var(--edge);
  border-bottom: 3px solid var(--edge);
  /* Titles run long ("Pokemon Mystery Dungeon: Explorers of Time and
     Darkness"), so let them wrap instead of clipping. */
  overflow-wrap: anywhere;
}


/* One fixed height for the art no matter how many covers are in it, so every
   card in a row lines up and a single cover is not blown up to fill the width
   a pair would have used. Covers sit close together: a wide gap made a pair
   read as two unrelated things instead of two versions of one game. */
.gm-art {
  height: 186px;
  width: 100%;
  display: flex;
  gap: 5px;
  /* Centred, not bottom-aligned. Bottom-aligning sat the short wide logos on
     the floor of the box with all the empty space above them, so Winds and
     Waves and Friends looked like they had slipped. */
  align-items: center;
  justify-content: center;
  padding: 16px 14px 0;
  box-sizing: border-box;
}

/* Sized to CONTENT, not to equal halves. Equal halves capped every cover at
   50% of the card whether or not there was anything beside it, so the single
   Switch boxes came out tiny. Now the image grows to the
   art box's full height and the cell takes whatever width that needs. */
.gm-cell {
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The width cap lives here, not on the image. A percentage max-width on the
     img resolves against this cell, and the cell is content-sized, so the
     image was constraining itself and every single cover came out at 64% of
     the size it should have been. The cell resolves its percentage against the
     art box, which has a real width, so the cap behaves.

     It stops a wide logo dwarfing a box art: at full height a 1.7:1
     logo runs nearly 300px against a portrait box's 105px. Portrait art never
     reaches this cap and is untouched. */
  max-width: 53%;
}

.gm-art img {
  max-height: 170px;
  max-width: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  /* Box art is photographic, not pixel art, so it must not inherit the
     pixelated rendering the sprite cards use. */
  image-rendering: auto;
  filter: drop-shadow(2px 2px 0 rgba(0, 0, 0, 0.3));
}

/* A pair has to share the width, so each cover is capped at just under half.
   Portrait boxes hit the height limit first and never reach this; it catches
   the wide logo pairs (Winds and Waves) that otherwise overflow the card. */
.gm-pair .gm-cell { max-width: calc(50% - 3px); }

/* Wordmark logos, on the thirty games that have no box art to show.

   A logo has to be sized against the other cards, not against its own art box.
   Left to fill the box a 1.7:1 logo runs ~290px wide next to a Switch box's
   105px, and the shelf reads as one loud card among quiet ones. Capping the
   width instead of the height keeps them level: a logo is wide, so
   width is the dimension that decides how big it looks, and every one of the
   thirty lands at the same visual weight whatever its own aspect.

   Height also comes down. These are transparent, with no border or spine to
   contain them, so a logo at a box's full height reads considerably larger
   than the box does; the extra room becomes the breathing space around it. */
.gm-art.gm-logo img { max-height: 118px; filter: none; }
.gm-art.gm-logo .gm-cell { max-width: 47%; }
/* .gm-pair is on the CARD, .gm-logo on the art box inside it. That makes them
   a descendant pair, not a compound. Written as one compound this silently
   never matched. */
.gm-pair .gm-art.gm-logo .gm-cell { max-width: calc(43% - 3px); }

/* The handful with no cover in the Archives. A blank card reads as broken, so
   it gets a deliberate placeholder instead. See tools/local_art/ for how to
   supply one by hand. */
.gm-noart {
  width: 104px;
  height: 132px;
  display: grid;
  place-items: center;
  font-family: var(--px);
  font-size: 24px;
  color: var(--ink-dim);
  background: var(--paper-2);
  border: 3px solid var(--edge);
  border-radius: 3px;
  opacity: 0.7;
}

/* No bounce on hover: the dex's sprites bob because they are characters, but
   a box art jiggling reads as a glitch. The card's own lift is enough. */
.gm-card:hover img { animation: none; }

/* Stacked, not one line. Year, then the console logos on their own row, then
   the sales badge if it is switched on. Sharing a single row squeezed the
   logos down to something you could not identify, which defeats the point of
   using logos, not the system's name. */
.gm-foot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 12px 12px 0;
  box-sizing: border-box;
}

.gm-year {
  font-family: var(--px);
  font-size: 9px;
  line-height: 1.6;
  color: var(--ink-dim);
}

.gm-sys-row {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  justify-content: center;
}

/* The console's logo, painted through a MASK instead of drawn as an image.
   Most of these marks are dark line art, which disappears on a dark card; the
   Nintendo Switch ones are a white knockout, which disappears on a light one.
   Masking them and filling with currentColor makes every badge legible in both
   palettes and gives the row one weight instead of twenty brand colours.

   --r is the logo's own aspect ratio, stamped into games.js by
   tools/fetch_game_art.py. Width derives from it so the 8.7:1 Game Boy Advance
   wordmark and the square Switch tile both come out the same HEIGHT. A fixed
   box with object-fit shrank the square ones to a speck. */
/* The chip. Same treatment as .team-cap on the Evil Teams cards, so it reads
   as part of the site, not a new idea. It exists because the logos are
   shown in their true colours: Game Boy's mark is dark grey and disappears on
   a dark card, the Switch's is white on red and disappears on a light one, and
   a neutral light backdrop is the only thing that carries both. */
.gm-sys {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* One height for every chip, whatever shape the logo inside it is. Sizing
     the chip to its contents made the row ragged. A square Switch tile stood
     36px tall next to a 23px Game Boy Advance wordmark. Width still varies
     with the logo, and the ink inside stays area-normalised. */
  height: 36px;
  box-sizing: border-box;
  padding: 0 7px;
  /* LITERAL cream, not --edge-hi, and the same in both palettes. These logos
     are mostly black or near-black ink and cannot recolour, so the surface
     under them has to stay light: --edge-hi goes slate in dark mode, which put
     the black Switch 2 mark at 2.2:1 against its own chip. */
  background: #fffdf3;
  border: 2px solid var(--edge);
  border-radius: 2px;
  box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.35);
}

/* Width and height are computed per logo at build time so every badge holds
   the same AREA of ink. One shared height made the square Switch marks tiny
   against the wide wordmarks; one shared width made the Game Boy Advance mark
   9px tall. See badge_size in tools/fetch_game_art.py. */
.gm-sys img {
  display: block;
  object-fit: contain;
  image-rendering: auto;
  /* Backstop: the inline width/height come from the build, and this stops any
     future logo from bursting the fixed-height chip. */
  max-height: 24px;
}

/* The generic phone and desktop marks. Line art in the card's own ink, so they
   need no chip fill of their own and sit at a fixed size instead of the
   per-logo one the brand marks use. */
.gm-sys-icon { padding: 0 8px; }
.gm-sys-icon svg {
  display: block;
  width: auto;
  height: 22px;
  /* Literal too, for the same reason: the chip below is always cream, so the
     icon on it is always dark ink regardless of palette. */
  color: #2a251a;
}

/* Nintendo Power and the Pixel 4 have no logo on Bulbapedia, so they say their
   name instead of leaving a gap. */
.gm-sys-text {
  font-family: var(--px);
  font-size: 7px;
  line-height: 1.6;
  color: var(--ink-dim);
}

/* Hidden until the Sales toggle is on. Two thirds of the shelf has no
   published figure, so on by default meant most cards showed a gap. */
.gm-sales { display: none; }
body.gm-sales-on .gm-sales {
  display: inline-block;
  padding: 3px 6px 2px;
  font-family: var(--px);
  font-size: 7px;
  color: var(--accent-ink);
  background: var(--accent);
  border: 2px solid var(--edge);
  border-radius: 2px;
}

/* The toggle itself, in the filter band beside the sort. */
.px-btn.gm-metric svg { width: 12px; height: 10px; fill: currentColor; }
.px-btn.gm-metric { display: inline-flex; align-items: center; gap: 7px; }
.px-btn.gm-metric.on { color: var(--accent-ink); background: var(--accent); }

/* ---------- The entry ---------- */

/* The dex's entry is a two-column grid (sprite rail, then tabs). A game has
   one column of prose, so that grid is undone here, not worked around. */
.gm-detail .detail-body {
  display: block;
  padding: 26px 28px 28px;
}

.gm-detail-head {
  display: flex;
  gap: 22px;
  align-items: flex-start;
  flex-wrap: wrap;
}

.gm-arts { display: flex; gap: 12px; flex-wrap: wrap; }

/* Add-on art is 16:9 store banners, not boxes, so it gets its own row and its
   own size instead of being squeezed into the cover rail's proportions. */
.gm-dlc { margin-top: 4px; align-items: flex-start; }
.gm-dlc .gm-art-btn img { max-width: 260px; max-height: 150px; }

/* Never released. Shown in place of the year so a cancelled game reads as
   cancelled, not as missing data. */
/* Sits on its own line under the title but hugs its two words: block for the
   line break, fit-content so the border stops where the text does, auto
   margins to centre what is left. */
.gm-jponly {
  display: block;
  width: fit-content;
  margin: 6px auto 0;
  padding: 2px 5px 1px;
  font-family: var(--px);
  font-size: 6px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #f8efd8;
  border: 1px solid rgba(248, 239, 216, 0.45);
  border-radius: 2px;
}

.gm-art-btn {
  padding: 0;
  background: none;
  border: 0;
  cursor: zoom-in;
  line-height: 0;
}

.gm-art-btn img {
  max-width: 168px;
  max-height: 216px;
  object-fit: contain;
  image-rendering: auto;
  filter: drop-shadow(3px 3px 0 rgba(0, 0, 0, 0.35));
  transition: transform 0.13s ease-out;
}
.gm-art-btn:hover img { transform: translate(-2px, -2px); }

/* No drop shadow on wordmark art, in the entry or on the card.

   `drop-shadow` follows the alpha channel, not the element box. A box art is
   an opaque rectangle, so it reads as one clean offset edge, like a printed
   case. Behind a logo it traces every letterform, so a hard black copy
   sits 3px off each letter, doubling outlines these marks already carry and
   filling the counters of the letters. The bigger the art, the further off it
   sits and the worse it reads, so the entry showed it first.

   None beats a smaller one here: these logos are drawn
   with their own outline and glow, so they already separate from the panel. */
.gm-arts.gm-logo .gm-art-btn img { filter: none; }

.gm-detail-title { flex: 1 1 240px; min-width: 0; }

.gm-detail-title h2 {
  margin: 4px 0 0;
  font-family: var(--px);
  font-size: 15px;
  line-height: 1.6;
  overflow-wrap: anywhere;
}

.gm-cat {
  margin: 0;
  font-family: var(--px);
  font-size: 8px;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.gm-first {
  margin: 10px 0 0;
  font-family: var(--vt);
  font-size: 21px;
  color: var(--ink-dim);
}

.gm-blurb {
  margin: 22px 0 0;
  font-family: var(--vt);
  font-size: 22px;
  line-height: 1.45;
}

.gm-h {
  margin: 26px 0 10px;
  font-family: var(--px);
  font-size: 10px;
  padding-bottom: 8px;
  border-bottom: 2px solid var(--paper-edge);
}

/* Two columns of label/value on desktop, one on a phone. */
.gm-specs, .gm-rels {
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 2px 22px;
}

.gm-spec, .gm-rel {
  display: flex;
  gap: 10px;
  align-items: baseline;
  padding: 6px 0;
  border-bottom: 1px dotted var(--paper-edge);
}

.gm-spec dt, .gm-rel dt {
  flex: 0 0 96px;
  font-family: var(--px);
  font-size: 7px;
  text-transform: uppercase;
  color: var(--ink-dim);
}

.gm-spec dd, .gm-rel dd {
  margin: 0;
  flex: 1 1 auto;
  font-family: var(--vt);
  font-size: 20px;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

/* The date a sales figure was published. It is not optional detail: a 2013
   number and a 2026 number are not comparable, and the shelf sorts on them. */
.gm-asof {
  font-size: 16px;
  color: var(--ink-dim);
}

.gm-source {
  margin: 24px 0 0;
  font-family: var(--vt);
  font-size: 19px;
  color: var(--ink-dim);
}
.gm-source a { color: inherit; }

@media (max-width: 720px) {
  /* One card per row below the breakpoint. Two wide cards side by side on a
     phone would put each pair of covers at about 60px across, which is smaller
     than the portrait card ever was. */
  .gm-grid { grid-template-columns: 1fr; }
  .gm-art { height: 158px; padding: 12px 12px 0; }
  .gm-art img { max-height: 146px; }
  .gm-strip { font-size: 7px; }
  .gm-sys { height: 31px; padding: 0 6px; }
  .gm-sys img { transform: scale(0.85); transform-origin: center; }
  .gm-sys-icon svg { height: 19px; }
  .gm-detail .detail-body { padding: 20px 16px 22px; }
  .gm-detail-head { gap: 14px; }
  .gm-art-btn img { max-width: 116px; max-height: 150px; }
  .gm-detail-title h2 { font-size: 12px; }
  .gm-spec dt, .gm-rel dt { flex-basis: 78px; }
}

/* Separator between two dates for the same region (an original release and a
   later re-release), dimmed so the dates read as the content. */
.gm-sep { color: var(--ink-dim); }

/* ==========================================================================
   Game Library filter bar

   ONE row: search left, category chips centre, Sort + Sales right.

   It was two rows and six filters. System / Series / Generation / Year are gone
   (Sam's call): all four are still reachable from the search box, which
   matches against a game's systems, series and year as well as its name. With
   them went the labels above each control and the "Showing" chip row, both of
   which existed only to explain four selects that all read "Any".

   Every control in the row is the same height, which is not automatic: an
   <input>, a <select> and two kinds of button each resolve their own box from
   font metrics and padding, and they landed 33 / 32 / 33 / 35px apart. One
   variable sets all four, with box-sizing so the 3px borders count inside it.
   ========================================================================== */

body[data-credits="games"] #controls {
  --gm-h: 34px;
  display: flex;
  align-items: center;
  /* 12 and 6 (below), not 14 and 8: at the roomier pair the five chips needed
     704px of a 700px lane on a 1280 laptop and scrolled by four pixels. */
  gap: 12px;
  /* NOWRAP is the point: Sam asked for a single row. The chip group in the
     middle is the flexible one, so it absorbs the slack and, when there is
     none, scrolls instead of pushing Sort onto a second line. */
  flex-wrap: nowrap;
}

body[data-credits="games"] #controls .search-wrap,
body[data-credits="games"] #controls #search,
body[data-credits="games"] #controls .select,
body[data-credits="games"] #controls .px-btn,
body[data-credits="games"] #catTabs .dex-tab {
  height: var(--gm-h);
  box-sizing: border-box;
}
/* The chips and buttons centre their own label once they have a fixed height;
   the vertical padding that used to set that height would now fight it. */
body[data-credits="games"] #catTabs .dex-tab,
body[data-credits="games"] #controls .px-btn {
  display: inline-flex;
  align-items: center;
  padding-top: 0;
  padding-bottom: 0;
}
body[data-credits="games"] #controls .select { padding-top: 0; padding-bottom: 0; }

/* Scoped to this page. Written as a bare `#controls .search-wrap` it also hits
   the dex, whose search sits in a #controls too: at (0,1,1) it outranks the
   base `.search-wrap` rule (0,1,0) and stops the dex's search bar stretching
   across its row. */
body[data-credits="games"] #controls .search-wrap {
  flex: 0 0 200px;
  min-width: 0;
}
/* The magnifier is centred on the input's box, so it has to be measured against
   the height set above instead of the wrapper's auto height. Both are 34px
   here, which makes the inherited top:50% land true. */
body[data-credits="games"] #controls .search-icon { top: 50%; translate: 0 -50%; }

/* The middle group takes the leftover width and centres itself in it, so the
   chips stay visually between search and Sort however many categories there
   are. min-width:0 lets it shrink below its content, and that is what allows the
   scroll, not an overflowing row. */
body[data-credits="games"] #catTabs {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  /* NOWRAP, explicitly. `.dex-tabs` wraps by default, which is right on the dex
     where the tabs own their row. Here it made the chips stack three deep
     inside the scroller the moment Sort widened, and align-items:center then
     centred a three-line block against a one-line search box. */
  flex-wrap: nowrap;
  justify-content: center;
  align-items: center;
  gap: 6px;
  /* The scroller is the fallback for a genuinely narrow window, not the normal
     state. The gaps above are what keep it unused at 1280. */
  overflow-x: auto;
  scrollbar-width: none;
}
body[data-credits="games"] #catTabs::-webkit-scrollbar { display: none; }
body[data-credits="games"] #catTabs .dex-tab { flex: 0 0 auto; }

/* Display controls, not filters: they change how the shelf is shown instead of
   what is on it, so they sit apart at the far end. */
.gm-display {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
}
/* Sort's options are sentences ("Release: Newest First"), so it needs the room.
   210px, measured: the longest is 160px of VT323 at 19px, plus 12 + 30 padding
   (the 30 is the arrow's lane) and 6px of border, leaving 8px of slack. It
   clipped twice on the way here. At the 174px inherited from the old two-row
   bar, and again at 200px once title-casing added 8px to the longest option,
   so re-measure, not eyeball if these labels change again. */
body[data-credits="games"] #controls #sort { width: 210px; }

/* The tablet band's Game Library shape. Sits AFTER the filter bar's own rules
   because the one-row layout above pins the chips to flex: 0 0 auto in a
   nowrap scroller, and a band written earlier in the file loses that argument
   on order: the chip strip shrank to zero width and the category buttons
   vanished. Search and the display controls share the first row; the chips
   run as a full-width slab on the second, each line edge to edge. */
@media (min-width: 721px) and (max-width: 1180px) {
  body[data-credits="games"] #controls { flex-wrap: wrap; }
  body[data-credits="games"] #controls .search-wrap { flex: 1 1 auto; }
  body[data-credits="games"] #controls .gm-display { flex: 0 0 auto; }
  body[data-credits="games"] #controls #catTabs {
    flex: 1 1 100%;
    order: 1;
    overflow: visible;
    flex-wrap: wrap;
  }
  body[data-credits="games"] #catTabs .dex-tab { flex: 1 1 auto; }
}

@media (max-width: 720px) {
  /* The phone layout is the collapsible Filters overlay, where the row becomes
     a stack and there is no leftover width to centre anything in. */
  body[data-credits="games"] #controls {
    flex-wrap: wrap;
    gap: 10px;
  }
  body[data-credits="games"] #controls .search-wrap { flex: 1 1 100%; }

  /* No chips on the phone at all: the category picker is the dropdown on the
     mobile bar now, which names the group you are in, so the Filters overlay
     is down to the two controls that are actually filters here -- Sort and
     Sales. Chips in the overlay would be a second copy of the same switch. */
  body[data-credits="games"] #catTabs { display: none; }

  .gm-display { flex: 1 1 100%; }
  body[data-credits="games"] #controls #sort { flex: 1 1 auto; width: auto; }
}

/* ---------- Set families ----------

   213 sets in one list is a wall, and the date sort made it worse rather than
   better: the Japanese promo runs and the Vending sheets are filed by the era
   they belong to, so Newest First put 1998 cards at the top. One family at a
   time fixes that, and the bar sits above Jump To because it decides what
   Jump To is even listing. */
/* flow-root on the container, or the family bar's top margin collapses
   through it and lands outside the browse pane: the bar sat flush against the
   toolbar however large its margin-top was. */
.cc-browse { display: flow-root; }

.cc-family {
  display: flex;
  flex-wrap: wrap;
  /* Centred: four buttons are the page's top-level choice, and left-aligning
     them read as a filter strip attached to whatever came next. */
  justify-content: center;
  gap: 8px;
  /* Even above and below, measured from the toolbar to the buttons and from
     the buttons to the Jump To panel's own top edge -- the panel has a
     background, so its edge is what the eye reads, not the label inside it.
     The scroll container contributes 33px above and 33 is matched below, which
     measures even at the very top of the page. The toolbar is sticky though,
     so the moment you scroll it slides over that gap and the bar reads as
     crowded from above. The extra 10 buys back what the toolbar takes, at the
     cost of looking slightly airy while pinned to the top -- which is the
     state nobody browses in. */
  /* Measured from the PAINTED toolbar edge, not from .cc-bar. The dark strip
     (.tcg-topbar-wrap) extends 13px below .cc-bar's content box, so every
     "equal" measurement taken from .cc-bar was 13px off and the bar always
     sat visibly closer to the toolbar than to Jump To. The container puts the
     buttons 20px below the painted edge; +6 makes the visible gap 26, equal
     to everything below. */
  margin: 6px 0 26px;
}
.px-btn.cc-family-b {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
/* The active family is the loud one; the rest read as available. */
.px-btn.cc-family-b.on {
  background: var(--accent);
  color: var(--ink-on-accent, #1a1206);
}
.ccf-n {
  font-family: var(--px);
  font-size: 8px;
  opacity: 0.75;
}
/* Two spellings of each family, one shown at a time. The full name carries
   the "English" that makes the four labels answer one question; the short one
   is what fits a phone. aria-label always reads the full name, so nothing is
   lost to a screen reader. */
.ccf-short { display: none; }
@media (max-width: 720px) {
  /* Two per row, full width, so the block lines up with the toolbar above it
     and every button is the same size regardless of its label's length. */
  .cc-family {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px;
    margin: 4px 0 18px;
  }
  .px-btn.cc-family-b {
    justify-content: center;
    gap: 5px;
    min-width: 0;
    padding-inline: 6px;
    font-size: 7px;
    border-radius: 0;
  }
  .cc-family-b .ccf-long { display: none; }
  .cc-family-b .ccf-short { display: inline; }
}

/* Locked on: Pocket is the thing being looked at, so the toggle reports that
   rather than offering a choice it cannot honour. */
.px-btn#ccPocket.locked {
  opacity: 0.75;
  cursor: default;
}

/* ---------- Bebe's corner note ----------

   The chat-box shape people know from every support widget, wearing the
   site's pixel style. Bottom-right because Mew's dialogue box owns
   bottom-centre, and z-indexed under it: if both are ever up, the companion
   that talks wins over the note that waits. */
.bebe-note {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 260;
  width: min(320px, calc(100vw - 36px));
  padding: 14px 16px 16px;
  background: var(--panel);
  border: 3px solid var(--edge);
  border-radius: 4px;
  box-shadow: inset 0 0 0 2px var(--edge-hi), 5px 5px 0 rgba(0, 0, 0, 0.35);
  opacity: 0;
  translate: 0 12px;
  transition: opacity 0.25s ease-out, translate 0.25s ease-out;
}
.bebe-note.shown { opacity: 1; translate: 0 0; }
.bebe-note[hidden] { display: none; }

.bn-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 9px;
}
.bn-avatar {
  width: 42px;
  height: 42px;
  image-rendering: pixelated;
  background: var(--panel-2);
  border: 2px solid var(--edge);
  border-radius: 4px;
}
.bn-name {
  font-family: var(--px);
  font-size: 11px;
  color: var(--ink);
}
.bn-text {
  margin: 0 0 12px;
  font-family: var(--vt);
  font-size: 17px;
  line-height: 1.35;
  color: var(--ink);
}
.bn-send { display: block; text-align: center; text-decoration: none; }
/* Hide sits where the close used to, mirroring the avatar's frame on the
   opposite corner. It collapses the note instead of dismissing it: the note
   folds down to the sprite square plus a Show button, so Bebe stays reachable
   without the message camping on the page. */
.bn-hide {
  position: absolute;
  top: 14px;
  right: 16px;
}
/* The note's own buttons wear the avatar frame's stroke -- 2px edge, 4px
   radius -- instead of the px-btn's 3px, so every box inside the note reads
   as one family. */
.px-btn.bn-hide, .px-btn.bn-show, .px-btn.bn-send {
  border: 2px solid var(--edge);
  border-radius: 4px;
}
.px-btn.bn-hide, .px-btn.bn-show {
  padding: 5px 8px 4px;
  font-size: 8px;
}
.bn-show { display: none; }

/* Collapsed: the container shrinks around the sprite and the Show button.
   Everything that is the message hides; the pair that brings it back stays. */
.bebe-note.min { width: auto; padding: 8px; }
.bebe-note.min .bn-head { margin-bottom: 0; }
.bebe-note.min .bn-name,
.bebe-note.min .bn-text,
.bebe-note.min .bn-send,
.bebe-note.min .bn-hide { display: none; }
/* Collapsed, the button matches the sprite box's full height, so the pair
   reads as two tiles of one bar rather than a picture with a small tag. */
.bebe-note.min .bn-show {
  display: inline-flex;
  align-items: center;
  /* 42, not 42 + borders: the site is border-box, so the sprite's rendered
     box is exactly its 42px, frame included. */
  height: 42px;
  padding: 0 12px;
}

@media (max-width: 720px) {
  .bebe-note { right: 10px; bottom: 10px; }
}
