/* Rawr-o-dex
 *
 * Palette is triangulated from three independent sources, not invented:
 *   - shoprawrs.com computed styles (bg #101015, H2 accent #8CFFFF,
 *     Instrument Serif headings, Inter 300 body, 24px pill radius)
 *   - the shop logo's own pixels (witch-hat purple, aqua creature, gold moon)
 *   - Shoprawrs/Brand & Voice Guide.md ("deep purple, smoky gray/black,
 *     glowing magenta, candlelight gold")
 * Swap the six brand values below and the whole page follows.
 */
:root {
  --ink:        #101015;   /* site background, straight off shoprawrs.com */
  --ink-2:      #16161d;   /* raised surfaces */
  --purple:     #7860a8;   /* the witch hat */
  --purple-lo:  #5b297d;
  --aqua:       #8cffff;   /* the site's own heading accent */
  --aqua-dim:   #60c0c0;   /* the creature */
  --aqua-rim:   #3c9a9a;   /* the flat seal's inset rim */
  --gold:       #f0d878;   /* candlelight */

  --text:       rgba(255,255,255,.92);
  --text-2:     rgba(255,255,255,.77);
  --text-3:     rgba(255,255,255,.48);
  --hair:       rgba(255,255,255,.10);
  --radius:     24px;

  --serif: "Instrument Serif", Georgia, serif;
  --sans:  Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }
/* Any class that sets `display` beats the UA stylesheet's [hidden] rule, so
   `hidden` silently did nothing on .card, .empty-suggest and .empty (the empty
   state's "Show everything" showed under every view). One rule for all. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--ink);
  color: var(--text-2);
  font: 300 16px/1.5 var(--sans);
  overflow-x: hidden;           /* iPhone must never scroll sideways */
  -webkit-font-smoothing: antialiased;
}

/* Faint white star specks over the near-black, so it doesn't read flat -
   no hue, unlike the purple/teal blooms this replaced (owner-approved AI-tell
   cleanup, 2026-09-27). Six layers at odd tile sizes so their one speck each
   never lines up into a visible grid. */
.glow {
  position: fixed; inset: 0; pointer-events: none; z-index: 0;
  background-image:
    radial-gradient(circle at 23px 41px, rgba(255,255,255,.3) 0 .7px, transparent 1.4px),
    radial-gradient(circle at 97px 12px, rgba(255,255,255,.2) 0 .6px, transparent 1.3px),
    radial-gradient(circle at 150px 88px, rgba(255,255,255,.26) 0 .8px, transparent 1.5px),
    radial-gradient(circle at 61px 140px, rgba(255,255,255,.16) 0 .6px, transparent 1.3px),
    radial-gradient(circle at 190px 30px, rgba(255,255,255,.22) 0 .7px, transparent 1.4px),
    radial-gradient(circle at 12px 96px, rgba(255,255,255,.14) 0 .5px, transparent 1.2px);
  background-size: 173px 131px, 211px 167px, 257px 199px, 139px 223px, 307px 181px, 97px 113px;
}

body > * { position: relative; z-index: 1; }

/* Visually hidden but still reachable by a screen reader - the sheet's live
   region announcing ask/save/send state changes lands here. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

a { color: var(--aqua); text-decoration-color: rgba(140,255,255,.35); text-underline-offset: 3px; }

/* ---------- masthead ---------- */

.masthead { text-align: center; padding: 2.4rem 1.25rem .5rem; }

.crest {
  width: 104px; height: 104px; border-radius: 50%;
  display: block; margin: 0 auto .9rem;
  border: 1px solid rgba(255,255,255,.14);
  box-shadow: 0 14px 40px rgba(0,0,0,.6);
  background: #0d0d12;
}

.masthead h1 {
  font: 400 clamp(2.6rem, 11vw, 4.2rem)/1 var(--serif);
  color: #fff;
  margin: 0 0 .35rem;
  letter-spacing: .01em;
}

.tagline {
  font-family: var(--serif);
  font-size: clamp(1.05rem, 4.2vw, 1.4rem);
  color: var(--aqua);
  margin: 0 0 .55rem;
}

.eyebrow { font-size: .8rem; color: var(--text-3); margin: 0; }
/* The link's own text line is .8rem/1.5 = 19.2px tall - well under DESIGN.md's
   44px tap-target floor. padding-block grows the actual hit area to 44px;
   the equal-and-opposite margin-block cancels the layout space that padding
   would otherwise add, so the visible line neither moves nor changes the
   masthead's rhythm (critic fix, 2026-09-28). */
.eyebrow a {
  color: var(--text-3);
  display: inline-flex; align-items: baseline;
  min-height: 44px; padding-block: 12.5px; margin-block: -12.5px;
}
/* Simple Icons' Etsy glyph, inlined (CC0) - sized off the text's own em so it
   scales with it, nudged down from the text baseline to land on its optical
   centre rather than sitting high the way an SVG's own bottom-edge baseline
   otherwise would. fill is set on the <path> itself (see index.html); no
   color rule needed here since currentColor already inherits the link's own
   --text-3 (above). */
.etsy-ico { width: .95em; height: .95em; margin-right: .32em; position: relative; top: .12em; flex: none; }

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

.controls {
  position: sticky; top: 0; z-index: 20;
  padding: .85rem 1rem .7rem;
  margin-top: 1.1rem;
  background: linear-gradient(to bottom, rgba(16,16,21,.97) 74%, rgba(16,16,21,0));
  backdrop-filter: blur(10px);
}

.searchwrap { position: relative; max-width: 34rem; margin: 0 auto .7rem; }

