/* 30th Celebration — soft, round and built for a phone first. */

/* ---------- the colours ----------

   Every colour in this file comes from one of the custom properties below, and a
   palette is a block that redefines them. Nothing else changes: there is exactly
   one set of component rules, and ten sets of ten values.

   The palette blocks are plain attribute selectors rather than `:root[…]`, so
   they also work partway down the tree. That is what lets each swatch in the
   preferences menu carry `data-palette` and paint itself in the real colours of
   the palette it offers, instead of a copy kept in step by hand.

   Specificity is identical between `:root` and `[data-palette="…"]`, so the
   palettes must stay *below* this block — order is what decides them.

   Contrast is not a matter of taste here. Anything carrying meaning clears 4.5:1
   against what it sits on, `--ink-soft` included, in all ten. The tightest in
   each palette is noted beside it. */

:root,
[data-palette="cream"] {
  --cream: #fff8f3;
  --paper: #ffffff;
  --ink: #3f3336;
  /* Carries real information — card numbers, English names, counts — so it has to
     clear 4.5:1 on cream. The old value sat at 3.7:1 and was the most-read text
     in the app. */
  --ink-soft: #6b5a5e;
  /* Decoration only: never put words that matter in this. */
  --ink-faint: #9a8b8e;
  --line: #f2e3dc;
  --edge: #d9c5bd;
  /* The colour that fills a button or a ring. Named for what it was, and it still
     is here; in the other palettes it is whatever leads them. */
  --pikachu: #ffcb05;
  /* The ink written *on* that fill. A separate value because a pale fill needs a
     dark word on it and the fill changes from palette to palette. */
  --on-pikachu: #4a3a10;
  --blush: #ff6f92;
  /* The one strong colour: a link, and the wipe button once it is armed. It was
     #d43c66, which read 4.31:1 on cream — under the line, on words as load-bearing
     as "Clear the filters". */
  --hot: #c8355d;
  --on-danger: #93314b;
  /* The warm glow across the top of the page, as two corner lights. */
  --glow-a: rgba(255, 203, 5, 0.22);
  --glow-b: rgba(255, 111, 146, 0.18);
  --focus-ring: rgba(255, 111, 146, 0.55);
}

/* worst 4.84:1 */
[data-palette="pastel"] {
  --cream: #fdf2f7;
  --paper: #ffffff;
  --ink: #443145;
  --ink-soft: #6e5670;
  --ink-faint: #a494a6;
  --line: #fbe3ee;
  --edge: #e2c6d6;
  --pikachu: #fbc9df;
  --on-pikachu: #5a2c44;
  --blush: #9fc1ee;
  --hot: #b93f68;
  --on-danger: #8d3355;
  --glow-a: rgba(254, 205, 224, 0.5);
  --glow-b: rgba(223, 235, 253, 0.55);
  --focus-ring: rgba(159, 193, 238, 0.65);
}

/* worst 4.70:1 */
[data-palette="taro"] {
  --cream: #f1ecf4;
  --paper: #ffffff;
  --ink: #3d3346;
  --ink-soft: #615468;
  --ink-faint: #9b90a6;
  --line: #e8e0ee;
  --edge: #cbbcd8;
  --pikachu: #c3b3d4;
  --on-pikachu: #33244a;
  --blush: #d9a6c9;
  --hot: #a4497f;
  --on-danger: #7c3d63;
  /* Held down to 0.32: at the 0.45 it started at, the subtitle read 4.40:1. */
  --glow-a: rgba(195, 179, 212, 0.32);
  --glow-b: rgba(250, 238, 205, 0.5);
  --focus-ring: rgba(163, 132, 196, 0.6);
}

/* worst 4.80:1 */
[data-palette="coquette"] {
  --cream: #fff8e7;
  --paper: #fffdf6;
  --ink: #4a3327;
  /* The coffee from the reference, used exactly as given — it lands at 7.31:1. */
  --ink-soft: #6f4e37;
  --ink-faint: #a98a72;
  --line: #f3e6cf;
  --edge: #d8bfa6;
  --pikachu: #f7dad9;
  --on-pikachu: #5b3a2a;
  --blush: #ffd0cd;
  --hot: #b34f4a;
  --on-danger: #8a3d38;
  --glow-a: rgba(251, 242, 192, 0.55);
  --glow-b: rgba(255, 208, 205, 0.5);
  --focus-ring: rgba(111, 78, 55, 0.45);
}

/* worst 4.60:1 */
[data-palette="rilakkuma"] {
  --cream: #f9f1db;
  --paper: #fffaf0;
  --ink: #43332c;
  --ink-soft: #6d5348;
  --ink-faint: #a68d80;
  --line: #efe4cf;
  --edge: #cbb09f;
  --pikachu: #ef98b4;
  --on-pikachu: #4a1f2d;
  --blush: #c8e0da;
  --hot: #b6456b;
  --on-danger: #8d3355;
  --glow-a: rgba(239, 203, 210, 0.5);
  --glow-b: rgba(200, 224, 218, 0.55);
  --focus-ring: rgba(239, 152, 180, 0.6);
}

/* worst 4.68:1 */
[data-palette="berry"] {
  --cream: #f8f4e8;
  --paper: #fffcf6;
  --ink: #463430;
  /* The brown from the reference, as given: 5.94:1 on paper. */
  --ink-soft: #775c56;
  --ink-faint: #a68e88;
  --line: #eee6d8;
  --edge: #cfbdb4;
  --pikachu: #ffd3d4;
  --on-pikachu: #5c2f30;
  --blush: #8ecfba;
  --hot: #b34b5c;
  --on-danger: #8a3a49;
  --glow-a: rgba(255, 211, 212, 0.55);
  --glow-b: rgba(213, 235, 228, 0.6);
  --focus-ring: rgba(142, 207, 186, 0.7);
}

/* worst 4.69:1 */
[data-palette="matcha"] {
  --cream: #f2f6ea;
  --paper: #fcfdf8;
  --ink: #33402f;
  --ink-soft: #54684d;
  --ink-faint: #8fa088;
  --line: #e3edda;
  --edge: #bccdb2;
  --pikachu: #cfe3ab;
  --on-pikachu: #2f3d22;
  --blush: #eab9a0;
  --hot: #a35a3c;
  --on-danger: #7e4530;
  --glow-a: rgba(207, 227, 171, 0.5);
  /* The caramel half of the latte, kept light enough for the subtitle: 4.69:1. */
  --glow-b: rgba(234, 185, 160, 0.36);
  --focus-ring: rgba(124, 160, 110, 0.6);
}

/* worst 4.86:1 */
[data-palette="peach"] {
  --cream: #fff3ec;
  --paper: #fffaf7;
  --ink: #4a3129;
  --ink-soft: #77544a;
  --ink-faint: #ab8b80;
  --line: #fbe3d6;
  --edge: #e3bfae;
  --pikachu: #ffc9a3;
  --on-pikachu: #5a3213;
  --blush: #ff9fb0;
  --hot: #b6445c;
  --on-danger: #93384c;
  --glow-a: rgba(255, 201, 163, 0.5);
  --glow-b: rgba(255, 159, 176, 0.35);
  --focus-ring: rgba(255, 159, 176, 0.65);
}

/* worst 5.09:1 — the roomiest of the ten */
[data-palette="lavender"] {
  --cream: #f4f2fb;
  --paper: #fdfcff;
  --ink: #383350;
  --ink-soft: #5c5677;
  --ink-faint: #9a93b5;
  --line: #e7e3f6;
  --edge: #c6c0e0;
  --pikachu: #cfc6f2;
  --on-pikachu: #2e2650;
  --blush: #f6c6de;
  --hot: #9a4b86;
  --on-danger: #743a66;
  --glow-a: rgba(207, 198, 242, 0.5);
  --glow-b: rgba(246, 198, 222, 0.45);
  --focus-ring: rgba(160, 145, 226, 0.6);
}

/* worst 4.74:1 */
[data-palette="sky"] {
  --cream: #eef5fc;
  --paper: #fbfdff;
  --ink: #2f3b4a;
  --ink-soft: #4f6178;
  --ink-faint: #8a9aae;
  --line: #dfeaf6;
  --edge: #b6c8dc;
  --pikachu: #b8dcf5;
  --on-pikachu: #1f3a4d;
  --blush: #ffc6d5;
  --hot: #b04a6a;
  --on-danger: #87374f;
  --glow-a: rgba(184, 220, 245, 0.55);
  --glow-b: rgba(255, 198, 213, 0.4);
  --focus-ring: rgba(126, 180, 224, 0.65);
}

/* ---------- everything that is not a colour ---------- */

:root {
  --radius: 18px;
  --shadow: 0 2px 10px rgba(93, 61, 51, 0.06);
  --focus: 0 0 0 3px var(--focus-ring);
  /* How far down the group headings must stick to clear the filter bar. Kept in
     step with the bar's real height, which is why that height is explicit. */
  --bar-h: 82px;
  /* The page's one inset. Every band down the page — masthead, progress panel,
     filter bar, group headings, card rows, footer — is held off the edge by this
     and nothing else, so they share a left and a right edge by construction
     rather than by six literals agreeing. It steps up once, at 640px, and they
     all step with it. */
  --gutter: 0.7rem;
  --body: "Nunito", ui-rounded, "SF Pro Rounded", system-ui, sans-serif;
  --display: "Baloo 2", var(--body);
}

/* The bar grows by one row while anything is narrowing it; the headings follow.
   By exactly one row whether that is one pill or a dozen — see .active-filters. */
body:has(.active-filters) {
  --bar-h: 120px;
}

* {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  /* The close-up pins the body while it is open, which leaves the document with
     nothing to scroll and takes the scrollbar away with it. Reserving the gutter
     costs nothing on a phone, which has no scrollbar to reserve, and stops the
     page behind the overlay jumping sideways on a desktop that does. */
  scrollbar-gutter: stable;
}

body {
  margin: 0;
  padding: 0 0 max(2rem, env(safe-area-inset-bottom));
  /* The warm glow belongs to the top of the page and scrolls away with it. A fixed
     attachment would cost a full-page repaint on every scroll of a list this long. */
  background:
    radial-gradient(circle at 12% -4%, var(--glow-a), transparent 42%),
    radial-gradient(circle at 92% 2%, var(--glow-b), transparent 38%),
    var(--cream);
  background-repeat: no-repeat;
  background-size: 100% 20rem, 100% 20rem, auto;
  color: var(--ink);
  font-family: var(--body);
  font-size: 16px;
  line-height: 1.45;
}

/* One focus ring for everything, instead of leaving buttons to the browser's
   thin default. */
:focus-visible {
  outline: none;
  box-shadow: var(--focus);
  border-radius: 8px;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---------- the debug strip ----------

   Only ever on the page in a build made with the `telemetry` feature, which says
   out loud that it is recording her taps. In the flow, above the masthead, and
   never pinned to an edge: a strip that floats over the page is an element on
   top of the list, and an element on top of the list is one of the things the
   probe exists to rule out. It is not allowed to become a sixth suspect.

   Calm on purpose. Nothing red, nothing that pulses, nothing that reads as
   something having gone wrong — it is a note about what the build is, and the
   app underneath it is working normally. */

.probe-note {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0;
  /* The notch, on a home-screen launch: `viewport-fit=cover` means the top of
     the page really is under it. */
  padding: calc(0.6rem + env(safe-area-inset-top)) 1.1rem 0.6rem;
  border-bottom: 1px solid var(--edge);
  background: var(--line);
  color: var(--ink);
  font-size: 0.76rem;
  line-height: 1.35;
}

.probe-dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--pikachu);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--pikachu) 30%, transparent);
}

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

.masthead {
  padding: 1rem var(--gutter) 0.75rem;
  text-align: center;
}

.masthead h1 {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(1.45rem, 6vw, 2.1rem);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.01em;
}

.subtitle {
  margin: 0.15rem 0 0;
  font-family: var(--display);
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--ink-soft);
}

/* ---------- overview ---------- */

.overview {
  margin: 0 var(--gutter);
  padding: 0.85rem 0.9rem;
  border-radius: 22px;
  background: var(--paper);
  box-shadow: 0 6px 22px rgba(93, 61, 51, 0.09);
}

.total {
  display: flex;
  align-items: center;
  gap: 0.85rem;
}

.ring-wrap {
  position: relative;
  flex: none;
  width: 88px;
  height: 88px;
}

.ring {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}

.ring-track,
.ring-fill {
  fill: none;
  stroke-width: 11;
  stroke-linecap: round;
}

.ring-track {
  stroke: var(--line);
}

.ring-fill {
  stroke: var(--pikachu);
  transition: stroke-dasharray 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

/* A round cap paints a visible dot at zero, which would claim progress she has
   not made yet. */
.ring-fill.empty {
  stroke-linecap: butt;
}

.ring-label {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  text-align: center;
  line-height: 1.05;
}

.ring-label strong {
  font-family: var(--display);
  font-size: 1.4rem;
  font-weight: 800;
}

.ring-label span {
  font-size: 0.66rem;
  font-weight: 700;
  color: var(--ink-soft);
}

.total-words {
  min-width: 0;
}

.cheer {
  margin: 0;
  font-family: var(--display);
  font-size: 0.98rem;
  font-weight: 600;
  line-height: 1.25;
}

.left {
  margin: 0.2rem 0 0;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--ink-soft);
}

/* ---------- subsets, folded away ---------- */

.subsets {
  margin-top: 0.7rem;
  border-top: 1px solid var(--line);
}

.subsets > summary {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-height: 44px;
  font-size: 0.8rem;
  font-weight: 800;
  cursor: pointer;
  list-style: none;
}

.subsets > summary::-webkit-details-marker {
  display: none;
}

.subsets-label {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
}

.subsets-caret {
  width: 14px;
  height: 14px;
  color: var(--ink-faint);
}

/* A plain transform, with nothing animating it: the arrow points the right way
   from the first paint, open or shut, whether or not anything ever moved. */
.subsets[open] .subsets-caret {
  transform: rotate(180deg);
}

/* One bar per rarity, so every subset stays legible while the panel is folded. */
.sparks {
  flex: 1 1 auto;
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 16px;
}

.spark {
  position: relative;
  flex: 1 1 auto;
  max-width: 10px;
  height: 100%;
  border-radius: 2px;
  background: var(--line);
  overflow: hidden;
}

.spark-fill {
  position: absolute;
  inset: auto 0 0 0;
  background: var(--accent);
  transition: height 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

.subsets-count {
  flex: none;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.meters {
  display: grid;
  gap: 0.15rem;
  margin: 0.3rem 0 0.4rem;
  padding: 0;
  list-style: none;
}

/* Each meter is also the filter for its rarity. */
.meter {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: 100%;
  min-height: 44px;
  padding: 0.25rem 0.4rem;
  border: 0;
  border-radius: 12px;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.16s ease;
}

.meter.on {
  background: color-mix(in srgb, var(--accent) 16%, var(--paper));
}

.meter-mark {
  flex: none;
  width: 22px;
  height: 22px;
}

.meter-mark svg {
  width: 100%;
  height: 100%;
}

/* ---------- the icons ----------

   Every icon in the chrome is an inline SVG on a 24-box from crates/app/src/icon.rs,
   dropped into a span that carries this class. The box is sized here, per slot, and
   the drawing fills it — so an icon is never sized by a font size, and never picks up
   a shape from whatever font the device reached for. They paint in `currentColor`, so
   each one is the ink of whatever it sits in, in all ten palettes.

   18px is the resting size; the slots below say where that is wrong. */
.icon {
  flex: none;
  display: block;
  width: 18px;
  height: 18px;
}

.icon > svg {
  display: block;
  width: 100%;
  height: 100%;
}

.meter-body {
  flex: 1 1 auto;
  min-width: 0;
}

.meter-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.76rem;
  font-weight: 700;
}

.meter-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.meter-count {
  flex: none;
  /* Pushed to the end by this rather than by space-between, so the tick that
     appears beside a finished name sits next to the name instead of drifting into
     the middle of the row. */
  margin-left: auto;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.meter-track {
  display: block;
  height: 7px;
  margin-top: 3px;
  border-radius: 99px;
  background: var(--line);
  overflow: hidden;
}

.meter-fill {
  display: block;
  height: 100%;
  border-radius: 99px;
  background: var(--accent);
  transition: width 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Aligned to the label row rather than the two-line body, so it sits level with
   the name it belongs to. */
.meter-pct {
  flex: none;
  align-self: flex-start;
  width: 2.5rem;
  padding-top: 1px;
  text-align: right;
  font-size: 0.78rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}

/* Full ink, not a tint of the accent. These two were mixed 80% into the rarity
   colour, which read 2.08:1 for the Pikachu yellow and 2.48:1 for the gold — on
   the number that says a whole rarity is finished. The meter is already filled
   end to end in its accent; the number does not have to repeat it. */
.meter.done .meter-pct,
.meter-tick {
  color: var(--ink);
}

/* The row is baseline-aligned for the two bits of text in it; the tick is a box
   and has no baseline worth aligning to, so it is centred on the row instead. */
.meter-tick {
  align-self: center;
  width: 14px;
  height: 14px;
}

/* The two axes under the meters, in the same folded panel. Chips, not meters:
   a type cuts across the rarities rather than partitioning them, and a progress
   bar on one would invent a goal nobody set. */
.axis-label {
  margin: 0.55rem 0 0.3rem;
  font-size: 0.72rem;
  font-weight: 800;
  color: var(--ink-soft);
}

.kinds {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin: 0 0 0.5rem;
  padding: 0;
  list-style: none;
}

.kind {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  /* Comfortably tappable without being a row of its own: 32 high inside a 44
     tall target made by the padding, so eleven of them still fit in four lines
     on a 375 screen. */
  min-height: 32px;
  padding: 0 0.6rem;
  border: 1.5px solid var(--line);
  border-radius: 99px;
  background: var(--paper);
  color: inherit;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 700;
  cursor: pointer;
  transition:
    background 0.16s ease,
    border-color 0.16s ease;
}

.kind.on {
  border-color: var(--accent, var(--blush));
  background: color-mix(in srgb, var(--accent, var(--blush)) 18%, var(--paper));
}

.kind-dot {
  flex: none;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--accent, var(--blush));
  /* The pale ones — Colorless most of all — need an edge to be a disc at all. */
  box-shadow: inset 0 0 0 1px rgb(0 0 0 / 18%);
}

/* Full art has no colour of its own, so its dot is the shape instead: a ring,
   for a card that is all picture and no border. */
.kind-dot--art {
  background: none;
  box-shadow: inset 0 0 0 2px var(--ink-faint);
}

/* ---------- filter bar ---------- */

.filterbar {
  position: sticky;
  top: 0;
  z-index: 4;
  margin-top: 0.8rem;
  padding: 8px var(--gutter);
  /* Opaque on purpose. A backdrop-filter here is a compositing trap: on a page
     this tall it stops the whole list below from painting at all. */
  background: var(--cream);
  border-bottom: 1px solid var(--line);
}

.filter-row {
  display: flex;
  gap: 0.45rem;
  height: 44px;
}

.search {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-width: 0;
  padding: 0 0.7rem;
  border-radius: 99px;
  background: var(--paper);
  box-shadow: var(--shadow);
}

.search-icon {
  width: 16px;
  height: 16px;
  color: var(--ink-faint);
}

.search input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 0.88rem;
  font-weight: 600;
}

.search input:focus {
  outline: none;
}

.search:focus-within {
  box-shadow: var(--focus);
}

.segmented {
  flex: none;
  display: flex;
  align-items: center;
  padding: 3px;
  border-radius: 99px;
  background: var(--paper);
  box-shadow: var(--shadow);
}

.seg {
  min-height: 38px;
  padding: 0 0.6rem;
  border: 0;
  border-radius: 99px;
  background: none;
  color: var(--ink-soft);
  font: inherit;
  font-size: 0.76rem;
  font-weight: 800;
  cursor: pointer;
  transition: background 0.16s ease, color 0.16s ease;
}

.seg.on {
  background: var(--pikachu);
  color: var(--on-pikachu);
}

/* What is narrowing the list — and so what the counter above is counting — since
   the panel that set it may well be folded away by now. One row and one row
   only: the group headings stick to the bottom of this bar at the fixed --bar-h
   above, so a second line of pills would slide every heading over its own cards.
   Several choices can be on at once per axis, so there is no ceiling on how many
   pills there are; the row scrolls sideways and never wraps, however many. */
.active-filters {
  display: flex;
  flex-wrap: nowrap;
  gap: 0.35rem;
  overflow-x: auto;
  scrollbar-width: none;
}

.active-filters::-webkit-scrollbar {
  display: none;
}

.active-rarity {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-height: 32px;
  margin-top: 6px;
  white-space: nowrap;
  padding: 0 0.7rem;
  border: 1.5px solid var(--accent, var(--blush));
  border-radius: 99px;
  background: color-mix(in srgb, var(--accent, var(--blush)) 16%, var(--paper));
  color: #4a3a33;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 800;
  cursor: pointer;
}

.active-rarity-x {
  width: 14px;
  height: 14px;
  opacity: 0.7;
}

/* ---------- groups ---------- */

.group {
  margin: 1.1rem 0 0;
}

.group.empty {
  display: none;
}

.group-heading {
  position: sticky;
  top: var(--bar-h);
  z-index: 2;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  margin: 0;
  padding: 0.5rem var(--gutter);
  /* Fully opaque: a gradient that faded out let card art show through the band. */
  background: var(--cream);
  box-shadow: 0 6px 8px -6px rgba(93, 61, 51, 0.16);
}

.group-name {
  font-family: var(--display);
  font-size: 1.02rem;
  font-weight: 700;
}

.group-count {
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--ink-soft);
}

/* ---------- cards ---------- */

.cards {
  display: grid;
  gap: 0.45rem;
  margin: 0.45rem 0 0;
  padding: 0 var(--gutter);
  list-style: none;
}

.card {
  /* The row's control is cut into rectangles, and the shop link is one of the
     seams between them.

     A button is one rectangle and cannot have a hole punched in it, so a single
     button covering the row cannot leave room inside itself for a link. Laying
     the link over it instead would put a press in the hands of whichever the
     browser decided was on top — the exact bug that made this app unusable on her
     phone for days — and an absolutely positioned link is that same bug wearing a
     different hat. So the control is cut along the line the link wants and the
     link takes the seam. Rectangles that tile the row exactly: no overlap, because
     the grid has not left them anywhere to overlap, and no gap either, so not one
     pixel of the row does nothing. Every one of them claims the card, and the
     seams between them are harmless — a press that lands a hair to one side of a
     seam does what it would have done a hair to the other.

     The link's track is a fixed 18px rather than `auto`, and that is the one thing
     in here that must not be simplified. `auto` would size the track to the link
     and collapse it to nothing on a row that has none — the eight basic energies,
     and every row on the list once she switches the links off — and the rarity pill
     would slide 18px left and touch the number on exactly those rows. A fixed track
     is the gap whether or not anything is standing in it, so the pill sits 18px from
     the number on all 216 rows, in both languages, with the links on or off. The
     name and the number start 103.2px from the row's edge on all 216 rows too,
     which is where her picture has them. */
  display: grid;
  /* Five across and two down, and every track is here because something in the
     row has to be its own rectangle:

       c1  the tick and the artwork          c4  the rarity pill
       c2  the collector number              c5  the rarity mark
       c3  the dollar — the link             r1  the name    r2  the meta line

     The name spans c2 to c5, so it starts on the number's own left edge and the
     two line up without anybody being told to. The artwork spans both rows; the
     mark takes r2 only, because it stands on the meta line rather than in the
     middle of the row — see `.card-hit--mark`, and see `.card-hit--name` for what
     covers r1 of c5 now that the mark has left it. c3 is the 18px gap between the
     number and the pill, fixed rather than `auto` for the reason above: it is the
     gap, not the link, and it has to be there on the rows that have no link. */
  grid-template-columns: auto auto 18px minmax(0, 1fr) auto;
  grid-template-rows: auto minmax(0, 1fr);
  min-width: 0;
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow);
  /* Only the press. Whether the card is owned is drawn, not faded to: a
     transition is a timeline like any other, and a timeline that does not run
     leaves the row painted as whatever it was before the tap. `transform` may
     strand — a card left a hair small is nothing — but `background` says which
     cards she owns, and that must never be a frame behind the truth. */
  transition: transform 0.14s ease;
}

.card:active {
  transform: scale(0.988);
}