.searchicon {
  position: absolute; left: 1.05rem; top: 50%; transform: translateY(-50%);
  width: 18px; height: 18px; pointer-events: none;
  fill: none; stroke: var(--text-3); stroke-width: 2; stroke-linecap: round;
}

#q {
  width: 100%;
  padding: .82rem 2.6rem .82rem 2.85rem;
  font: 400 16px/1.2 var(--sans);   /* 16px or iOS zooms the page on focus */
  color: var(--text);
  background: var(--ink-2);
  /* Same 3:1 non-text contrast fix as the sheet's field borders, applied here
     too since this is the page's other free-standing input boundary. */
  border: 1px solid rgba(255,255,255,.34);
  border-radius: var(--radius);
  outline: none;
  transition: border-color .16s, box-shadow .16s;
  -webkit-appearance: none;
}
#q::placeholder { color: var(--text-3); }
#q:focus {
  border-color: rgba(140,255,255,.5);
  box-shadow: 0 0 0 4px rgba(140,255,255,.11);
}
#q::-webkit-search-decoration, #q::-webkit-search-cancel-button { -webkit-appearance: none; }

.clear {
  position: absolute; right: .55rem; top: 50%; transform: translateY(-50%);
  width: 30px; height: 30px; border: 0; border-radius: 50%;
  background: rgba(255,255,255,.08); color: var(--text-2);
  font-size: 19px; line-height: 1; cursor: pointer;
}
.clear:hover { background: rgba(255,255,255,.16); color: #fff; }

/* ---------- autocomplete ---------- */

.autocomplete {
  position: absolute; top: calc(100% + .4rem); left: 0; right: 0; z-index: 30;
  margin: 0; padding: .3rem; list-style: none;
  background: #14141b;
  border: 1px solid var(--hair);
  border-radius: 18px;
  box-shadow: 0 18px 46px rgba(0,0,0,.66);
  max-height: 19rem; overflow-y: auto;
}

.ac-item {
  display: flex; align-items: center; gap: .7rem;
  padding: .5rem .65rem; border-radius: 13px;
  cursor: pointer; color: var(--text-2);
}
.ac-item[aria-selected="true"], .ac-item:hover { background: rgba(120,96,168,.26); color: #fff; }
.ac-item img { width: 34px; height: 34px; image-rendering: pixelated; flex: none; }
/* Fixed width so "Pokopia" doesn't push the name past where "#0004"/"Character"
   leave it (tied with "Poké Ball", the untouched .ac-num case, so the column
   already held this length before the Trainer->Character rename). Coloured to
   echo the chips: gold for Pokopia, lilac for Character. */
.ac-num { font-size: .72rem; color: var(--text-3); font-variant-numeric: tabular-nums; min-width: 3.3rem; }
.ac-num.kpo { color: var(--gold); }
.ac-num.trn { color: #b6a0e0; }
.ac-name { flex: 1; font-size: .95rem; }
.ac-tag { font-size: .68rem; letter-spacing: .06em; text-transform: uppercase; color: var(--text-3); }

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

.chips {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: .4rem; max-width: 46rem; margin: 0 auto .45rem;
}

.chip {
  display: inline-flex; align-items: center; gap: .42rem;
  padding: .43rem .9rem;
  font: 400 .82rem/1 var(--sans);
  color: var(--text-2);
  background: var(--ink-2);
  border: 1px solid var(--hair);
  border-radius: 999px;
  cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}
.chip:hover { border-color: rgba(255,255,255,.26); color: #fff; }
.chip.is-on {
  background: rgba(120,96,168,.3);
  border-color: rgba(140,255,255,.42);
  color: #fff;
}
/* The UA's thin blue default ring otherwise stands out against every other
   focus state on the page, which all use the aqua system ring. */
.chip:focus-visible, .clear:focus-visible, .linkish:focus-visible {
  outline: 2px solid var(--aqua); outline-offset: 2px;
}
.chip-sm { padding: .34rem .72rem; font-size: .74rem; }

/* The generation filter's 9 region chips, and the category filter's 4
   (Pokémon/Pokopia/Poké Balls/Trainers), each fold behind their own button -
   too much always-on chrome for filters almost nobody needs once everything
   is already on screen, sectioned (see .section-head below). Both toggles
   share this one component (built once, reused rather than copy-pasted): the
   row holding them, the >=44px hit-box mechanics, and the panel that expands
   INLINE under it. See syncDisclosureLabel/toggleDisclosurePanel/
   closeDisclosurePanel in app.js. */
.filter-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .4rem .6rem; margin: 0 auto; }
.filter-group { display: inline-flex; align-items: center; }

/* .filter-toggle and .filter-clear-btn are real >=44px hit boxes at every
   viewport (2.5.8), not just the ~25-32px their visible chip/circle read as.
   The real button carries no visuals of its own - a small inner element (the
   chip pill, the dot) does the drawing, centered, so the tap target grows
   without the chip looking any chunkier. pointer-events:none on the inner
   element keeps hover/click hit-testing on the real (bigger) button in
   every case. */
.filter-toggle {
  min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
}
.filter-toggle-label { pointer-events: none; }
.filter-toggle[aria-expanded="true"] .filter-toggle-label { background: rgba(120,96,168,.2); border-color: rgba(140,255,255,.3); }
.filter-toggle:hover .filter-toggle-label { border-color: rgba(255,255,255,.26); color: #fff; }
.filter-toggle:focus-visible .filter-toggle-label { outline: 2px solid var(--aqua); outline-offset: 2px; }
/* No .filter-toggle-label.is-on rule needed - the label already carries
   .chip, so the shared .chip.is-on treatment (above) applies as-is when a
   generation or category is picked. */

.filter-clear-btn {
  width: 44px; height: 44px; flex: none; border: 0; border-radius: 50%;
  background: none; color: var(--text-2);
  display: flex; align-items: center; justify-content: center;
  padding: 0; margin: 0; cursor: pointer;
}
.filter-clear-dot {
  display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: rgba(255,255,255,.08);
  font-size: 14px; line-height: 1;
  pointer-events: none;
}
.filter-clear-btn:hover .filter-clear-dot { background: rgba(255,255,255,.16); color: #fff; }
.filter-clear-btn:focus-visible .filter-clear-dot { outline: 2px solid var(--aqua); outline-offset: 2px; }
.filter-panel { opacity: .92; }

.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; }
.dot-stock { background: var(--aqua); box-shadow: inset 0 0 0 1.5px var(--aqua-rim); }
.dot-arch  { background: #8c8c96; }
.dot-none  { background: rgba(255,255,255,.2); }
/* The "Requested" chip wears the same gold heart as an asked card's pip. */
.heart { font-style: normal; font-size: 11px; line-height: 1; color: var(--gold); }

.count {
  text-align: center; font-size: .78rem; color: var(--text-3);
  margin: .15rem 0 .2rem; padding: 0 1rem;
}

.hint {
  display: flex; align-items: center; justify-content: center; gap: .38rem;
  font-size: .76rem; color: var(--text-2);
  margin: 0 0 1.15rem; padding: 0 1rem;
}
.hint svg {
  width: 13px; height: 13px; flex: none;
  fill: none; stroke: var(--gold); stroke-width: 2; stroke-linejoin: round;
}

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

.grid {
  display: grid;
  /* `1fr` as the max, not a fixed cap: auto-fill counts repetitions using the
     MAX track size when that size is definite, so capping at 108px cost a whole
     column on a 390px phone (3 instead of 4). Browsing 1,025 of anything wants
     every column it can get. */
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
  gap: .5rem;
  max-width: 82rem;
  margin: 0 auto;
  padding: 0 .75rem 3rem;
}

/* A handful of results in fluid columns leaves one lonely stretched card on the
   left. Below a threshold, lay them out centred at their natural size instead. */
.grid.sparse {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}
.grid.sparse .card { width: 108px; }
/* Sparse mode is a flex row, so a heading needs its own explicit full-width
   line instead of the grid-column span below. */
.grid.sparse .section-head { flex: 1 0 100%; }

/* One label style for the region headings inside the dex grid AND Pokopia's
   and Trainers' own section headings below it - eleven sections down one
   scroll read as one system, not the old view's lone page title. Sized as a
   running section label (reused from the old compact "search everywhere"
   heading), not a masthead. */
.section-head {
  grid-column: 1 / -1;
  font: 400 1.2rem/1 var(--serif);
  text-align: center;
  color: var(--text-2);
  margin: 1.6rem 0 .6rem;
}
/* The DOM :first-child is always Kanto, but a search or filter can hide it
   while Johto (say) is the first heading actually on screen - apply() moves
   this class to whichever heading that is, every keystroke. */
.grid > .section-head.is-first-visible { margin-top: 0; }
.section-head .n {
  font-family: var(--sans);
  font-size: .62rem;
  font-weight: 300;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  vertical-align: middle;
  margin-left: .3rem;
}

/* The region's main-series games - one small muted line under its heading,
   the same full-width grid row mechanism as .section-head itself (a
   sibling, not a child, so it never joins the heading's accessible name -
   see build()/apply() in app.js). Pulled up against the heading's own
   bottom margin (.6rem) rather than stacking a second gap under it. Matches
   .strip-note's colour (--text-3) so every section's note-style copy reads
   as one system. */
.section-sub {
  grid-column: 1 / -1;
  text-align: center;
  font-size: .72rem;
  color: var(--text-3);
  margin: -.3rem 0 .9rem;
}
/* Sparse mode (a handful of results) turns .grid into a flex row -
   .section-head already gets its own full-width flex line (below); this
   needs the same or it would try to sit inline at its own content width. */
.grid.sparse .section-sub { flex: 1 0 100%; }

.card {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  padding: .5rem .25rem .45rem;
  border: 1px solid transparent;
  border-radius: 16px;
  background: transparent;
  text-decoration: none;
  color: inherit;
  font: inherit;
  cursor: default;
  content-visibility: auto;                 /* 1,025 cards stay cheap */
  contain-intrinsic-size: 118px;
  transition: transform .14s, background .14s, border-color .14s;
}


.card .num {
  font-size: .62rem; color: var(--text-3);
  font-variant-numeric: tabular-nums; letter-spacing: .04em;
}
/* Under the Requested filter a card's number line becomes its ask count
   ("♥ 3"), from the public tally - the one place the count matters. */
.f-requested .card .num[data-asks] { font-size: 0; letter-spacing: 0; }
.f-requested .card .num[data-asks]::before {
  content: "\2665\FE0E\00a0" attr(data-asks);
  font-size: .62rem; letter-spacing: .04em; color: var(--gold);
}

.card img {
  width: 68px; height: 68px;
  image-rendering: pixelated;               /* sprites are pixel art; keep them crisp */
  transition: filter .18s, transform .18s, opacity .18s;
}

.card .nm {
  font-size: .68rem; line-height: 1.25; text-align: center;
  color: var(--text-3);
  max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* --- state: in the shop --- */
.card.in_stock { cursor: pointer; }
.card.in_stock .nm { color: var(--text); }
.card.in_stock:hover, .card.in_stock:focus-visible {
  background: rgba(120,96,168,.2);
  border-color: rgba(140,255,255,.4);
  transform: translateY(-3px);
  outline: none;
}
.card.in_stock:hover img { transform: scale(1.09); }

/* a small flat seal marks a live listing */
.card.in_stock::after {
  content: ""; position: absolute; top: .45rem; right: .5rem;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--aqua); box-shadow: inset 0 0 0 1.5px var(--aqua-rim);
}

/* --- state: made before, not currently listed --- */
.card.archive { cursor: pointer; }
/* Was grayscale(1) brightness(.86) at .62 - too dark to read as "sold", not
   "broken". Owner-approved from a before/after render, 2026-09-28. */
.card.archive img { filter: grayscale(1); opacity: .9; }
.card.archive .nm { color: var(--text-3); }
.card.archive:hover, .card.archive:focus-visible {
  background: rgba(255,255,255,.055);
  border-color: rgba(255,255,255,.2);
  transform: translateY(-3px);
}
.card.archive:hover { outline: none; }
.card.archive:hover img { opacity: .95; }

/* A gray status dot, same geometry as the in-stock aqua pip above, so a
   sold-out card reads "state at a glance" the same way an available one
   does instead of relying on the dimmer sprite alone. */
.card.archive::after {
  content: ""; position: absolute; top: .45rem; right: .5rem;
  width: 8px; height: 8px; border-radius: 50%;
  background: #8c8c96;
}

/* --- state: never made --- */
/* brightness(0) flattens the sprite to a shape, invert(1) makes that shape
   white so it reads on the near-black ground. That is the classic
   "who's that Pokémon?" silhouette, done with one filter and no extra assets. */
.card.none img { filter: brightness(0) invert(1); opacity: .17; }
/* Was .24, which measured 2.13:1 against the ground - under WCAG AA's 4.5:1,
   on the name under 868 of the 1,025 cards. .46 measures ~5:1 and still reads
   as clearly receded next to the .92 of an available one. */
.card.none .nm { color: rgba(255,255,255,.46); }
.card.none:hover img { opacity: .3; }

/* Poké Balls override the silhouette above: every ball is the same round
   shape, so brightness(0)+invert(1) turns all 26 unmade ones into identical
   gray dots with no way to tell Quick Ball from Timer Ball. Dimming the real
   sprite instead keeps each one's pattern readable - a third, lighter state
   next to sold-out's grayscale(1)/.9 (higher specificity than .card.none img
   above, so this wins outright rather than layering). .45 measured too close
   to sold-out's .9 to read as a clearly distinct step at 60px - raised to .6
   (hover .75) so each ball's pattern stays legible while staying clearly
   below both sold-out and in-stock (critic fix, 2026-09-28). */
.card.ball.none img { filter: grayscale(1); opacity: .6; }
.card.ball.none:hover img { opacity: .75; }

/* A visible keyboard-focus ring for every card state, including `.card.none`
   (which had none) and `.card.archive` (whose own focus-visible style is a
   border-color change too low-contrast to count as one - see the border
   split above). */
.card:focus-visible { outline: 2px solid var(--aqua); outline-offset: 2px; }

.card.flash { animation: flash 1.5s ease-out; }
@keyframes flash {
  0%, 30% { background: rgba(240,216,120,.3); border-color: var(--gold); }
  100%    { background: transparent; border-color: transparent; }
}

/* This device already asked for it - a small gold pip, same corner and
   ::after trick as the in-stock pip above, never both at once since asked
   only ever applies to a none/archive card. A sold-out card is BOTH
   .archive and .asked at once though, and both use ::after - this rule sits
   after .card.archive::after in the cascade and has equal specificity, so it
   wins on every shared property, but the geometry ones (width/height/
   border-radius/background) must be named here too or the archive dot's
   circle leaks through behind the heart glyph. */
.card.asked::after {
  content: "\2665\FE0E";   /* text-presentation selector: no red emoji heart on iOS */
  position: absolute; top: .38rem; right: .48rem;
  width: auto; height: auto; border-radius: 0; background: none;
  font-size: 11px; line-height: 1; color: var(--gold);
}

/* ---------- strips: Pokopia + trainers ---------- */

.strip {
  max-width: 82rem;
  margin: 0 auto 1.5rem;
  padding: 1.1rem 1rem 1.2rem;   /* matches the page's 16px gutter, not a narrower one */
  border-top: 1px solid var(--hair);
  border-bottom: 1px solid var(--hair);
}
.strip-note { text-align: center; font-size: .75rem; color: var(--text-3); margin: 0 0 1rem; }

.strip-gold { background: rgba(240,216,120,.028); }
.strip-gold h2 { color: var(--gold); }
.strip-purple { background: rgba(120,96,168,.06); }
.strip-purple h2 { color: var(--purple); filter: brightness(1.5); }
.strip-aqua { background: rgba(96,192,192,.045); }
.strip-aqua h2 { color: var(--aqua-dim); }

/* Pokopia variants: gold, like the shop's candlelight accent. */
.card.kpo .num { color: var(--gold); font-size: .53rem; letter-spacing: .09em; text-transform: uppercase; }
.card.kpo:hover { background: rgba(240,216,120,.12); border-color: rgba(240,216,120,.45); }
.card.kpo .nm { font-size: .64rem; white-space: normal; line-height: 1.2; }

/* Unstocked Pokopia cards silhouette like the dex (Shahrum, 2026-09-27: only
   what Heather has made shows in colour). They used to dim their art instead. */

/* Peakychu has no artwork of its own: four generations failed to give it the
   drooping ears and spectral wash, and this tint over the real Pikachu sprite
   reads better than any of them did. */
.card.tint-moss  img { filter: hue-rotate(48deg) saturate(1.5) brightness(.92); }
.card.tint-ghost img { filter: brightness(1.7) saturate(.25) drop-shadow(0 0 7px rgba(200,235,255,.75)); }
.card.tint-paint img { filter: hue-rotate(190deg) saturate(2.1); }
.card.tint-prof  img { filter: hue-rotate(-25deg) saturate(1.3); }

/* Trainers have no sprite at all, so they get a monogram disc. */
.trainer-row {
  display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem;
  max-width: 60rem; margin: 0 auto;
}
.card.trn { width: 100px; }
.card.trn .num { color: var(--text-3); font-size: .6rem; letter-spacing: .1em; }
.card.trn .nm { font-size: .66rem; white-space: normal; line-height: 1.2; color: var(--text); }
.card.trn:hover { background: rgba(120,96,168,.22); border-color: rgba(140,255,255,.35); }

.tr-face {
  width: 52px; height: 52px; margin: .3rem 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: linear-gradient(150deg, rgba(120,96,168,.85), color-mix(in srgb, var(--purple-lo) 85%, transparent));
  border: 1px solid rgba(255,255,255,.16);
  font: 400 1.4rem/1 var(--serif); color: #fff;
}
.tr-face-lg { width: 72px; height: 72px; font-size: 2rem; flex: none; }
.card.trn.archive .tr-face { filter: grayscale(1); opacity: .9; }

/* Poké Balls: real 30x30 bag sprites, shown at 2x so they read clearly next
   to the dex/Pokopia/trainer cards' bigger sprites - a fixed size rather
   than the 68/78px the other cards scale to, since doubling a 30px source
   further would blur it. */
.card.ball img { width: 60px; height: 60px; }
.card.ball .nm { font-size: .66rem; }
/* Ball cards carry no visible .num kicker text ("POKÉ BALL" said nothing
   inside a section where every card is already a ball) - but the .num span
   itself still renders empty (buildStrip()), sized the same as the old
   kicker, so the row's height/icon position matches a Pokopia or Trainer
   card without a separate padding hack, AND applyRequested()/relabel() -
   which hang the "♥ N requested" count and aria-label on .num for every
   card kind - keep working here too (critic fix, 2026-09-28: ball cards
   never showed their ask count under Requested before this). */
.card.ball .num { font-size: .53rem; letter-spacing: .09em; }

/* The three category chips (Pokopia, Poké Balls, Trainers) are coloured like
   the sections they open, so the selector reads as gens-then-extras. */
.chip-pokopia { border-color: rgba(240,216,120,.35); color: var(--gold); }
.chip-pokopia.is-on { background: rgba(240,216,120,.2); border-color: var(--gold); color: #fff; }
/* Poké Balls: Creature Aqua, the one primary/secondary brand hue Pokopia
   (gold) and Trainers (purple/lilac) hadn't already claimed - not the full
   Spirit Aqua used for "live" seals and links (The Seal Means Live Rule),
   so a balls-category tint never reads as an in-stock signal. */
.chip-balls { border-color: rgba(96,192,192,.4); color: var(--aqua-dim); }
.chip-balls.is-on { background: rgba(96,192,192,.22); border-color: var(--aqua-dim); color: #fff; }
.chip-trainers { border-color: rgba(160,130,215,.4); color: #b6a0e0; }
.chip-trainers.is-on { background: rgba(120,96,168,.42); border-color: #b6a0e0; color: #fff; }

/* As a section stacked below the grid rather than a separate view, no
   dividing rules of its own - its .section-head (above) already matches
   the dex regions above it. */
.strip { border: 0; background: none; padding-top: .2rem; }
/* #pokopia/#trainers reuse .grid for layout, whose padding-bottom (3rem) is
   sized for the page's own bottom edge - inside a .strip it just stacked an
   extra gap before the next section, on top of the strip's and heading's own
   spacing (measured: ~120px before "Characters" vs. ~34px between
   two dex regions). */
.strip .grid { padding-bottom: 0; }
/* A strip immediately followed by another one (Pokopia -> Trainers) doesn't
   need its own trailing padding/margin too - that's what stacked the
   remaining ~72px on top of the shared heading's own margin-top (measured:
   the trailing padding-bottom+margin-bottom below Pokopia, PLUS the next
   strip's padding-top, on top of the next heading's own 1.6rem). A strip
   with nothing after it (Characters, today) keeps its own spacing untouched -
   this only tightens the seam BETWEEN two strips. */
.strip:has(+ .strip) { padding-bottom: 0; margin-bottom: 0; }

/* The four availability chips must hold one line on a phone - they are the
   primary control and a wrapped second row pushes the grid down. Tightened
   rather than renamed, so the labels stay readable. Measured at 360px, the
   narrowest phone worth targeting. */
@media (max-width: 560px) {
  .chips-status { flex-wrap: nowrap; gap: .28rem; padding: 0 .1rem; }
  .chips-status .chip {
    padding: .42rem .58rem;
    font-size: .75rem;
    gap: .28rem;
    white-space: nowrap;
    /* One line was easy; a 27px-tall chip is not a comfortable thumb target.
       The row had ~70px of horizontal slack at 360px, so height is the free
       axis - spend it here rather than shrinking the labels further. */
    min-height: 38px;
  }
  .chips-status .dot { width: 6px; height: 6px; }
  .chips-status .dot-stock { box-shadow: inset 0 0 0 1px var(--aqua-rim); }
  /* The gen/Pokopia/Trainers row is the same thumb target as the status
     chips above it; at ~24.7px tall it sat under the 2.5.8 floor. */
  .chip-sm { min-height: 32px; }
  /* Five chips, the last one "Requested": spend padding and a hair of type
     size, not a second line. Measured to fit 360px; "All" stays ~29px wide. */
  .chips-status { gap: .16rem; }
  .chips-status .chip { padding-inline: .4rem; font-size: .72rem; }
}

/* 360-369px phones (most small Androids) need a few px more. */
@media (max-width: 369px) {
  .chips-status { gap: .12rem; }
  .chips-status .chip { padding-inline: .34rem; }
}

/* Below 360px even the tightened padding above runs out of room - drop the
   Requested chip to a heart-only target. aria-label (see index.html) keeps its name. */
@media (max-width: 359px) {
  .chip[data-status="requested"] .lbl { position: absolute; clip-path: inset(50%); }
}

/* ---------- embed mode ---------- */

/* Dropped into a modal on shoprawrs.com: no crest, no title, search at the top. */
body.embed .masthead { display: none; }
body.embed .controls { margin-top: 0; padding-top: .75rem; }
body.embed .foot .legend { margin-bottom: .5rem; }

/* ---------- picker sheet ---------- */

body.locked { overflow: hidden; }

.sheet {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: flex-end; justify-content: center;
  background: rgba(6,6,10,.72);
  backdrop-filter: blur(3px);
  animation: fade .16s ease-out;
}
@keyframes fade { from { opacity: 0; } }

.sheet-inner {
  position: relative;
  width: 100%; max-width: 30rem;
  max-height: 86vh; overflow-y: auto;
  background: #14141b;
  border: 1px solid var(--hair);
  border-radius: 22px 22px 0 0;
  padding: 1.35rem 1.15rem calc(1.35rem + env(safe-area-inset-bottom));
  animation: rise .2s cubic-bezier(.2,.8,.3,1);
}
@keyframes rise { from { transform: translateY(18px); } }

/* Bottom scroll affordance for a sheet taller than its own 86vh cap (app.js's
   updateOverflowCue toggles this once on open and again on scroll) - a soft
   fade over the last ~36px says "more below" instead of letting a sold-out
   sheet's "ask for more" CTA vanish past the fold with no cue. Cleared once
   scrolled within a few px of the end. */
.sheet-inner.has-more {
  mask-image: linear-gradient(to bottom, #000 calc(100% - 36px), transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 36px), transparent 100%);
}

.sheet-x {
  position: absolute; top: .8rem; right: .8rem;
  width: 32px; height: 32px; border: 0; border-radius: 50%;
  background: rgba(255,255,255,.08); color: var(--text-2);
  font-size: 20px; line-height: 1; cursor: pointer;
}
.sheet-x:hover { background: rgba(255,255,255,.17); color: #fff; }

.sheet-head { display: flex; align-items: center; gap: .9rem; margin-bottom: 1.1rem; }
.sheet-head img { image-rendering: pixelated; flex: none; }
.sheet-head img.is-sold { filter: grayscale(1); opacity: .9; }
.sheet-head h2 { font: 400 1.7rem/1 var(--serif); color: #fff; margin: .1rem 0 .25rem; }
.sheet-num { font-size: .68rem; color: var(--text-3); margin: 0; font-variant-numeric: tabular-nums; }
.sheet-sub { font-size: .78rem; color: var(--aqua); margin: 0; }
.sheet-sub-muted { color: var(--text-2); }

.sheet-list { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: .4rem; }

/* :not(.it-ask) on both: .it-ask is itself an <a> nested inside .sheet-list
   now (a real link - see againButtonHtml in app.js), and this selector's
   (0,1,1) specificity would otherwise beat .it-ask's own (0,1,0) rule below,
   turning the 44px circular control into a full bordered grid row. */
.sheet-list a:not(.it-ask), .sheet-list .it-dead {
  display: grid; grid-template-columns: 68px 1fr; align-items: center; gap: .65rem;
  padding: .68rem .8rem;
  border: 1px solid var(--hair);
  border-radius: 14px;
  text-decoration: none;
  background: rgba(255,255,255,.03);
}
.sheet-list a:not(.it-ask):hover { background: rgba(120,96,168,.26); border-color: rgba(140,255,255,.4); }
/* Was `opacity: .55` on the whole row, which dropped `.it-meta` under 4.5:1
   (about 3.4:1, 2.4:1 on a dead item). Dim the title only; `.it-meta` gets
   its own readable color per state instead. */
.sheet-list a.is-out .it-title, .sheet-list .it-dead .it-title { color: var(--text-2); }
.sheet-list a.is-out .it-meta { color: var(--text-3); }

.it-text { display: flex; flex-direction: column; gap: .2rem; min-width: 0; }
.it-title { font-size: .84rem; color: var(--text); line-height: 1.3; }
.it-meta  { font-size: .72rem; color: var(--aqua-dim); }
.it-dead .it-meta { color: var(--text-3); }

/* The photo, or an empty box holding its place so text still lines up when
   a listing has none. `.it-thumb-empty` never gets a real src, so it never
   requests i.etsystatic.com - the sheet's rows only exist once it's open. */
.it-thumb {
  width: 68px; height: 54px; flex: none;
  border-radius: 10px; object-fit: cover;
  background: var(--ink-2);
}
.it-thumb-empty { display: block; }
.sheet-list a.is-out .it-thumb, .sheet-list .it-dead .it-thumb { opacity: .55; }

.sheet-shop {
  display: block; text-align: center;
  font-size: .78rem; color: var(--text-3); text-decoration: none;
  padding: .5rem;
}
.sheet-shop:hover { color: var(--aqua); }

/* A not-buyable row's link/.it-dead and its "ask again" control are siblings
   (see sheetListRow in app.js), laid out side by side; the row keeps its own
   grid layout from `.sheet-list a, .sheet-list .it-dead` above and just
   shrinks to share the line with the control. `.it-ask` is itself a direct-
   child <a> now (a real link, not a button - see againButtonHtml), so this
   must explicitly exclude it: `.sheet-row > a`'s (0,1,1) specificity would
   otherwise beat `.it-ask`'s own `flex: none` (0,1,0) and stretch it to fill
   the row instead of staying a fixed 44px circle. */
.sheet-row { display: flex; align-items: center; gap: .2rem; }
.sheet-row > a:not(.it-ask), .sheet-row > .it-dead { flex: 1; min-width: 0; }

/* Same two-layer shape as .filter-clear-btn/.filter-clear-dot: a real 44px
   hit box with no visuals of its own, holding a small always-visible tinted
   circle - colored like the rest of the sheet's secondary text (--text-3)
   rather than the aqua "live" or gold "yours" accents, since this is
   neither. A background on the full 44px control (rather than a smaller
   inner circle) read far heavier than that established convention once up
   to 8 of these stack in one sheet - critic fix, 2026-09-28. It's a real
   <a href> (see againButtonHtml in app.js), so text-decoration/color need
   their own reset from the global `a` rule near the top of this file. */
.it-ask {
  width: 44px; height: 44px; flex: none; border: 0; border-radius: 50%;
  background: none; color: var(--text-3); text-decoration: none;
  display: flex; align-items: center; justify-content: center;
  padding: 0; margin: 0; cursor: pointer;
}
.it-ask-dot {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 50%;
  background: rgba(255,255,255,.08);
  pointer-events: none;
}
.it-ask:hover .it-ask-dot { background: rgba(255,255,255,.16); color: #fff; }
.it-ask:focus-visible .it-ask-dot { outline: 2px solid var(--aqua); outline-offset: 2px; }
.it-ask-ico {
  width: 17px; height: 17px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}

/* Empty until a tap on .it-ask fills it in (see showAgainToast) - present
   from the sheet's first paint, same as .sr-live, so a screen reader is
   already watching this role="status" region when its text changes.
   `position: sticky` pins it to the bottom of .sheet-inner's own scrollport
   (its nearest scrolling ancestor, per the CSS spec sticky insets resolve
   against that container's PADDING box) rather than scrolling away with the
   rows above it - critic fix, 2026-09-28: on an 8-row sheet (Bulbasaur) the
   toast sat far below the fold with no auto-scroll, AND the shop opens in a
   new tab that covers the page, so a visitor switching back after visiting
   Etsy never saw it either way. Sticky needs no JS and stays put through
   both. Because .sheet-inner's own padding already reserves
   `calc(1.35rem + env(safe-area-inset-bottom))` at the bottom, `bottom: 0`
   here lands the toast just above that reserved strip for free - no extra
   safe-area math needed on this element. A solid background (not the usual
   translucent .03) stops rows scrolling underneath from showing through. */
.again-toast {
  position: sticky; bottom: 0; z-index: 1;
  text-align: center; font-size: .78rem; color: var(--text-2);
  margin: .6rem 0 0; padding: .6rem .8rem;
  border: 1px solid var(--hair); border-radius: 14px;
  background: #14141b;
}
.again-toast:empty { display: none; margin: 0; padding: 0; border: 0; }
/* The clipboard-failure fallback: the message itself, styled to invite a
   tap-and-hold "Select All" instead of sitting there as inert-looking text
   (critic fix, 2026-09-28 - see showAgainToast). */
.again-toast-copy { display: block; -webkit-user-select: all; user-select: all; }
.again-toast-hint { display: block; margin-top: .3rem; font-size: .68rem; color: var(--text-3); }

.card[data-picker], .card[data-ask] { cursor: pointer; }

/* ---------- ask / notify / suggest ---------- */

.field { display: flex; flex-direction: column; gap: .32rem; margin-bottom: .85rem; }
.field label { font-size: .78rem; color: var(--text-2); }
.field input, .field textarea {
  padding: 0 1rem; height: 3.1rem;
  font: 400 16px/1.2 var(--sans);   /* 16px or iOS zooms the page on focus */
  color: var(--text);
  background: var(--ink-2);
  /* Was .14 (~1.5:1 against the sheet) - under the 3:1 the 1.4.11 non-text
     contrast check wants for a form field's boundary. */
  border: 1px solid rgba(255,255,255,.34);
  border-radius: 14px;
  outline: none;
}
.field textarea { height: auto; padding: .75rem 1rem; line-height: 1.4; resize: none; }
.field input:focus, .field textarea:focus {
  border-color: rgba(140,255,255,.5);
  box-shadow: 0 0 0 4px rgba(140,255,255,.11);
}
.field input[aria-invalid="true"], .field textarea[aria-invalid="true"] {
  border-color: #ff9f9f;
}
.field-error { text-align: left; font-size: .74rem; color: #ff9f9f; margin: .3rem 0 0; }

.btn-primary {
  display: flex; align-items: center; justify-content: center; gap: .5rem;
  width: 100%; height: 3.375rem; margin-bottom: .6rem;
  border: 0; border-radius: 999px;
  background: var(--aqua); color: var(--ink);
  font: 600 1rem/1 var(--sans); cursor: pointer;
}
.btn-primary:hover:not(:disabled) { filter: brightness(1.06); }
.btn-primary:disabled { opacity: .45; cursor: default; }
/* A busy button (mid-request) is neither disabled-forever nor clickable -
   distinct styling so "Asking…" doesn't read the same as a dead control. */
.btn-primary[aria-busy="true"] { opacity: .7; cursor: progress; }

.btn-ghost {
  display: block; width: 100%; min-height: 2.75rem;
  background: none; border: 0; padding: 0;
  color: var(--text-2); font: inherit; font-size: .9rem; cursor: pointer;
}
.btn-ghost:hover { color: #fff; }

.btn-outline {
  height: 3.1rem; padding: 0 1.75rem; margin-top: .4rem;
  border: 1px solid rgba(140,255,255,.42); border-radius: 999px;
  background: rgba(120,96,168,.3); color: #fff;
  font: 500 .95rem/1 var(--sans); cursor: pointer;
}
.btn-outline:hover { border-color: var(--aqua); }

.ask-intro { font-size: .78rem; color: var(--text-2); margin: 0 0 .6rem; }

.ask-row {
  display: flex; align-items: center; gap: .6rem;
  padding: .7rem .8rem; margin-bottom: .9rem;
  border: 1px solid var(--hair); border-radius: 14px;
  background: rgba(255,255,255,.03);
  font-size: .88rem; color: var(--text);
}
.ask-row-done { border-color: rgba(240,216,120,.38); background: rgba(240,216,120,.08); }
.ask-heart {
  width: 19px; height: 19px; flex: none;
  fill: none; stroke: var(--aqua-dim); stroke-width: 2; stroke-linejoin: round;
}
/* fill uses currentColor in the markup so the six brand tokens re-skin this
   without touching inline SVG attributes. */
.ask-heart-gold { stroke: none; color: var(--gold); }

/* interaction-only Turnstile stays invisible until it actually needs to
   challenge someone, so it takes no space by default. */
.ask-turnstile:empty { margin: 0; }
.ask-turnstile { margin-bottom: .4rem; }

.sheet-thanks-h { font: 400 1.5rem/1.15 var(--serif); color: #fff; margin: 0 0 .3rem; }
.sheet-thanks-p { font-size: .89rem; color: var(--text-2); margin: 0 0 .9rem; }
.sheet-fine { text-align: center; font-size: .74rem; color: var(--text-3); margin: -.25rem 0 0; }
.sheet-error { text-align: center; font-size: .82rem; color: #ff9f9f; margin: .6rem 0 0; }

.sheet-done {
  display: flex; flex-direction: column; align-items: center; gap: .6rem;
  text-align: center; padding: .5rem 0 .25rem;
}
/* stroke uses currentColor in the markup; set here instead of inline hex so
   the six brand tokens re-skin this too. */
.sheet-done svg { color: var(--aqua); }
.sheet-done h3 { font: 400 1.55rem/1.1 var(--serif); color: #fff; margin: 0; }
.sheet-done p { font-size: .89rem; color: var(--text-2); margin: 0; }

@media (min-width: 640px) {
  .sheet { align-items: center; }
  .sheet-inner { border-radius: 22px; }
}

/* ---------- footer ---------- */

/* A no-match search used to leave #empty ~560px down the page (a 48px empty
   grid plus this section's own padding), well below the fold once the phone
   keyboard is up. Cut the padding and let the panel below be the hero of the
   remaining space instead of a small block adrift in it. */
.empty {
  display: flex; flex-direction: column; align-items: center; gap: 1.1rem;
  text-align: center; color: var(--text-3); padding: .75rem 1rem 2rem;
}
.empty-msg { margin: 0; font-size: .78rem; color: var(--text-2); }

.empty-suggest {
  width: 100%; max-width: 26rem; box-sizing: border-box;
  min-height: min(46svh, 24rem);
  padding: 1.4rem 1.25rem; border-radius: 20px;
  background: rgba(120,96,168,.14); border: 1px solid rgba(140,255,255,.22);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .6rem;
}
.empty-suggest h2 { font: 400 1.9rem/1.1 var(--serif); color: #fff; margin: 0; }
.empty-suggest p { margin: 0; font-size: .86rem; color: var(--text-2); }
.empty-suggest .btn-primary { width: auto; padding: 0 1.6rem; margin: .3rem 0 0; }

.linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--aqua); font: inherit; text-decoration: underline; text-underline-offset: 3px;
}

.foot {
  border-top: 1px solid var(--hair);
  padding: 1.6rem 1.25rem 3rem;
  text-align: center;
}

.legend {
  display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem 1.4rem;
  font-size: .76rem; color: var(--text-3); margin: 0 0 1rem;
}
.legend span { display: inline-flex; align-items: center; gap: .42rem; }

/* The copyright and fan-art notice lives here. It was .3 (2.66:1) - the one
   paragraph on the page that most needs to be readable was the least legible. */
.fine { font-size: .68rem; color: var(--text-3); margin: .3rem 0; }

@media (min-width: 640px) {
  .grid { grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: .6rem; }
  .card img { width: 78px; height: 78px; }
  .card .nm { font-size: .72rem; }
}

/* On a small phone the masthead + controls took 88.6% of a 320px-tall-viewport
   screen, so the standalone page opened with ZERO Pokemon visible and no cue
   that a grid existed below. Shrink the crest and tighten the padding so the
   first row peeks above the fold. Embed mode drops the masthead entirely and
   is unaffected. */
@media (max-width: 480px) {
  .masthead { padding: 1.2rem 1.25rem .4rem; }
  .crest { width: 72px; height: 72px; margin-bottom: .6rem; }
  .masthead h1 { font-size: clamp(2.1rem, 9vw, 2.6rem); }
  .tagline { font-size: 1rem; margin-bottom: .4rem; }
  .controls { margin-top: .7rem; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