/* A card you own gets a wash of its rarity's colour, so a filled page reads as
   colour rather than as a column of ticks. */
.card.owned {
  background: linear-gradient(
    100deg,
    color-mix(in srgb, var(--accent) 22%, var(--paper)),
    var(--paper) 62%
  );
}

/* Everything below is true of both halves of the row's control: the same
   hardening, the same behaviour, the same handler behind them. Only the padding
   and which track they take differ. */
.card-hit {
  grid-row: 1;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  min-width: 0;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* Every one of these is here to stop iOS eating the tap:
     - manipulation removes the double-tap-to-zoom wait
     - user-select stops a tap on the name becoming a text selection
     - touch-callout stops a tap on the artwork raising the save/copy menu */
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* The picture: the row's left padding, the tick and the artwork, spanning both
   rows, and no padding on the right — the name's own padding provides the space
   after it, and a padding here would only push the two apart twice. */
.card-hit--art {
  grid-column: 1;
  grid-row: 1 / -1;
  flex: none;
  padding: 0.45rem 0 0.45rem 0.55rem;
}

/* The name, across the three tracks the meta line is cut into and the mark's as
   well. Top-aligned, so its first line is where her picture has it.

   It reaches into c5 because the mark no longer does: the mark dropped to r2 to
   stand on the meta line, which left r1 of its column with nothing covering it,
   and an uncovered strip in the middle of a row is a press that does nothing. The
   name takes it. There is no overlap to worry about — the mark is in the row
   below, and two cells in different rows cannot reach each other.

   The right padding is what keeps the name's own text where it was. The cell used
   to stop at c5's left edge; now it stops at the row's right edge, so the mark's
   24px and the row's 0.7rem of padding are given back here. The name's content box
   comes out at exactly the width it had, which is what `.card-run`'s negative
   margin is measured against. */
.card-hit--name {
  grid-column: 2 / 6;
  grid-row: 1;
  display: block;
  /* 8.2 at the top lands the name's ink 12.6px down the row, against the 12.8 in
     her picture. */
  padding: 8.2px calc(0.7rem + 24px) 0 0.7rem;
}

/* The three cells of the meta line. They take the whole of the second row, which
   is everything under the name — the line itself and the slack beneath it — so
   the row stays covered to its bottom edge, and their contents sit at the top of
   it where the line belongs. */
.card-hit--num,
.card-hit--rar {
  grid-row: 2;
  align-items: start;
  /* 3.5 at the top lands the number's ink 36.1px down the row, which is hers to
     the tenth. The link between them carries the same padding so the dollar sits
     on the number's line rather than near it. */
  padding: 3.5px 0 0;
}

.card-hit--num {
  grid-column: 2;
  padding-left: 0.7rem;
}

/* The pill hangs 1.76px lower than everything else on this line unless it is told
   not to, and that is the crooked he was looking at.

   The number is bare text: its box *is* its line box, so 3.5px of padding puts its
   line where the line belongs. The pill is text inside a lozenge, and the lozenge
   has 0.11rem — 1.76px — of padding above the text. Give the two the same 3.5 and
   their boxes line up while their words do not: the pill's letters sit 1.76px low
   and its baseline 1.76px below the number's. Boxes agreeing while text disagrees
   is exactly the kind of wrong that survives a measurement and still looks bent.

   So this cell takes 3.5 less the pill's own top padding, and the pill's line box
   lands on the number's. Measured after: baselines 0.01px apart, box centres 0.02
   apart, which is the float and not a discrepancy. */
.card-hit--rar {
  grid-column: 4;
  padding-top: 1.74px;
  padding-right: 0.7rem;
}

/* And when there is no link, the gap it would have stood in belongs to the rarity.

   c3 is a fixed 18px so that the pill sits in the same place whether or not there
   is a link to put in it — see `.card` — but a fixed track with nothing in it is
   18px of row that does nothing, and "not one pixel of the row does nothing" is the
   rule this layout is built to keep. It would be 18px on each of the eight basic
   energies, and 18px on all 216 rows the moment she switches the links off: a
   stripe down the whole list where a press lands on neither the number nor the
   pill. Before the dollar existed that space was inside the one text button and it
   ticked the card, so this is the old behaviour restored rather than a new idea.

   The rarity takes it rather than the number because it is the rarity's own gap —
   and because c4 is `minmax(0, 1fr)`. A flexible track is sized from the free
   space left over rather than from what spans it, so widening this item across
   c3 and c4 moves neither track edge and the pill does not budge; spanning the
   number across c2 and c3 instead would shrink the `auto` track c2 by the 18px it
   gained and slide the pill left, which is the whole bug over again. The padding
   puts the pill's background back on c4's own left edge, where it was.

   Nothing can be overlapped here: the only thing that could have occupied c3 is
   the link, and the selector is the statement that there isn't one. */
.card:not(:has(> .shop-link)) .card-hit--rar {
  grid-column: 3 / 5;
  padding-left: 18px;
}

/* The rarity mark, at the far right and standing on the meta line.

   It used to be centred down the whole height of the row, which put it 3.1px above
   the number and the pill on a plain row: three things on what reads as one band,
   on three different centre lines. With a grid inspector open that is the first
   thing you see, and it is what he saw.

   So the mark comes down into r2 and takes the meta line's own top padding. That
   is the only way to align it, because the meta line is not at a fixed height: it
   is the last line of a text block that flows, so it sits 41.3px down on 187 rows
   and 56.9px down on the twenty-nine that carry an English second name. A mark
   pinned to one number would be right on some rows and wrong on the rest; a mark
   in r2 is on the line on all 216, measured: the number, the dollar, the pill and
   this never more than 0.02px apart on any row, at any width, in either language.

   What that costs, and it is worth being plain about it: the column of marks is no
   longer perfectly straight down the list — on those twenty-nine rows the mark sits
   15.6px lower than on its neighbours. But the pill on those rows is already 15.6px
   lower, and it always was. The choice is not between straight and crooked, it is
   between the mark agreeing with its own row and the mark agreeing with the row
   above it. Agreeing with its own row is what was asked for, and the mark and the
   pill both say the rarity, so pairing them is right twice over. */
.card-hit--mark {
  grid-column: 5;
  grid-row: 2;
  flex: none;
  align-items: start;
  justify-content: flex-end;
  /* The same 3.5px the number and the pill take, so this starts level with the
     top of the meta line. */
  padding: 3.5px 0.7rem 0 0;
}

/* And then up by half the difference between a 24px mark and the 17.63px line it
   stands on, which centres it on the line rather than hanging it from the top.
   (24 - 17.63) / 2 = 3.185. Scoped to the row: the grid's tiles use `.mark` too,
   and nothing here is any of their business. */
.card-hit--mark .mark {
  margin-top: -3.19px;
}

/* Square where they meet, round where the row is.

   Worth the rules, because a rounded corner is not only a look: hit testing
   follows it, and a press inside a corner that has been rounded away lands on
   nothing. When the row was two halves with `var(--radius)` on all four corners
   of each, the seams wore a pair of notches apiece and a sweep of every pixel of
   a row found 210 of them. There are more seams now, so only the four corners
   that face the outside of the row are round and every one that faces a seam is
   square. */
.card-hit {
  border-radius: 0;
}

.card-hit--art {
  border-top-left-radius: var(--radius);
  border-bottom-left-radius: var(--radius);
}

/* The top right corner changed hands when the mark moved down to the meta line:
   the name reaches into that column in r1 now, so the name owns that corner and
   the mark keeps only the one below it. Getting this wrong would not show as a
   wrong-looking corner — the row's own radius would still draw it — it would show
   as a press near the corner landing on nothing, which is the whole reason these
   four rules exist. */
.card-hit--name {
  border-top-right-radius: var(--radius);
}

.card-hit--mark {
  border-bottom-right-radius: var(--radius);
}

/* Nothing inside the row is its own target. Whatever she hits — art, name, number,
   rarity mark — the press belongs to the row. */
.card-hit > * {
  pointer-events: none;
}


/* ---------- the way out to the shop ----------

   A link, never a price. Nothing in this app fetches ligapokemon.com.br, reads it
   or shows a number from it: see Card::market_url. This is chrome on a page that
   is mostly for ticking cards, so it is drawn in the faintest ink there is and is
   hers to switch off entirely. */
.shop-link {
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  display: grid;
  place-items: center;
  color: var(--ink-faint);
  /* No rule ever turned the browser's own link underline off, so the dollar wore
     one — and at seven pixels of glyph it renders as a row of dots rather than a
     line. The mark is not prose and does not need underlining to be found: it is
     the only green thing on the row, it sits alone in its own cell, and it
     carries a label of its own for anyone not reading by eye. */
  text-decoration: none;
}

/* Beside the collector number, in a cell of its own between the number and the
   rarity. This link must stay outside the row's buttons — an `<a>` inside a
   `<button>` is invalid, and `.card-hit > * { pointer-events: none }` would make
   it dead to the touch anyway — and the two must never share a pixel, because a
   press in shared space is settled by stacking order rather than by geometry, and
   that is the bug that made this list untickable on her phone. The grid is what
   says so: this owns c3 of the meta row, the number owns c2, the rarity owns c4,
   and none of them can reach another. (On a row with no link the rarity takes c3
   as well, so that the 18px is not dead — see `.card-hit--rar` below. There is
   nothing to collide with there, because there is no link on those rows.)

   Eighteen pixels wide, which is the gap between the number and the pill, and the
   number is the one thing in this rule that was chosen by eye rather than derived.

   The history is worth two sentences, because the number has moved twice. The cell
   was 44px once — a finger — and it took the 36 it needed out of the gap, which
   stood the pill 36px further from the number than it had ever been; that is what
   he objected to, and it went back to 8, the gap exactly, with nothing to spare. At
   8 the dollar has 0.34px of air either side of it and reads as part of the number
   — "011/128$", a currency suffix rather than a mark of its own — which is what he
   objected to next, and fairly. He asked for room on both sides of it, and that is
   a licence to move the pill, which is the thing that had been forbidden.

   So: 18, giving 5.34px to the left of the glyph and 5.36px to the right, and
   moving the pill 10px from where it had been. 16 and 20 were built and looked at
   beside it. 16 still reads a little tight; at 20 the dollar starts to float in the
   middle of the gap rather than stand between two things. 18 is where the glyph is
   plainly separate from both neighbours and plainly still on their line.

   The touch target follows from the width and is the reason not to be stingy here:
   18 × 31.8px at its smallest, 572px², where the 8px cell managed 254. Still under
   the 44 × 44 the guidance asks for — an 8px-wide cell was an eighth of it and this
   is a quarter — but it is the difference between a mark you can hit and one you
   cannot. A tap that misses still lands on the number or the pill either side of
   it, both of which tick the card, and the undo toast takes that back.

   Height is free and all of it is taken: the cell runs the whole of the meta row,
   from under the name to the bottom edge of the card, so it is 31.8px tall on a row
   that carries an English second name and 47.4px on one that does not, against a
   glyph 17.6px tall.

   The glyph is centred in the gap. It is 7.3px wide in Nunito at this size and
   weight; every font under it in the stack is narrower — 6.8px for the generic
   sans, 6.7 for system-ui, 6.1 for ui-rounded — so the webfont is the wide case and
   the offline first paint has more room rather than less. The cell does not hide
   its overflow, so a wider dollar would be drawn a hair over its neighbours rather
   than cut, and it would still be only drawing — `overflow` moves no hit rectangle. */
.card > .shop-link {
  grid-column: 3;
  grid-row: 2;
  /* No width here on purpose: c3 is 18px and this stretches to fill it, so the 18
     is written down once, on the track that has to hold it open on the rows that
     have no link to put in it. */
  justify-items: center;
  align-items: start;
  /* Level with the number beside it: the same top padding the number's own cell
     has, so the two sit on one line rather than near it. */
  padding: 3.5px 0 0;
  /* Square: this cell has a seam on both sides and a rounded corner here is a
     press that lands on nothing. */
  border-radius: 0;
}

.shop-mark {
  display: block;
  width: 19px;
  height: 19px;
}

.shop-mark svg {
  display: block;
  width: 100%;
  height: 100%;
}

.shop-link:focus-visible {
  box-shadow: var(--focus);
}

/* The tile keeps its two controls in the bottom corners and its run badge top
   right, so this takes the one corner nobody else is using. Top left is the only
   part of the artwork it can cover without hiding anything: a Pokémon card
   centres its name and puts the stage word — "Basic", "Stage 1" — in this corner,
   which is the one thing on the card the checklist already says twice. */
.tile > .shop-link {
  position: absolute;
  top: -4px;
  left: -4px;
  width: 40px;
  height: 40px;
  color: #fff;
}

.tile > .shop-link .shop-mark {
  width: 26px;
  height: 26px;
  padding: 4px;
  border-radius: 50%;
  background: rgba(46, 33, 30, 0.72);
}

/* the round tick */

.tick {
  position: relative;
  flex: none;
  width: 28px;
  height: 28px;
  /* The app's primary control: it has to look like something you press. */
  border: 2.5px solid var(--edge);
  border-radius: 50%;
  background: #fdf3ee;
  /* No transition, on purpose. This circle *is* the answer to "do I have this
     card", so it has to be readable off `.owned` alone, with nothing in between.
     A transition starts at the old value and only reaches the new one if a
     timeline runs; the pop below is decoration laid over the top of a tick that
     is already correct. */
}

.tick::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  /* Drawn to the proportions of the circle it sits in. Hers is 54% of the disc
     across with a stroke of about 4.2px; this is 53% with 3.8, which is as close
     as a rotated box gets — see the note under `.owned .tick::after` for the part
     of her checkmark this shape cannot make. The tile in the grid keeps the old,
     lighter pair below: its circle is 24px and this one would crowd it. */
  width: 14px;
  height: 7px;
  border: 0 solid #fff;
  border-width: 0 0 3.8px 3.8px;
  border-radius: 1px;
  transform: rotate(-45deg) translate(1px, -2px) scale(0.4);
  opacity: 0;
  /* Drawn or not drawn, straight away, for the same reason as the circle above.
     The checkmark used to spring in, and a spring that never runs is a checkmark
     that stays where it was — which is exactly what "it will not untick" looks
     like from the other side of the screen. */
}

.card-hit:focus-visible {
  box-shadow: var(--focus);
}

.owned .tick {
  border-color: color-mix(in srgb, var(--accent) 80%, #6b5148);
  background: color-mix(in srgb, var(--accent) 88%, #6b5148);
}

/* The one shape in her picture this does not reproduce, written down so nobody
   re-measures it and thinks it is a bug. Her checkmark is 54% of the disc wide by
   40% tall; this one is square, because a box with two borders rotated 45° has a
   right angle at its vertex and a right angle cannot be wider than it is tall. A
   drawn checkmark opens that vertex out past 90°, which is what makes hers sit
   flat. Matching it means an SVG path in place of these four lines, on the one
   element in the app that must paint the truth with no animation and no timeline
   anywhere near it. Not worth the trade for six pixels; noted, not forgotten. */
.owned .tick::after {
  opacity: 1;
  transform: rotate(-45deg) translate(1px, -2px) scale(1);
}

/* artwork */

/* The box a row keeps for a picture. It is this size whether a picture arrives or
   not, which is the whole point of it: nothing in the line moves either way. */
.art {
  flex: none;
  /* Five by seven, the shape every card in this set is printed at, and big
     enough to tell two Pikachus apart without opening either. It is also what
     sets the height of every row: the tallest text a row can carry — name,
     English name and the meta line — comes to 54px, so all 199 rows are this
     one number tall whether or not the card has a second name. */
  width: 44px;
  height: 62px;
  border-radius: 6px;
  background: var(--line);
}

/* Every picture in the app sits in one of these — the row's box above, a tile in
   the grid, the close-up — and the stand-in for one that is not there is laid over
   the top of it. See crates/app/src/art.rs. */
.art-slot {
  position: relative;
  display: block;
  overflow: hidden;
}

/* Fills the control it sits in, which is where the grid keeps its shape. */
.art-slot--fill {
  position: absolute;
  inset: 0;
}

/* The close-up has nothing to take its height from until a picture arrives, so the
   shape is stated rather than inherited: every card in this set is printed at five
   by seven. Holding it means the overlay does not jump when the full-size picture
   lands — or when it never does. */
.art-slot--card {
  aspect-ratio: 5 / 7;
}

.art-slot > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A picture that failed keeps its box and stops painting, which is what there is
   instead of the browser's broken-picture glyph. `visibility` rather than
   `display`: the element is still there, holding the one retry it is owed, and an
   image that is not displayed is never asked for again. */
.art-slot > img.gone {
  visibility: hidden;
}

/* A card she does not have sits in the dark until she finds it. Lit or unlit the
   moment the class changes, like everything else that reports state — and on the
   picture alone, so the stand-in underneath keeps its contrast in all ten
   palettes. */
.art > img {
  filter: saturate(0.6) opacity(0.8);
}

.owned .art > img {
  filter: none;
}

/* The stand-in: a wash of the card's own type colour with our drawing centred on
   it. Cards with no type of their own pass a palette line as the tint rather than
   a colour written down in Rust. The drawing is currentColor throughout, so every
   palette inks it itself. */
.art-blank {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--tint) 22%, var(--cream));
  color: var(--ink-soft);
}

.art-blank svg {
  width: 70%;
  height: auto;
}

/* One drawing at three sizes, and therefore three stroke weights. It scales with
   the slot it is in, so a weight that reads at forty pixels across would be a fat
   crayon at three hundred; these hold the *apparent* weight steady instead, which
   is what the single weight in icon.rs is for. */
.art-slot--fill .art-blank svg {
  width: 52%;
  stroke-width: 1.05;
}

.art-slot--card .art-blank svg {
  width: 50%;
  stroke-width: 0.8;
}

/* The close-up, and only the close-up, paints two foil layers over the picture.
   They are siblings of it, so without this they cover the stand-in as well — and a
   holographic sheen belongs to a printed picture. Spread across a card that has
   none it reads *as* the picture, and it washes the drawing underneath down to
   almost nothing: the RGB Mews carry the strongest sheen in the set, and they are
   exactly the cards this was drawn for.

   Deliberately not on `.art-blank` itself. The grid keeps its tick and its shop
   link in the corners of the tile, over the picture's own box, and a stand-in
   lifted a layer covers both. */
.art-slot--card .art-blank {
  z-index: 1;
}

/* text */

/* The type of the meta line, which used to live on a wrapper span of its own. The
   line is three grid cells now with no wrapper to hang it on, so it is stated
   once here for the three things that sit on it.

   The dollar is in this list on purpose and not by accident: "mesma fonte do
   número da carta, mesmo tamanho e tudo" — it differs from the number in colour
   and in nothing else, and the way to keep that true is to give the two the same
   declaration rather than two that happen to agree today.

   0.76rem is not a compromise, and it is the one number here that was read off
   her picture rather than chosen: her "023/128" is 48.2px of ink wide and this is
   47.6, and her rarity pill is 90.7 wide by 20.1 tall against 87.9 by 20.8 here.
   An earlier reading of that pill said 149.8 and had this line set much larger to
   match; the sample had picked up the row's own yellow wash either side of the
   pill and measured the two together. Her line is this line.

   This line used to be a flex wrapper that could put too much on one row, and on
   the thirty Pikachu rows it did: the number, the run and the rarity, 46.3 + 59.5
   + 86.0 with 14.4 of gaps, 206px of nowrap pills in a text block 165px wide at
   375. It ran past the block and was painted under the rarity mark, the ears
   drawn over "Rara Pikachu". Two things fixed that and both still hold — the run
   went up to the name's line, and the pill is in a grid cell of its own now,
   which cannot be overrun the way a flex line could. What is left of the old
   backstop is the ellipsis on `.card-rarity`, and it stays for the same reason
   the wrap did. */
.card-hit--num,
.card-hit--rar,
.card > .shop-link {
  font-size: 0.76rem;
  font-weight: 700;
  line-height: 1.45;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* The soft green, and it is a literal colour rather than a custom property on
   purpose. A property has to be declared in all ten palette blocks or it falls
   back to whatever it inherits, and a green that is right in `cream` and absent
   in `taro` is exactly the kind of thing that passes review and then surprises
   her. There is nothing here to declare, so there is nothing to forget.

   It is also not in the palettes for the same reason the rarity accents are not:
   it means a fixed thing — money — rather than a mood, and it should be the same
   green whichever palette the page is wearing.

   On "fofo", honestly: a pastel green cannot carry text at this size. The
   softest green this app already owns is the basic-energy accent, #78c98d, and
   it manages 1.9:1 on paper where the number beside it holds 5.9:1 — it would be
   a hint of a smudge, not a dollar sign. So this is a leaf green rather than a
   pastel one, picked to be the greyest, least shouty green that is still at
   least as legible as the number it stands next to: measured against all ten
   palettes' paper and all hundred owned-row washes, worst case 6.2:1 on paper
   and 4.6:1 on a wash, where `--ink-soft` itself measures 5.9 and 4.4. */
.card-money {
  color: #296a46;
}

/* The name's own line, and the one thing allowed to share it: the Pikachu run,
   which rides out to the top right corner of the row on a negative margin.

   The negative margin is the whole trick and it is worth understanding, because
   the obvious way to put something in that corner is to position it absolutely,
   and the obvious way is wrong here. An absolutely positioned pill takes no room,
   so the name would flow underneath it and the two would be kept apart by a
   hard-coded gutter — a number that is right until a webfont fails to load and
   the pill grows. These two are flex siblings instead, so the name's width is
   *computed* from the pill's: it ellipsises before it can reach it, at any font,
   in any language, for ever. The margin only changes where the pill is drawn, not
   how much room the line gives it.

   It is pulled right by the mark's column and the gap before it, which lands its
   right edge exactly where the mark's is — the two right-hand things in the row
   share an edge, which is what her picture shows. */
.card-head {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
}

.card-name {
  /* Takes the line, and gives the run whatever it asks for. `min-width: 0` is
     what lets a long name ellipsise instead of pushing the run out of the row —
     a flex item's floor is its own content until it is told otherwise. */
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 700;
  font-size: 1rem;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The English name, for the cards whose Brazilian printing reads differently. */
.card-alt {
  display: block;
  font-size: 0.78rem;
  font-style: italic;
  line-height: 1.25;
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-rarity {
  /* Deeper than it was, because in her picture this is a soft fat lozenge rather
     than a tight one — a pill about 1.75 times the height of the words in it. The
     padding is the half of that which costs the line nothing: height is free here,
     the artwork beside it is taller than the whole text block. */
  padding: 0.11rem 0.45rem;
  border-radius: 99px;
  /* Dark text on a tint of the accent. Tinting the *text* by the accent put every
     one of these under 3.6:1, and the yellow ones near 2:1. */
  background: color-mix(in srgb, var(--accent) 24%, #fff);
  color: #4a3a33;
  white-space: nowrap;
  /* The backstop for the meta line, and the last of it: a label too long for the
     cell — some future rarity, some other language — ellipsises inside its own
     background rather than escaping the row and being painted under the mark, the
     way "Rara Pikachu" once was. The wrap this used to stand beside is gone with
     the flex line it belonged to; the cell cannot be overrun the way that line
     could, so the ellipsis is on its own now and is kept for the same reason.

     It has never once fired, and that is the point of keeping it: measured across
     all 216 rows in both languages at one, two and three columns, the widest pill
     there is comes to 96.7px ("Futuristic Rare", and 94.4 for "Rara Futurista")
     against a cell that is 133.7px at its narrowest, so nothing is clipped
     anywhere. Three declarations that change nothing today are what make "this
     line cannot overflow" a fact about the rule rather than a count of the seven
     labels that happen to exist. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* In these sections every row carries the same rarity, so saying it on each row
   is noise the section heading already covers. */
.group--classic .card-rarity,
.group--energy .card-rarity,
.group--rgb .card-rarity {
  display: none;
}

.mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
}

.mark svg {
  width: 100%;
  height: 100%;
}

.nothing {
  margin: 2rem 1.1rem;
  text-align: center;
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--ink-soft);
}

.linkish {
  padding: 0;
  border: 0;
  background: none;
  color: var(--hot);
  font: inherit;
  font-weight: 800;
  text-decoration: underline;
  cursor: pointer;
}

/* ---------- colophon ---------- */

.colophon {
  margin-top: 1.8rem;
  padding: 0 var(--gutter);
  text-align: center;
  font-size: 0.76rem;
  color: var(--ink-soft);
}

/* Which build this is. Quiet — smaller than the disclaimer above it, and set in
   the same soft ink rather than the faint one, because it is the one line in the
   footer someone may be asked to read back out loud. Monospaced so a hyphen, a
   one and an l cannot be confused down a phone line, and letter-spaced a little
   so a run of digits does not clump.

   Deliberately not styled as a warning: it is a fact about the build, present in
   every build, and it should look like part of the colophon it sits in. */
.build {
  margin: 0.45rem 0 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.68rem;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
}

/* ---------- words, codes and the way back in ---------- */

.backup-why {
  margin: 0 0 0.7rem;
  font-size: 0.78rem;
  line-height: 1.4;
}

.field {
  display: block;
  margin-bottom: 0.5rem;
}

.field > span {
  display: block;
  margin-bottom: 0.2rem;
  font-size: 0.72rem;
  font-weight: 800;
  color: var(--ink-soft);
}

.code {
  width: 100%;
  padding: 0.45rem 0.55rem;
  border: 1.5px solid var(--line);
  border-radius: 12px;
  background: var(--cream);
  color: var(--ink);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.74rem;
  line-height: 1.4;
  resize: vertical;
}

.code:focus {
  outline: none;
  box-shadow: var(--focus);
  border-color: transparent;
}

.backup-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.btn {
  min-height: 40px;
  padding: 0 0.9rem;
  border: 0;
  border-radius: 99px;
  background: var(--pikachu);
  color: var(--on-pikachu);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 800;
  cursor: pointer;
}

.btn.ghost {
  background: var(--cream);
  color: var(--ink-soft);
  box-shadow: inset 0 0 0 1.5px var(--line);
}

/* The one thing to do on a screen that has one thing to do. */
.btn.wide {
  display: block;
  width: 100%;
  min-height: 46px;
  font-size: 0.86rem;
}

.btn.danger {
  background: color-mix(in srgb, var(--blush) 16%, var(--paper));
  color: var(--on-danger);
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--blush) 45%, var(--paper));
}

/* Armed to wipe everything: it should not look like the button it just was. */
.btn.danger.armed {
  background: var(--hot);
  color: #fff;
  box-shadow: none;
}

.note:empty {
  display: none;
}

.note {
  margin: 0.55rem 0 0;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--ink-soft);
}

/* ---------- wider screens ---------- */

@media (min-width: 640px) {
  /* Wider screens get a wider inset — once, here. The bands below used to carry
     this number themselves, and the filter bar was the one that never got it:
     it sat 6.4px proud of the panel above it and the rows below it at every
     width past this one. Nothing reads 1.1rem directly any more. */
  :root {
    --gutter: 1.1rem;
  }
  .overview {
    padding: 1.1rem 1.2rem;
  }
  .meters {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.15rem 1.2rem;
  }
  .cards {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  /* Only 28 cards carry a second name, so without this a two-line row and a
     three-line row in the same grid row drift out of alignment. */
  .card-hit {
    align-items: flex-start;
  }
  /* The mark is deliberately not in this list. It used to be, because it used to
     be centred down the whole row and `flex-start` above would have dropped it to
     the top. It stands on the meta line now, in a cell of its own in r2, and
     `align-self: center` here would centre it in that cell instead — which is the
     slack under the meta line, not the line. Its own rule holds at every width. */
  .tick,
  .art {
    align-self: center;
  }
  /* The meta line used to be set smaller here, and it no longer needs to be.

     When it was a flex line inside the text block, two columns left that block
     111px instead of 159 and the number and the longest label wanted 150 of it:
     rows put the rarity on a line of its own and the list stopped agreeing on how
     tall a row was. 0.7rem was the largest that fitted. The line is grid cells
     across the whole row now rather than a flex line inside the text block, so it
     has the room without being shrunk — measured at 0.76rem in both languages, the
     widest pill there is comes to 96.7px ("Futuristic Rare"; 94.4 for "Rara
     Futurista") against a cell that is 133.7px at its narrowest, which is Mew ex
     at 158/128 in two columns. 37px spare at the tightest point, nothing clipped
     anywhere, and all 216 rows at one height at every width. The rule that used to
     be here named `.card-meta`, which stopped existing when the line became cells,
     so it had been doing nothing for a while before it was taken out. */
}

@media (min-width: 1024px) {
  body {
    padding-inline: max(0px, calc(50vw - 33rem));
  }
  .cards {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  .meters {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

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

/* ---------- how it feels ---------- */

/* Sharp on purpose: a slow swell would lag behind her finger. Everything here
   animates transform and opacity only, so 199 rows stay smooth on a phone. */

.masthead {
  position: relative;
}

/* ---------- preferences ---------- */

/* Above the sticky filter bar, which is z-index 4 — the menu overlaps it as soon
   as it opens. .masthead sets no z-index of its own, so this competes at the top
   level rather than being trapped under it. */
.prefs {
  position: absolute;
  top: 0.55rem;
  right: 0.6rem;
  z-index: 30;
}

.gear {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ink-soft);
  cursor: pointer;
  transition: background 0.16s ease, transform 0.16s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.gear:active {
  transform: scale(0.88);
}

.gear.on {
  background: var(--paper);
  box-shadow: var(--shadow);
}

.prefs-menu {
  position: absolute;
  top: 48px;
  right: 0;
  /* Never wider than the phone it is on, with a thumb's width to spare either
     side. At 375px this settles at about 336px. */
  width: min(21rem, calc(100vw - 1.2rem));
  /* 36rem is the whole menu with nothing cut off — at 34rem it came six pixels
     short and scrolled on the phone this is built for, which is the worst of
     both. On anything shorter the 72vh takes over and it scrolls properly. */
  max-height: min(72vh, 36rem);
  overflow-y: auto;
  /* A flick inside the menu must not drag the two hundred rows behind it. */
  overscroll-behavior: contain;
  padding: 0.5rem;
  border-radius: 18px;
  background: var(--paper);
  box-shadow: 0 10px 34px rgba(93, 61, 51, 0.22);
  text-align: left;
}

.prefs-heading {
  margin: 0.55rem 0 0.3rem;
  padding: 0 0.35rem;
  font-size: 0.7rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* A switch, whose target is the whole row rather than the little track at the
   end of it. */
.switch {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  width: 100%;
  min-height: 46px;
  padding: 0 0.35rem;
  border: 0;
  border-radius: 12px;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 0.88rem;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
}

.switch-track {
  position: relative;
  flex: none;
  width: 46px;
  height: 27px;
  border-radius: 99px;
  background: var(--line);
  box-shadow: inset 0 0 0 1.5px var(--edge);
  transition: background 0.18s ease, box-shadow 0.18s ease;
}

.switch[aria-checked="true"] .switch-track {
  background: var(--pikachu);
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--on-pikachu) 25%, var(--pikachu));
}

.switch-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 21px;
  height: 21px;
  border-radius: 50%;
  background: var(--paper);
  box-shadow: 0 1px 3px rgba(93, 61, 51, 0.3);
  transition: transform 0.18s cubic-bezier(0.22, 1.2, 0.36, 1);
}

/* The knob's position is a transition off a plain rule, never an animation: it
   has to be right the instant the page paints, and right again if nothing ever
   moves. */
.switch[aria-checked="true"] .switch-knob {
  transform: translateX(19px);
}

.switch:focus-visible {
  box-shadow: var(--focus);
}

/* Ten palettes, two across. A single column would be four hundred pixels of menu
   before the language even appeared. */
.palette-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.35rem;
}

/* Each tile carries data-palette, so it is painted in the palette it offers —
   its own cream underneath, its own ink on top. It is a small picture of what
   the page is about to look like, rather than a label for it. */
.swatch-tile {
  position: relative;
  display: grid;
  gap: 4px;
  min-height: 62px;
  padding: 7px 8px;
  border: 1.5px solid var(--edge);
  border-radius: 12px;
  background: var(--cream);
  color: var(--ink);
  font: inherit;
  font-size: 0.72rem;
  font-weight: 700;
  line-height: 1.2;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.16s ease, transform 0.12s ease;
}

.swatch-tile:active {
  transform: scale(0.97);
}

/* Marked by its border and its tick, and *not* by tinting its background: the
   whole point of the tile is that it shows what the palette looks like, and a
   wash of the accent over it would make the chosen one the single tile that
   lies about its own colours. */
.swatch-tile.on {
  border-color: var(--blush);
  border-width: 2.5px;
  /* The border moved from 1.5px to 2.5px, so take the difference back out of the
     padding — otherwise the chosen tile is two pixels taller than its neighbours
     and the whole row shifts as she tries them. */
  padding: 6px 7px;
}

.swatch {
  display: flex;
  gap: 3px;
  height: 14px;
}

.swatch > span {
  flex: 1 1 auto;
  border-radius: 3px;
}

.swatch-a {
  background: var(--pikachu);
}

.swatch-b {
  background: var(--blush);
}

.swatch-c {
  background: var(--edge);
}

.swatch-d {
  background: var(--ink);
}

.swatch-name {
  padding-right: 20px;
}

/* Beside the name, not above it: up in the corner it landed on top of the
   swatch strip, and on half the palettes that put dark ink on a dark swatch. */
.swatch-check {
  position: absolute;
  right: 8px;
  bottom: 7px;
  width: 14px;
  height: 14px;
  color: var(--ink);
  /* Hidden rather than absent, so the name is laid out against the same width
     whether or not this palette is the chosen one. */
  visibility: hidden;
}

.swatch-tile.on .swatch-check {
  visibility: visible;
}

/* The two languages, in the same segmented control the filter bar uses. */
.lang-pick {
  width: 100%;
  background: var(--cream);
  box-shadow: inset 0 0 0 1.5px var(--line);
}

.lang-pick .seg {
  flex: 1 1 auto;
  min-height: 44px;
  font-size: 0.82rem;
}

/* Erasing the collection. The only control in this menu that is not a
   preference, so it is the only one set apart from them: its own rule above it,
   its own room, and danger colours rather than the menu's.

   Deliberately *not* full width and not in the column of switches. A row that
   looks like the rows above it is a row that gets tapped by a thumb aiming at
   the one above it, and this is the tap that cannot be taken back. */
.wipe {
  margin: 1rem 0 0.2rem;
  padding: 0.75rem 0.35rem 0.2rem;
  border-top: 1.5px solid var(--line);
}

.wipe-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}

.wipe-go {
  font-size: 0.76rem;
}

/* Said in full, in the danger ink, and only while it means something. Not
   animated and not timed: it is on screen for exactly as long as the button
   above it is armed, and `.note:empty` takes the row away entirely the rest of
   the time. */
.wipe .note {
  margin: 0.5rem 0 0;
  font-size: 0.72rem;
  line-height: 1.4;
  font-weight: 700;
  color: var(--on-danger);
  text-wrap: pretty;
}

/* The tick: squash, then overshoot back. The two names are identical — swapping
   between them is what replays the animation on the next tap. */
@keyframes tick-pop-a {
  /* Full size at 0%, for the same reason the ring is invisible there: a paused
     animation renders its first frame, and a permanently shrunken tick is not a
     resting state worth having. */
  0% {
    transform: scale(1);
  }
  8% {
    transform: scale(0.72);
  }
  52% {
    transform: scale(1.09);
  }
  100% {
    transform: scale(1);
  }
}

@keyframes tick-pop-b {
  /* Full size at 0%, for the same reason the ring is invisible there: a paused
     animation renders its first frame, and a permanently shrunken tick is not a
     resting state worth having. */
  0% {
    transform: scale(1);
  }
  8% {
    transform: scale(0.72);
  }
  52% {
    transform: scale(1.09);
  }
  100% {
    transform: scale(1);
  }
}

.tick[data-tap="a"] {
  animation: tick-pop-a 200ms cubic-bezier(0.22, 1.2, 0.36, 1);
}

.tick[data-tap="b"] {
  animation: tick-pop-b 200ms cubic-bezier(0.22, 1.2, 0.36, 1);
}

/* A ring thrown off the tick as it lands. */
/* Both ends are invisible, and so is the very first frame.
   
   A paused animation renders its first keyframe, and phones pause animations all
   the time — a backgrounded tab, Low Power Mode. If the ring were visible at 0%
   it would simply stay drawn, which is how it came to look as though a ticked card
   could not be unticked. Starting from nothing means every resting state, stalled
   or finished, is no ring at all. */
@keyframes tick-ring-a {
  0% {
    transform: scale(0.72);
    opacity: 0;
  }
  14% {
    opacity: 0.55;
  }
  100% {
    transform: scale(1.55);
    opacity: 0;
  }
}

@keyframes tick-ring-b {
  0% {
    transform: scale(0.72);
    opacity: 0;
  }
  14% {
    opacity: 0.55;
  }
  100% {
    transform: scale(1.55);
    opacity: 0;
  }
}

.tick[data-tap="a"]::before,
.tick[data-tap="b"]::before {
  content: "";
  position: absolute;
  inset: -4px;
  border: 2px solid var(--accent);
  border-radius: 50%;
  /* Invisible unless an animation is actively driving it.
  
     This used to rely on the animation ending at opacity 0, which meant that on
     any device where the animation does not run — Reduce Motion, Low Power Mode —
     the ring sat there permanently as a solid coloured circle around the tick.
     The card unticked correctly underneath, but it still *looked* ticked, so
     unticking appeared not to work at all. Never let a resting state depend on an
     animation having run. */
  opacity: 0;
  pointer-events: none;
}

/* No `forwards`. Both keyframe sets already end where the base rule above starts —
   invisible — so filling forwards bought nothing, and it cost the one thing worth
   avoiding here: an animation whose effect never ends, sitting permanently on a
   pseudo-element laid over the tick. The tick is the element that paints whether
   she owns the card; nothing decorative should be pinned to it for ever. */
.tick[data-tap="a"]::before {
  animation: tick-ring-a 360ms cubic-bezier(0.16, 0.8, 0.3, 1);
}

.tick[data-tap="b"]::before {
  animation: tick-ring-b 360ms cubic-bezier(0.16, 0.8, 0.3, 1);
}

/* A rarity filling up. Restrained on purpose: it happens nine times, and she is
   already looking at the meter when it does. */
@keyframes meter-done-a {
  0% {
    transform: scale(1);
  }
  28% {
    transform: scale(1.028);
    background: color-mix(in srgb, var(--accent) 26%, var(--paper));
  }
  100% {
    transform: scale(1);
  }
}

@keyframes meter-done-b {
  0% {
    transform: scale(1);
  }
  28% {
    transform: scale(1.028);
    background: color-mix(in srgb, var(--accent) 26%, var(--paper));
  }
  100% {
    transform: scale(1);
  }
}

.meter[data-done="a"] {
  animation: meter-done-a 520ms cubic-bezier(0.22, 0.9, 0.28, 1);
}

.meter[data-done="b"] {
  animation: meter-done-b 520ms cubic-bezier(0.22, 0.9, 0.28, 1);
}

/* The gear turns once as the menu opens. A whole turn on purpose: 0deg and
   360deg are the same picture, so the resting state is the same whether the
   animation finished, never started, or was paused on its first frame. Nothing
   about the menu itself moves — a menu that faded in would be invisible and
   still tappable on a phone that paused it, which is the one thing that must
   not happen to a control. */
@keyframes gear-turn-a {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

@keyframes gear-turn-b {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

/* The two marks at the top of the page are a pair and are read as one: a share
   and a gear, side by side. The generic .icon size is for marks that sit inside
   something else, where 18px is right; these two stand alone and match each
   other at 24. They looked a size apart because only one of them said so. */
.gear-mark,
.share-mark {
  width: 24px;
  height: 24px;
}

.gear-mark[data-turn="a"] {
  animation: gear-turn-a 420ms cubic-bezier(0.32, 0.9, 0.28, 1);
}

.gear-mark[data-turn="b"] {
  animation: gear-turn-b 420ms cubic-bezier(0.32, 0.9, 0.28, 1);
}

/* ---------- confetti ---------- */

.confetti {
  position: fixed;
  inset: 0;
  z-index: 40;
  pointer-events: none;
  overflow: hidden;
}

.burst {
  position: absolute;
  top: 38%;
  left: 50%;
}

.bit {
  position: absolute;
  width: var(--size, 8px);
  height: var(--size, 8px);
  margin: calc(var(--size, 8px) / -2);
  border-radius: 50%;
  background: var(--c);
  opacity: 0;
  animation: fling 1100ms cubic-bezier(0.12, 0.8, 0.26, 1) var(--delay) forwards;
}

/* A few squares among the circles, so the finale reads as paper rather than dots. */
.bit.square {
  border-radius: 2px;
}

/* The finale throws further, for longer, and in three waves — the delays are set
   per piece, so the last wave is still in the air as the first comes down. */
.burst.finale .bit {
  animation-duration: 1900ms;
}

/* Only the once, so it can afford to be said out loud. */
.finale-word {
  position: absolute;
  top: 0;
  left: 0;
  transform: translate(-50%, -50%);
  padding: 0.5rem 1.1rem;
  border-radius: 99px;
  background: rgba(38, 26, 24, 0.9);
  color: #fff;
  font-family: var(--display);
  font-size: 1.05rem;
  font-weight: 800;
  white-space: nowrap;
  opacity: 0;
  animation: finale-word 2200ms cubic-bezier(0.22, 0.9, 0.28, 1) 120ms forwards;
}

/* Invisible at both ends, like everything else here: a paused animation must not
   leave a banner sitting on the screen. */
@keyframes finale-word {
  0% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(0.86);
  }
  10% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
  72% {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }
  100% {
    opacity: 0;
    transform: translate(-50%, -50%) scale(1.06);
  }
}

@keyframes fling {
  /* Starts invisible for the same reason the ring does: a paused animation would
     otherwise leave the pieces sitting on the screen. */
  0% {
    transform: rotate(var(--a)) translateY(0) rotate(0deg) scale(0.4);
    opacity: 0;
  }
  6% {
    opacity: 1;
  }
  70% {
    opacity: 1;
  }
  100% {
    transform: rotate(var(--a)) translateY(calc(var(--d) * -1)) rotate(var(--spin)) scale(1);
    opacity: 0;
  }
}

/* Everything above is decoration. If she has asked her phone to calm down, it
   calms down — the sound and the buzz still carry the feedback. */
@media (prefers-reduced-motion: reduce) {
  .tick[data-tap],
  .meter[data-done],
  .gear-mark[data-turn],
  .bit,
  .finale-word {
    animation: none !important;
  }
  /* Belt and braces: with no animation there is nothing for the ring to do. */
  .tick[data-tap]::before {
    display: none;
  }
  .confetti {
    display: none;
  }
}

/* The short code, both where it is handed out and where it is pasted back in:
   spaced out and unambiguous, because she may well be reading it off one screen
   and typing it into another. Now that it is the only typed route, the two ends
   of that journey are set identically on purpose. */
.sharecode {
  font-size: 0.92rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-align: center;
  text-transform: uppercase;
}

.sharecode::placeholder {
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
}

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

.viewswap {
  flex: none;
  display: grid;
  place-items: center;
  width: 44px;
  border: 0;
  border-radius: 99px;
  background: var(--paper);
  box-shadow: var(--shadow);
  color: var(--ink-soft);
  cursor: pointer;
}

/* The two faces of this button are the same frame with its dividing lines moved,
   so at one size they trade places without the button appearing to twitch. */
.viewswap .icon {
  width: 21px;
  height: 21px;
}

.cards.grid {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.5rem;
}

.tile {
  position: relative;
  border-radius: 10px;
  background: none;
  box-shadow: none;
}

.tile-art {
  position: relative;
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  touch-action: manipulation;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  border-radius: 10px;
  background: var(--line);
  /* Every Pokémon card is this shape, so the tile can be too — no letterboxing. */
  aspect-ratio: 5 / 7;
  overflow: hidden;
  cursor: pointer;
  transition: transform 0.18s cubic-bezier(0.22, 1.2, 0.36, 1);
}

.tile-art:active {
  transform: scale(0.96);
}

.tile-art img {
  pointer-events: none;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* A card she does not have sits in the dark until she finds it. Lit or unlit
     the moment the class changes, like everything else that reports state. */
  filter: saturate(0.25) brightness(0.86);
}

.tile.owned .tile-art img {
  filter: none;
}

.tile.owned .tile-art {
  box-shadow: 0 0 0 2.5px var(--accent), 0 4px 12px rgba(93, 61, 51, 0.18);
}

/* The corner control, so a tap meant for the picture never claims the card.
   Bottom left on purpose: a Pokémon card carries its name across the top, and
   only the set and artist credits down here. */
.tile-tick {
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  position: absolute;
  bottom: -4px;
  left: -4px;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.tile-tick > * {
  pointer-events: none;
}

.tile-tick .tick {
  width: 24px;
  height: 24px;
  border-width: 2.5px;
  background: var(--cream);
  box-shadow: 0 1px 4px rgba(93, 61, 51, 0.25);
}

/* The checkmark the list grew when its circle did, held back here: this circle
   is 24px and did not grow with it. */
.tile-tick .tick::after {
  width: 11px;
  height: 6px;
  border-width: 0 0 2.8px 2.8px;
}

.tile.owned .tick {
  border-color: color-mix(in srgb, var(--accent) 80%, #6b5148);
  background: color-mix(in srgb, var(--accent) 88%, #6b5148);
}

.tile-no {
  position: absolute;
  right: 4px;
  bottom: 4px;
  padding: 0.05rem 0.3rem;
  border-radius: 6px;
  background: rgba(46, 33, 30, 0.72);
  color: #fff;
  font-size: 0.62rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

@media (min-width: 640px) {
  .cards.grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

@media (min-width: 1024px) {
  .cards.grid {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
}

/* ---------- the close-up ---------- */

.viewer {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: 1rem;
  /* Room kept at the foot for the undo pill, which comes up at the instant she
     ticks a card — which is the instant she is looking at the two buttons in here.
     The pill is raised above this overlay (see `.toast-layer`) so it can still be
     read and still be pressed; without a band reserved for it, it would land on
     top of them. Reserved always, rather than only while the pill is up: a
     close-up that jumped sixty pixels the moment she ticked would be a worse
     answer than one that sits slightly high. */
  padding-bottom: calc(3.9rem + max(0.9rem, env(safe-area-inset-bottom)));
  background: rgba(34, 23, 21, 0.8);
  /* The dark is painted twice: once inside this box, and once as a spread shadow
     beyond every edge of it. A fixed box is the size of the viewport as the
     browser understood it when the box was laid out, and a phone changes its mind
     — Chrome slides its address bar away and the visual viewport grows underneath
     a box that does not grow with it, leaving a band at the bottom undimmed with a
     hard edge across it. Holding the page still (see viewer.rs) is what stops that
     happening; this is what stops it mattering if it happens anyway, on a browser
     or a gesture nobody here has. An outer shadow paints only outside the border
     box, so the two never overlap and the 80% never doubles into a seam.

     A plain spread, deliberately: not a blend mode, not a filter, neither of which
     this app is allowed to use and either of which would be the thing that stopped
     the overlay painting at all. */
  box-shadow: 0 0 0 100vmax rgba(34, 23, 21, 0.8);
  overscroll-behavior: contain;
}

/* Deliberately on the contents rather than the overlay. Fading the overlay itself
   would mean a paused animation leaves an invisible sheet that still swallows every
   tap; a card that has not finished rising is merely a card slightly out of place. */
@keyframes viewer-in {
  from {
    transform: translateY(8px);
  }
  to {
    transform: none;
  }
}

.viewer-inner {
  /* How wide the card is, said once. The controls underneath line their edges up
     with it, and two places agreeing about a number by coincidence is exactly how
     they stop agreeing. */
  --card-w: min(78vw, 20rem);
  animation: viewer-in 180ms cubic-bezier(0.22, 0.9, 0.28, 1);
  display: grid;
  justify-items: center;
  gap: 0.7rem;
  width: min(100%, 26rem);
}

/* The stage holds the perspective; the card inside it does the leaning, so the
   glare and the shine lean with it rather than sliding over a flat picture. */
.viewer-stage {
  width: var(--card-w);
  perspective: 1100px;
}

.viewer-card {
  position: relative;
  border-radius: 4.6% / 3.3%;
  overflow: hidden;
  /* Without this the shine and the glare blend against the whole page behind the
     overlay rather than against the card, and wash the entire screen out. */
  isolation: isolate;
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  /* Deliberately NOT preserve-3d. The shine and the glare are flat layers over the
     artwork, and preserve-3d stops the card flattening them into itself — which
     defeats the isolation above and leaves both blending against the whole page. */
  transform-style: flat;
  box-shadow:
    0 18px 40px rgba(20, 12, 10, 0.45),
    0 0 0 1px rgba(255, 255, 255, 0.08);
  transition: transform 340ms cubic-bezier(0.22, 0.9, 0.28, 1);
  will-change: transform;
}

/* While a finger is down, the card answers immediately. */
.viewer-stage.held .viewer-card {
  transition: transform 60ms linear;
}

/* No `.viewer-card img` rule: the picture and the stand-in are sized together by
   `.art-slot` above, and a second opinion here about the height is how the card
   collapsed to nothing on the day the picture did not arrive. */

/* The holographic layers.
   
   No blend modes anywhere, and that is deliberate. `mix-blend-mode` is the obvious
   tool for a foil effect, and `background-blend-mode` the obvious fallback — both
   break compositing in the same way `backdrop-filter` did earlier in this app:
   the overlay's own backdrop stops painting and the page shows straight through.
   Three different features, one failure, so the pattern is not worth arguing with
   on something that has to work on a phone nobody can debug.

   What is left is plain alpha, which cannot break. A radial gradient whose colour
   stops *are* the spectrum gives a pool of refracted light that follows the
   pointer, which is what a foil card actually looks like when you tilt it, and a
   faint fixed streak underneath stands in for the grain of the foil. */

.viewer-shine {
  position: absolute;
  inset: 0;
  opacity: var(--shine, 0.3);
  background:
    radial-gradient(
      circle at var(--mx, 50%) var(--my, 50%),
      rgba(255, 255, 255, 0.5) 0%,
      rgba(255, 119, 115, 0.3) 16%,
      rgba(255, 237, 95, 0.26) 28%,
      rgba(168, 255, 95, 0.24) 40%,
      rgba(131, 255, 247, 0.24) 52%,
      rgba(120, 148, 255, 0.26) 64%,
      rgba(216, 117, 255, 0.22) 76%,
      rgba(216, 117, 255, 0) 92%
    ),
    repeating-linear-gradient(
      108deg,
      rgba(255, 255, 255, 0.07) 0 6px,
      rgba(255, 255, 255, 0) 6px 14px
    );
  pointer-events: none;
}

/* The plain highlight: this is the part that reads as a real object catching a
   light, and it is doing most of the work. */
.viewer-glare {
  position: absolute;
  inset: 0;
  opacity: 0.55;
  background: radial-gradient(
    circle at var(--mx, 50%) var(--my, 50%),
    rgba(255, 255, 255, 0.7) 0%,
    rgba(255, 255, 255, 0.16) 26%,
    rgba(255, 255, 255, 0) 55%
  );
  pointer-events: none;
}

.viewer-words {
  text-align: center;
  color: #fff;
}

.viewer-name {
  margin: 0;
  font-family: var(--display);
  font-size: 1.15rem;
  font-weight: 800;
}

.viewer-alt {
  margin: 0.1rem 0 0;
  font-size: 0.85rem;
  font-style: italic;
  opacity: 0.8;
}

.viewer-meta {
  margin: 0.2rem 0 0;
  font-size: 0.76rem;
  font-weight: 700;
  opacity: 0.75;
}

/* The two under the card are not a pair of equals, and they used to be drawn as
   one: same width rule, same fill weight, same size of word. Claiming the card is
   what she opened the close-up to do; closing is the way out, and the way out does
   not need to shout.

   So the claim takes whatever width is left and the close takes only what its own
   word needs. That also fixes something the eye was reading as a centring fault:
   the claim's label turns over between "Marcar como minha" and "Tenho esta", and
   with both buttons sized to their text the close slid fifteen pixels sideways
   underneath her thumb at the exact moment she ticked. Now the claim absorbs the
   difference and the close does not move at all.

   As wide as the card above, not as wide as the column: the controls belong to
   the card, and lining their edges up with its edges is what says so.

   `nowrap`, and that is the point rather than an oversight. Allowed to wrap, the
   row folds the close onto a second line at 320px in Portuguese — where "Marcar
   como minha" is a dozen pixels too long — and then unfolds the moment she ticks,
   which is the same button jumping about under her thumb by another route. One
   row always; the claim gives up width instead, and sets its words on two lines if
   that is what it takes. */
.viewer-actions {
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  gap: 0.5rem;
  width: var(--card-w);
}

.viewer-tick,
.viewer-close {
  /* Comfortably past the 44px anyone can hit, on the two controls that matter
     most on this screen. */
  min-height: 48px;
  border: 0;
  border-radius: 99px;
  font: inherit;
  cursor: pointer;
}

/* The one she came for: the wide one, the filled one, the loud one.

   `min-width: 0` is what lets it give width up rather than shove the close off the
   row — a flex item refuses to shrink past its own words without it. The padding
   is stated on all four sides because the words are allowed to take two lines at
   the narrowest sizes, and a line that wraps against the top of the pill is worse
   than a line that wraps. */
.viewer-tick {
  flex: 1 1 auto;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.5rem 0.9rem;
  background: var(--paper);
  color: var(--ink);
  font-size: 0.92rem;
  font-weight: 800;
  line-height: 1.2;
  box-shadow: 0 6px 16px rgba(20, 12, 10, 0.34);
}

.viewer-tick-mark {
  width: 17px;
  height: 17px;
}

/* Claimed.

   The fill is the card's own rarity colour, lightened until it can carry a dark
   word. At full strength it cannot: three of the ten rarity accents fail on
   `#3a2c22` — the RGB Mews' red worst at 3.16:1 — and this has to be legible for
   every rarity, not for the Pikachu yellow it happens to have been looked at in.

   Said twice, and the second is the one that matters. Mixing 62% toward white
   clears 4.5:1 for all ten accents as they stand, but only because all ten are
   mid-to-light: a *dark* accent mixed by the same fraction stays dark, and the
   word on it goes under. Fixing the lightness instead of the mixing fraction is
   the version that holds whatever colour arrives — hue and chroma carry the
   rarity, and the lightness is ours to set, so the contrast is a property of the
   rule rather than of the palette of the day. The `color-mix` stays underneath as
   the answer for a browser without relative colour, where it is right for every
   accent that exists today and no worse than what it replaced for any other.

   The ring puts the colour back at full strength, where there is nothing to be
   read on top of it, and it is what makes claimed unmistakable at a glance rather
   than a shade of pale.

   `var(--accent, …)` with the palette's own lead as the fallback, everywhere: the
   accent is written on `.viewer-inner` now and this is an ancestor away from it,
   which is exactly the distance at which it went missing last time. If it ever
   goes missing again the button is a pale pill instead of a hole.

   Nothing here is transitioned, and nothing brings the class in by animation. What
   this button looks like *is* the answer to "do I own this card", and a paused
   animation renders its first frame — which is how ticked cards spent a fortnight
   looking impossible to untick on her phone. */
.viewer-tick.on {
  background: color-mix(in srgb, var(--accent, var(--pikachu)) 62%, #fff);
  background: oklch(from var(--accent, var(--pikachu)) clamp(0.86, l, 0.93) c h);
  color: #3a2c22;
  box-shadow:
    0 0 0 2px var(--accent, var(--pikachu)),
    0 6px 16px rgba(20, 12, 10, 0.34);
}

/* The way out. Present, reachable, plainly the quieter of the two: a hairline to
   sit on instead of a fill, a smaller word, and only the width that word needs. */
.viewer-close {
  flex: 0 0 auto;
  padding: 0 1.05rem;
  background: rgba(255, 255, 255, 0.09);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.38);
  color: #fff;
  font-size: 0.82rem;
  font-weight: 700;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .viewer-card {
    transform: none !important;
  }
  .viewer-inner {
    animation: none;
  }
}

/* ---------- the Pikachu run ---------- */

/* Thirty of the cards carry a second number counting their own run, printed beside
   a pair of ears. Shown alongside the set number here, never instead of it, so a
   card can still be found by either.

   In the list the ears come off it. They were drawn twice on those thirty rows —
   once in this pill and once as the rarity mark at the right edge, which for a
   Pikachu Rare *is* a pair of ears — and she asked for the repeat to go. It goes
   from the number rather than from the mark, because the mark is a system: all
   199 rows carry one, in the same vocabulary the printed checklist and the ten
   progress meters use, and knocking it off thirty of them to keep a duplicate
   would leave a hole exactly where the set is most itself. What makes this pill
   say "Pikachu run" without the drawing is the yellow it is painted in — the only
   pill in the row wearing it — and the rarity standing under it on the next line.

   The tile and the close-up keep their ears: neither shows a rarity mark beside
   them, so neither repeats anything. */

.run-mark {
  display: inline-grid;
  place-items: center;
  width: 13px;
  height: 13px;
}

.run-mark svg {
  width: 100%;
  height: 100%;
}

.card-run {
  flex: none;
  /* Out to the corner. The name's cell ends 24px short of where the mark's right
     edge is — it used to be because the cell stopped at the mark's column, and it
     is now because the cell reaches the row's edge and gives that 24px back as
     padding — so handing the same 24px forward as a negative margin puts this
     pill's right edge exactly on the mark's, measured at 0.00px apart. It cannot
     overlap the name: they are flex siblings, so the name's width is computed from
     this one's and ellipsises before it can reach it, and the margin only moves
     where this is painted. */
  margin-right: -24px;
  /* Up into the row's padding, which is the only place the clearance was to be
     found. This pill is 18.1px tall and runs 4.2 to 22.3 with the lift below.
     The mark used to be centred down the whole row, which started it 26.2px down
     and left 3.9px between the two — a gap you can just see. The mark stands on
     the meta line now and starts 29.3px down, so the gap is 7.0px and this is no
     longer the tight spot it was. The lift stays: it is what made 3.9 out of 0.8,
     and if the mark ever goes back up the row this is the reason it still clears.
     Her own drawing does not clear at all: her pill runs 6.4 to 32.9 and her ears
     start at 32.1, so the two touch. It is the same problem, and the row is not
     tall enough for the corner she wants unless the corner is used. */
  margin-top: -4px;
  padding: 0.11rem 0.45rem;
  border-radius: 99px;
  background: color-mix(in srgb, #ffcb05 26%, #fff);
  color: #4a3a33;
  font-size: 0.76rem;
  /* Tighter than the line it inherits: every pixel of this pill's height is a
     pixel off the gap under it. */
  line-height: 1.2;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.tile-run {
  position: absolute;
  top: 4px;
  right: 4px;
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  padding: 0.05rem 0.3rem;
  border-radius: 6px;
  background: rgba(255, 203, 5, 0.94);
  color: #4a3a10;
  font-size: 0.6rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.viewer-run {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  margin: 0.25rem 0 0;
  font-size: 0.76rem;
  font-weight: 700;
  opacity: 0.85;
}

.viewer-run .run-mark {
  width: 15px;
  height: 15px;
}

/* ---------- progress, where she is looking ---------- */

/* The ring is at the top of the page and she is not. Once she is ticking her way
   down two hundred rows nothing up there answers her, so the same number lives in
   the sticky bar as well — a line that visibly grows under her finger. */

.bar-progress {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  height: 16px;
  margin-top: 5px;
}

.bar-track {
  flex: 1 1 auto;
  height: 4px;
  border-radius: 99px;
  background: var(--line);
  overflow: hidden;
}

.bar-fill {
  display: block;
  height: 100%;
  border-radius: 99px;
  background: linear-gradient(90deg, var(--pikachu), var(--blush));
  transition: width 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.bar-count {
  flex: none;
  font-size: 0.68rem;
  font-weight: 800;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* ---------- a way back ---------- */

.toast-layer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: max(0.9rem, env(safe-area-inset-bottom));
  /* Above the close-up, which is 60. The pill comes up the instant a card is
     ticked, and a card can be ticked from inside the close-up — so underneath it,
     the one control that undoes what she just did was behind an 80% scrim and
     could not be pressed at all. Below the import sheet, which is 70 and is the
     one thing in this app allowed to cover everything: nothing may distract from
     a choice about overwriting her collection. */
  z-index: 65;
  display: grid;
  place-items: center;
  padding: 0 0.9rem;
  /* The layer must never eat a tap meant for the list; only the pill itself does. */
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  max-width: 100%;
  padding: 0.45rem 0.45rem 0.45rem 0.9rem;
  border-radius: 99px;
  background: rgba(38, 26, 24, 0.94);
  color: #fff;
  box-shadow: 0 6px 20px rgba(20, 12, 10, 0.3);
  /* The pill is something to read, not something to press. Only the Undo button
     inside it takes a tap.

     It used to take them itself, and it appears at the bottom of the screen the
     instant she ticks a card — directly over the rows her thumb is already on,
     for a few seconds, exactly when she would reach back to untick something.
     Every one of those taps landed on the pill and did nothing. That is a second,
     quieter way for the app to look as though it is ignoring her, and it is worth
     closing even now that the first one is understood. */
  pointer-events: none;
  /* Only the position is animated, never the opacity: a paused animation should
     leave the toast a little low, not invisible and unpressable. */
  animation: toast-in 200ms cubic-bezier(0.22, 1.1, 0.36, 1);
}

@keyframes toast-in {
  from {
    transform: translateY(14px);
  }
  to {
    transform: none;
  }
}

.toast-text {
  font-size: 0.82rem;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.toast-text strong {
  font-weight: 800;
}

.toast-undo {
  /* Re-enabled inside a pill that does not take taps. */
  pointer-events: auto;
  flex: none;
  min-height: 34px;
  padding: 0 0.9rem;
  border: 0;
  border-radius: 99px;
  background: var(--pikachu);
  color: var(--on-pikachu);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 800;
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  .toast {
    animation: none;
  }
}

/* Quiet, but present on every page — it is a disclaimer, not a banner. */
.disclaimer {
  max-width: 34rem;
  margin: 0.5rem auto 0;
  font-size: 0.68rem;
  line-height: 1.45;
  color: var(--ink-faint);
  text-wrap: pretty;
}

/* ---------- the picture ----------

   The button lives beside the gear, and what it opens shows one thing: the QR
   code. Everything a screenshot needs to still make sense a year later is inside
   `.plate`, so a crop that catches the code catches the date and the count with
   it — and everything that is not the code is either one tap away or the
   quietest thing on the screen. */

.keep {
  position: absolute;
  top: 0.55rem;
  /* Beside the gear, which is 44px wide at the same right margin. */
  right: calc(0.6rem + 44px);
  z-index: 30;
}

/* Only the press is animated. Whether the sheet is open is a *state*, and a
   state that arrives by transition is a state that never arrives on a phone
   that has paused its animations — so `.on` below changes the background with
   no transition on it at all. */
.keep-open {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ink-soft);
  cursor: pointer;
  transition: transform 0.16s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.keep-open:active {
  transform: scale(0.88);
}

.keep-open.on {
  background: var(--paper);
  box-shadow: var(--shadow);
}

/* Room at the top right for the close cross, which sits over the corner rather
   than taking a row of its own. */
.keep-sheet {
  position: relative;
  padding-top: 1rem;
  text-align: center;
}

.sheet-close {
  position: absolute;
  top: 0.35rem;
  right: 0.35rem;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
}

.sheet-close:active {
  transform: scale(0.88);
}

.sheet-back {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  min-height: 40px;
  margin: 0 0 0.2rem -0.4rem;
  padding: 0 0.4rem;
  border: 0;
  background: none;
  color: var(--ink-soft);
  font: inherit;
  font-size: 0.8rem;
  font-weight: 800;
  cursor: pointer;
}

.sheet-back .icon {
  width: 18px;
  height: 18px;
}

/* The keepsake itself, and the only thing on the first face with a border round
   it. It wears the palette's own paper because the code on it is drawn in the
   palette's own ink — measured at 10.7:1 or better in all ten before it is drawn,
   in crates/data/src/qr.rs, which is what keeps a pretty picture a readable one. */
.plate {
  display: grid;
  justify-items: center;
  gap: 0.45rem;
  margin: 0 0 0.75rem;
  padding: 0.55rem 0.55rem 0.7rem;
  border-radius: var(--radius);
  border: 1.5px solid var(--line);
  background: var(--paper);
}

/* As wide as the sheet will give it. At 375px this is about 290px across, which
   is the width the error correction level was chosen against — see
   crates/data/src/qr.rs, where that number is asserted rather than assumed. */
.plate .qr {
  width: 100%;
}

.plate .qr svg {
  display: block;
  width: 100%;
  height: auto;
}

.plate-caption {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.1rem 0.45rem;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--ink-soft);
  text-align: center;
}

.plate-caption strong {
  font-family: var(--display);
  font-size: 0.78rem;
  color: var(--ink);
}

.plate-name,
.plate-count {
  color: var(--ink);
}

/* The one thing that tells two screenshots apart in a camera roll, so it is not
   allowed to be the faintest thing on the plate. */
.plate-date {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.7rem;
  letter-spacing: 0.02em;
}

/* The whole of the instructions: one sentence for keeping it, one for moving it.
   Anything longer than this line means the picture is not explaining itself. */
.keep-how {
  margin: 0 0 0.7rem;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--ink-soft);
  text-wrap: pretty;
}

/* The two ways further in, side by side and quiet — a name to share under, and
   the way through with no camera in the room. */
.keep-asides {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 0.2rem 1rem;
  margin: 0.7rem 0 0;
}

/* Quieter than a link elsewhere on purpose: these are the two ways *past* the
   thing she came for, and neither should be competing with it. Still underlined,
   because a coloured word that is not underlined is not obviously a control. */
.keep-asides .linkish {
  min-height: 34px;
  color: var(--ink-soft);
  font-size: 0.76rem;
  font-weight: 700;
  text-decoration-color: var(--edge);
  text-underline-offset: 3px;
}

/* True, and worth saying once, in the smallest type on the screen. */
.keep-public {
  margin: 0.8rem 0 0;
  font-size: 0.7rem;
  line-height: 1.4;
  color: var(--ink-faint);
  text-wrap: pretty;
}

/* Said only on an iPhone opened from the home screen icon. Quiet enough not to
   alarm, present enough to be read before she wonders why nothing happened. */
.keep-warn {
  margin: 0 0 0.7rem;
  padding: 0.5rem 0.6rem;
  border-radius: 12px;
  background: var(--cream);
  box-shadow: inset 0 0 0 1.5px var(--line);
  font-size: 0.72rem;
  line-height: 1.45;
  color: var(--ink-soft);
  text-wrap: pretty;
}

/* The second face: a short code, a link, and the way back in. Left-set, because
   it is all labels and fields rather than one picture. */
.words {
  text-align: left;
}

.words-heading {
  margin: 1rem 0 0.4rem;
  font-family: var(--display);
  font-size: 0.86rem;
  font-weight: 800;
  color: var(--ink);
}

/* ---------- whose collection this is ---------- */

/* The name, directly above the code it goes into.

   Quiet on purpose. It sits at the top of the sheet, which is the loudest place
   on it, and the code underneath has to stay the thing the eye lands on — so no
   border, no label of its own, no button beside it, and a placeholder in the
   faint ink rather than a prompt in the soft one. Somebody who does not want to
   name anything reads one grey line and never touches it.

   Centred, because what it fills in appears centred in the caption three lines
   below, and the two should look like the same thing said twice. */
.whose-input {
  display: block;
  width: auto;
  min-height: 42px;
  /* Stops short of the close cross rather than passing under it, which turns the
     two into one header row instead of two things sharing a corner. */
  margin: 0 2.6rem 0.6rem 0;
  padding: 0.35rem 0.6rem;
  border: 0;
  border-radius: 12px;
  background: var(--cream);
  color: var(--ink);
  font: inherit;
  font-family: var(--display);
  font-size: 0.86rem;
  font-weight: 700;
  text-align: center;
}

.whose-input::placeholder {
  color: var(--ink-faint);
  font-weight: 600;
}

.whose-input:focus {
  outline: none;
  box-shadow: var(--focus);
}

.the-link {
  font-size: 0.68rem;
  word-break: break-all;
}

/* ---------- a link arriving ----------

   Nothing here happens on its own. Overwriting a real collection by accident is
   the one unforgivable failure in this feature, so every path out of this sheet
   is a tap she made. */

.sheet-scrim {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  padding: 1rem;
  background: rgba(34, 23, 21, 0.8);
  overscroll-behavior: contain;
}

.sheet {
  width: min(100%, 26rem);
  max-height: min(86vh, 44rem);
  overflow-y: auto;
  padding: 1rem 1.1rem 1.1rem;
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: 0 12px 34px rgba(34, 23, 21, 0.3);
  text-align: left;
}

.sheet-title {
  margin: 0 0 0.2rem;
  font-family: var(--display);
  font-size: 1.05rem;
  font-weight: 800;
  color: var(--ink);
  text-wrap: balance;
}

.sheet-count {
  margin: 0 0 0.4rem;
  font-size: 0.86rem;
  font-weight: 700;
  color: var(--ink-soft);
}

.sheet-why,
.sheet-diff {
  margin: 0 0 0.8rem;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--ink-soft);
  text-wrap: pretty;
}

.sheet-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.8rem;
}

/* ---------- looking at somebody else's ----------

   The banner is sticky rather than fixed: fixed would sit over the list on a
   phone, and this has to be readable without being in the way of what it is
   describing. It is above everything it needs to be above and nothing else. */

.visiting {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem 0.6rem;
  padding: 0.5rem 0.9rem;
  background: var(--pikachu);
  color: var(--on-pikachu);
  font-size: 0.78rem;
  font-weight: 800;
}

.visiting-who {
  min-width: 0;
  overflow-wrap: anywhere;
}

.visiting-safe {
  font-weight: 600;
  opacity: 0.85;
}

.visiting-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  min-width: 0;
}

/* The banner is painted in the accent, and a plain `.btn` is filled with the same
   accent — on here it was invisible, a word floating on the stripe. Both buttons
   read as paper on the accent instead: the way back is the solid one, comparing is
   the outline. */
.visiting-actions .btn {
  min-height: 34px;
  font-size: 0.72rem;
  background: var(--paper);
  color: var(--ink);
  box-shadow: none;
}

.visiting-actions .btn.ghost {
  background: transparent;
  color: var(--on-pikachu);
  box-shadow: inset 0 0 0 1.5px var(--on-pikachu);
}

/* A tap on a card does nothing while a guest collection is on screen — the
   refusal is in Tracker::write, not here. This only keeps a tap that does
   nothing from reading as a bug. `.card-hit` itself is untouched: its iOS
   hardening is load-bearing and none of it is any of this rule's business. */
.group.visiting .tick,
.group.visiting .mark {
  opacity: 0.55;
}

.group.visiting .card,
.group.visiting .tile {
  cursor: default;
}

/* ---------- the two side by side ---------- */

.compare-heads {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.7rem;
}

.compare-head {
  padding: 0.5rem;
  border-radius: 14px;
  background: var(--cream);
  text-align: center;
}

.compare-head strong {
  display: block;
  font-family: var(--display);
  font-size: 1.3rem;
  font-weight: 800;
  color: var(--ink);
}

.compare-head span {
  display: block;
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--ink-soft);
  overflow-wrap: anywhere;
}

.compare-head.theirs {
  box-shadow: inset 0 0 0 1.5px var(--edge);
}

.compare-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.45rem;
}

.compare-row {
  display: grid;
  grid-template-columns: 18px 1fr;
  grid-template-areas:
    "mark name"
    "bars bars";
  align-items: center;
  gap: 0.1rem 0.4rem;
}

.compare-mark {
  grid-area: mark;
  display: block;
  width: 16px;
  height: 16px;
  color: var(--accent);
}

.compare-mark svg {
  display: block;
  width: 100%;
  height: 100%;
}

.compare-name {
  grid-area: name;
  font-size: 0.72rem;
  font-weight: 800;
  color: var(--ink-soft);
}

.compare-bars {
  grid-area: bars;
  display: grid;
  gap: 0.15rem;
}

.compare-bar {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.compare-track {
  flex: 1;
  height: 7px;
  border-radius: 99px;
  background: var(--line);
  overflow: hidden;
}

.compare-fill {
  display: block;
  height: 100%;
  border-radius: 99px;
  background: var(--accent);
}

/* Hers reads as an outline against theirs, so which row is which is legible
   without a legend and without colour being the only thing carrying it. */
.compare-bar.mine .compare-fill {
  background: var(--ink-soft);
}

.compare-n {
  flex: none;
  min-width: 3.2rem;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.66rem;
  color: var(--ink-soft);
  text-align: right;
}
