/* ==========================================================================
   Pokemon card store

   White, black, grey. One typeface. Hairline rules instead of boxes, and
   nothing coloured competing with the cards - the cards are the only colour
   on the page, which is the point of a shop that sells them.

   --page is the background, --ink is the text. They are named by their job
   rather than their colour so the file still reads correctly if the theme is
   ever flipped back.
   ========================================================================== */

/* ------------------------------------------------------------------ tokens */

:root {
  --page: #fff;
  --surface: #fafafa;
  --surface-2: #f2f2f2;
  --surface-3: #e8e8e8;

  --line: #e9e9e9;
  --line-2: #dcdcdc;
  --line-3: #bdbdbd;

  /* Near-black rather than #000: pure black on pure white vibrates, and this
     is indistinguishable from black while being easier to read for long. */
  --ink: #0a0a0a;
  --grey-1: #4a4a4a;   /* secondary text - 8.9:1 on white */
  --grey-2: #6e6e6e;   /* labels, meta   - 5.3:1          */
  --grey-3: #767676;   /* faint, placeholders - 4.5:1     */

  --good: #157a4c;
  --bad: #c0392b;

  --r: 6px;
  --r-lg: 10px;

  /* No webfont. On a Mac and on an iPhone this resolves to SF Pro, which is
     better drawn than anything we could pull off a CDN - and it costs no
     network request, so the shop still looks right if fonts.googleapis.com
     is slow or blocked. */
  --ui: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto,
        "Helvetica Neue", Arial, sans-serif;

  --ease: cubic-bezier(.2, .7, .3, 1);
  --pad: clamp(18px, 4vw, 40px);

  /* Named by their job rather than their colour, so the dark block below only
     has to change what they point at. --glass and --shade are the two that make
     that possible: everything translucent asks for a colour to lighten with and
     a colour to cast a shadow in, and in the dark those swap over. */
  --glass: 255, 255, 255;
  --veil: 255, 255, 255;
  --shade: 10, 10, 10;
  --stroke: #000;
  --scheme: light;
}

/*
 * Dark.
 *
 * Written once, applied two ways: to anyone whose device asks for it and has
 * not chosen otherwise, and to anyone who pressed the switch. The
 * :not([data-theme="light"]) guard is what lets the switch win over the device
 * in both directions - without it, choosing light on a dark phone would do
 * nothing.
 *
 * The greys are not the light ones inverted. Text on a dark ground reads
 * heavier than the same contrast ratio on a light one, so the secondary greys
 * sit closer to the background than their light-mode counterparts do, and --ink
 * is off-white rather than white for the same reason pure black was avoided on
 * white.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --page: #0b0b0c;
    --surface: #141416;
    --surface-2: #1c1c1f;
    --surface-3: #26262a;

    --line: #26262a;
    --line-2: #34343a;
    --line-3: #52525b;

    --ink: #f2f2f3;
    --grey-1: #b8b8bd;
    --grey-2: #99999f;
    --grey-3: #8a8a90;

    --good: #4ade80;
    --bad: #f87171;

    --glass: 255, 255, 255;
    --veil: 18, 18, 20;
    --shade: 0, 0, 0;
    --stroke: #fff;
    --scheme: dark;
  }
}

:root[data-theme="dark"] {
  --page: #0b0b0c;
  --surface: #141416;
  --surface-2: #1c1c1f;
  --surface-3: #26262a;

  --line: #26262a;
  --line-2: #34343a;
  --line-3: #52525b;

  --ink: #f2f2f3;
  --grey-1: #b8b8bd;
  --grey-2: #99999f;
  --grey-3: #8a8a90;

  --good: #4ade80;
  --bad: #f87171;

  --glass: 255, 255, 255;
  --veil: 18, 18, 20;
  --shade: 0, 0, 0;
  --stroke: #fff;
  --scheme: dark;
}

/* Form controls, scrollbars and the like follow along without being styled. */
html { color-scheme: var(--scheme); }

* { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: var(--ui);
  font-size: 15px;
  line-height: 1.55;
  letter-spacing: -.005em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }

/* Icons are Lucide symbols injected from /icons.svg. They inherit colour, and
   they always sit beside a word - if the sprite is missing nothing breaks. */
.icon-sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.icon { width: 1.05em; height: 1.05em; flex: none; display: block; }
.icon-brand { width: 22px; height: 22px; }
.icon-lg { width: 22px; height: 22px; }
.icon-xl { width: 30px; height: 30px; margin: 0 auto 20px; color: var(--line-3); }
.btn-icon { display: inline-flex; }
a { color: inherit; text-decoration: none; }

::selection { background: var(--ink); color: var(--page); }

:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 2px;
}

.wrap { width: min(1280px, 100% - (var(--pad) * 2)); margin-inline: auto; }

/* Every page ends with air under it. This used to come from the grid's own
   bottom padding, which meant the card page - which is not a grid - ended flush
   against the last line of text once the footer was removed. */
main { padding-bottom: clamp(56px, 8vw, 100px); }

/* Landing spot for the nav's Shop link. Without it the browser scrolls the
   filter bar to the very top of the window, which is underneath the sticky
   header. */
#shop { scroll-margin-top: 74px; }
@media (max-width: 700px) { #shop { scroll-margin-top: 120px; } }

/* ------------------------------------------------------------------ type */

h1, h2, h3 { margin: 0; font-weight: 550; letter-spacing: -.025em; line-height: 1.15; }

.display { font-weight: 550; letter-spacing: -.03em; line-height: 1.08; }

/* Small, wide, uppercase - the line along the bottom edge of a real card. */
.meta {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--grey-2);
}

.dim { color: var(--grey-1); }
.faint { color: var(--grey-3); font-size: 13px; }
.tnum { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------ header */

.topbar {
  position: sticky;
  top: 0;
  z-index: 60;
  background: rgba(var(--veil), .85);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid transparent;
  transition: border-color .25s;
}
.topbar.scrolled { border-bottom-color: var(--line); }

.topbar-inner {
  display: flex;
  align-items: center;
  gap: 20px;
  min-height: 62px;
  /* The glass nav is centred against this, not against the row. */
  position: relative;
}

.brand { display: flex; align-items: center; gap: 10px; }

.brand-mark {
  width: 22px;
  height: 22px;
  flex: none;
  display: grid;
  place-items: center;
  transition: transform .4s var(--ease);
}
.brand:hover .brand-mark { transform: rotate(-8deg) scale(1.05); }

.gate-mark { display: flex; margin-bottom: 16px; color: var(--ink); }
.gate-mark .icon { width: 26px; height: 26px; margin: 0; color: inherit; }

.brand-name { font-size: 15px; font-weight: 600; letter-spacing: -.01em; }

.topbar-spacer { flex: 1; }
.topbar .meta { display: none; }
@media (min-width: 820px) { .topbar .meta { display: block; } }

/* ------------------------------------------------------------- glass nav
   The three sections, in a pill that floats over whatever is behind it.

   Centred by taking it out of the flex row and pinning it to the middle of the
   bar. Putting it in the row would centre it between the brand and the
   buttons, which are different widths, so it would sit off-centre by however
   much they differ - and move every time the cart count changed.

   The glass is four things stacked, and it needs all four to read as glass
   rather than as a grey box: a backdrop blur, a background that is mostly
   transparent, a hairline border, and a highlight along the top edge where the
   light would catch. `saturate` is what stops the blur washing the colours
   underneath into grey. */
.glass-nav {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 4px;
  border-radius: 999px;
  z-index: 1;

  background: rgba(var(--veil), .55);
  border: 1px solid rgba(var(--glass), .22);
  box-shadow:
    0 1px 2px rgba(var(--shade), .04),
    0 8px 24px -12px rgba(var(--shade), .18),
    inset 0 1px 0 rgba(var(--glass), .9);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .glass-nav {
    background: rgba(var(--veil), .42);
    /* 12px, not 18, and no saturate.

       A backdrop-filter is re-computed every frame the thing behind it moves,
       which on a sticky bar means every frame of every scroll. The cost climbs
       with the blur radius, and saturate() is a second full-area pass on top of
       it for an effect nobody can name when it is gone. Twelve looks the same
       through frosted glass and costs a good deal less on a phone. */
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    /* Its own layer, so scrolling the page underneath does not force the bar to
       be re-rasterised along with it. */
    will-change: backdrop-filter;
  }
}

/* Without backdrop-filter - older Firefox, and anything with the effect turned
   off for battery - the rules above already leave a solid-enough pill. Nothing
   here depends on the blur to stay legible. */

.glass-nav a {
  position: relative;
  display: block;
  padding: 7px 15px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 550;
  letter-spacing: -.005em;
  color: var(--grey-1);
  white-space: nowrap;
  transition: color .18s var(--ease), background .18s var(--ease);
}
.glass-nav a:hover { color: var(--ink); background: rgba(var(--veil), .7); }

/* The current section. Drawn from aria-current so the marker and what a screen
   reader announces cannot drift apart. */
.glass-nav a[aria-current="page"] {
  color: var(--ink);
  background: rgba(var(--veil), .92);
  box-shadow: 0 1px 2px rgba(var(--shade), .07), inset 0 1px 0 rgba(var(--glass), .35);
}

.topbar-login { flex: none; }

/* ------------------------------------------------------------ dark switch

   A switch, not a button with a moon on it: it has two states and it should
   look like it has two states. role="switch" and aria-checked carry the same
   fact to anyone not looking at it. */
.theme-toggle {
  flex: none;
  appearance: none;
  border: 0;
  background: none;
  padding: 4px;
  margin-left: 2px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
}
.theme-track {
  position: relative;
  display: block;
  width: 38px;
  height: 22px;
  border-radius: 999px;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--line-2);
  transition: background .22s var(--ease), box-shadow .22s var(--ease);
}
.theme-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--page);
  box-shadow: 0 1px 2px rgba(var(--shade), .35), 0 0 0 1px var(--line-2);
  transition: transform .22s var(--ease), background .22s var(--ease);
}
.theme-toggle[aria-checked="true"] .theme-track {
  background: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}
.theme-toggle[aria-checked="true"] .theme-knob {
  transform: translateX(16px);
  background: var(--page);
}
.theme-toggle:hover .theme-track { box-shadow: inset 0 0 0 1px var(--line-3); }
.theme-toggle[aria-checked="true"]:hover .theme-track { box-shadow: inset 0 0 0 1px var(--ink); }

@media (prefers-reduced-motion: reduce) {
  .theme-track, .theme-knob { transition: none; }
}

/* The order lookup, which lives on the shop page and is only shown when Orders
   is chosen. */
/* Top margin as well as bottom: with the headline hidden on this view there is
   nothing else holding the panel clear of the sticky header, and it ends up
   tucked under it. */
.order-lookup { margin-top: clamp(24px, 5vw, 56px); margin-bottom: 22px; }
.order-line {
  display: flex;
  justify-content: space-between;
  gap: 14px;
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
}
.order-line:last-of-type { border-bottom: 0; }
.order-total {
  display: flex;
  justify-content: space-between;
  gap: 14px;
  margin-top: 12px;
  padding-top: 14px;
  border-top: 1px solid var(--line-2);
  font-weight: 600;
}

/* The wrapper exists only for the phone layout below. On a wide screen it is
   display:contents, so it forms no box at all and the nav inside it is
   positioned against the header exactly as if the wrapper were not there. */
.glass-nav-line { display: contents; }

/* On a phone the pill cannot be centred and also fit beside the brand and two
   buttons, so it drops to a line of its own - still a pill, still hugging its
   three words rather than stretching across the screen. Centring it needs the
   wrapper: a flex item told to fill the line is as wide as the line, and the
   glass would stretch with it. */
@media (max-width: 700px) {
  .topbar-inner { flex-wrap: wrap; padding-bottom: 10px; row-gap: 10px; gap: 10px; }
  .glass-nav-line {
    display: flex;
    justify-content: center;
    order: 3;
    flex: 1 0 100%;
  }
  .glass-nav { position: static; transform: none; }
  .topbar-login { padding-inline: 12px; }
}

/* At 320px the brand, the two buttons and the pill wrapped to three rows and
   the header ate a sixth of the screen. The mark alone is enough of a brand at
   that width - the page title says the rest. */
@media (max-width: 380px) {
  .topbar-inner { gap: 8px; }
  .brand-name { display: none; }
  .topbar-login, .cart-btn { padding-inline: 10px; }
  .glass-nav a { padding-inline: 12px; }
}

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

.btn {
  appearance: none;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  letter-spacing: -.005em;
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  padding: 8px 15px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  position: relative;
  transition: border-color .18s, background .18s, color .18s, opacity .18s;
}
.btn:hover { border-color: var(--line-3); background: var(--surface-2); }
.btn:active { background: var(--surface-3); }
.btn:disabled { opacity: .35; cursor: not-allowed; }

.btn-primary {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--page);
}
.btn-primary:hover { background: #262626; border-color: #262626; }
.btn-primary:active { background: #383838; }

.btn-quiet { border-color: transparent; color: var(--grey-1); }
.btn-quiet:hover { color: var(--ink); background: var(--surface-2); border-color: transparent; }

.btn-danger { color: var(--grey-1); }
.btn-danger:hover { color: var(--bad); border-color: var(--line-3); background: transparent; }

.btn-lg { padding: 12px 22px; font-size: 14px; }
.btn-block { width: 100%; }

/* ------------------------------------------------------------------ forms */

label.field { display: block; margin-bottom: 16px; }
label.field > span { display: block; margin-bottom: 7px; }

input[type="text"], input[type="number"], input[type="search"],
input[type="password"], input[type="email"], select, textarea {
  width: 100%;
  font: inherit;
  font-size: 14.5px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  padding: 10px 13px;
  transition: border-color .18s, background .18s;
}
input:hover, select:hover, textarea:hover { border-color: var(--line-3); }
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--grey-3);
  background: var(--surface-2);
}
::placeholder { color: var(--grey-3); }
textarea { resize: vertical; min-height: 80px; line-height: 1.5; }

select {
  appearance: none;
  cursor: pointer;
  padding-right: 34px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--grey-2) 50%),
    linear-gradient(135deg, var(--grey-2) 50%, transparent 50%);
  background-position: right 16px center, right 11px center;
  background-size: 5px 5px;
  background-repeat: no-repeat;
}

input[type="checkbox"] { width: auto; accent-color: var(--ink); }

.input-icon { position: relative; display: flex; }
.input-icon input { padding-left: 38px; }
.input-icon-slot {
  position: absolute;
  left: 13px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  color: var(--grey-3);
  pointer-events: none;
}
.input-icon:focus-within .input-icon-slot { color: var(--grey-1); }

.check-row {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-bottom: 18px;
  cursor: pointer;
}

.row { display: flex; gap: 12px; }
.row > * { flex: 1; min-width: 0; }

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

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: clamp(18px, 3vw, 26px);
}
.panel + .panel { margin-top: 14px; }

.panel-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 18px;
  padding-bottom: 15px;
  border-bottom: 1px solid var(--line);
}
.panel-head h2 { font-size: 17px; }
/* An action in a panel heading sits at the far end of it, away from the title
   and the count that belong together on the left. */
.panel-head .btn { margin-left: auto; align-self: center; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: 4px 9px;
  border-radius: 3px;
  border: 1px solid var(--line-2);
  color: var(--grey-2);
  white-space: nowrap;
}
.chip-strong { color: var(--ink); border-color: var(--line-3); }
.chip-live { color: var(--good); border-color: rgba(125, 211, 160, .28); }
.chip-dot::before {
  content: "";
  width: 4px; height: 4px;
  border-radius: 50%;
  background: currentColor;
}

.notice {
  border: 1px solid var(--line-2);
  border-left: 2px solid var(--line-3);
  border-radius: var(--r);
  padding: 11px 15px;
  font-size: 13.5px;
  color: var(--grey-1);
  margin-bottom: 14px;
}
/* These two carried pale colours left over from when the shop was dark - on
   white they measured 1.7:1 and 1.4:1, which is why "Wrong password." looked
   like placeholder text rather than an error. Now 5.2:1 and 5.1:1.

   The faint tint is the one place colour earns its keep in a monochrome shop:
   it makes a message read as a message rather than as another empty input. */
.notice-error {
  border-color: #f0d8d5;
  border-left-color: var(--bad);
  background: #fdf4f3;
  color: var(--bad);
}
.notice-ok {
  border-color: #cfe8db;
  border-left-color: var(--good);
  background: #f2faf6;
  color: var(--good);
}
.notice-info { border-left-color: var(--line-3); }

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

/* One line of type and nothing else, so it no longer needs to be a grid with
   a second column for the numbers that used to sit beside it. */
.masthead {
  padding: clamp(40px, 8vw, 88px) 0 clamp(24px, 3.5vw, 38px);
}

.masthead h1 {
  font-size: clamp(31px, 5.4vw, 56px);
  font-weight: 550;
  letter-spacing: -.035em;
  line-height: 1.04;
  margin: 0;
  max-width: 20ch;
}
.masthead h1 em { font-style: normal; color: var(--grey-3); }

/* ------------------------------------------------------------------- wave

   Black line waves along the bottom, bold and thin, and a row of cards in clear
   cases you can drag.

   Everything moves by `transform`, `opacity` or scroll position and nothing
   else. Those are the only things a browser can change without recalculating
   the page on every frame, which is what keeps a decoration on the front page
   from costing a phone its battery.
*/
.wave {
  --gap: clamp(14px, 2vw, 26px);
  position: relative;
  /* The full width of the window rather than of the text column, so the lines
     run off both edges instead of stopping at the margin. */
  width: 100vw;
  margin-left: 50%;
  translate: -50% 0;
  margin-top: clamp(22px, 3.5vw, 44px);
  height: clamp(190px, 26vw, 300px);
}

/* An empty shop: the waves alone, and less of them, so the front page is short
   and deliberate rather than a tall gap where the stock will go. */
.wave-bare { height: clamp(110px, 14vw, 170px); }
.wave-bare .wave-rail { display: none; }

/* ---- the line art ---- */

.wave-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
/* Pure black, at full strength. --ink is a near-black picked for text - easier
   on the eye over a paragraph - but a drawn line is not text, and next to a
   card's colour it wants to be the darkest thing on the page. */
.wave-lines .ln { fill: none; stroke: var(--stroke); vector-effect: non-scaling-stroke; }
/* Each line slides along its own length at its own speed. One period is 400,
   260 or 520 units wide and the paths repeat, so sliding by exactly one period
   loops with no seam. */
@keyframes drift-a { to { transform: translate(-400px, 252px); } }
@keyframes drift-b { to { transform: translate(-350px, 263px); } }
@keyframes drift-c { to { transform: translate(-720px, 243px); } }
@keyframes drift-d { to { transform: translate(-300px, 273px); } }

.drift-a { animation: drift-a 9s linear infinite; }
.drift-b { animation: drift-b 14s linear infinite; }
.drift-c { animation: drift-c 11s linear infinite; }
.drift-d { animation: drift-d 17s linear infinite; }

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

.wave-rail {
  position: absolute;
  inset: 0 0 auto 0;
  height: clamp(150px, 20vw, 240px);
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Sideways only: a drag on a phone must still be able to scroll the page
     vertically. */
  touch-action: pan-x;
  overscroll-behavior-x: contain;
  cursor: grab;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.wave-rail::-webkit-scrollbar { display: none; }
.wave-rail.dragging { cursor: grabbing; }

.wave-track {
  display: flex;
  align-items: center;
  gap: var(--gap);
  width: max-content;
  height: 100%;
  padding-inline: var(--gap);
}

/*
 * A rigid case with a card in it - unbranded, because it has to be.
 *
 * A grading company's slab is that company's trade dress: the frame, the label
 * band, the colour. Drawing one here would put a grader's mark on cards that
 * have not been graded, on a page whose whole job is to tell a buyer what they
 * are getting. So this is the case itself and nothing else - and when a card
 * *is* graded, its own photograph goes in and shows the real thing.
 *
 * What makes it read as a hard case rather than a rounded rectangle is depth:
 * a bevelled outer edge, a well the card sits down inside, and a shadow cast by
 * the lip of that well onto the card.
 */
.slab {
  position: relative;
  flex: none;
  width: clamp(76px, 10.8vw, 122px);
  aspect-ratio: 5 / 7.6;
  border-radius: 9px;
  /* The acrylic: a faint vertical gradient, because a flat fill reads as paper
     and a moulded surface never has one even tone. */
  background:
    linear-gradient(168deg,
      rgba(var(--glass), .92) 0%,
      rgba(246, 246, 248, .72) 34%,
      rgba(var(--glass), .86) 62%,
      rgba(238, 238, 242, .8) 100%);
  box-shadow:
    /* the moulded outer edge */
    inset 0 1px 0 rgba(var(--glass), .95),
    inset 0 0 0 1px rgba(var(--glass), .6),
    inset 0 -1px 0 rgba(var(--shade), .06),
    /* where the case ends and the air begins */
    0 0 0 1px rgba(var(--shade), .1),
    /* the weight of it */
    0 2px 3px -2px rgba(var(--shade), .3),
    0 18px 30px -20px rgba(var(--shade), .55);
  overflow: hidden;
  animation: bob 4.6s ease-in-out infinite alternate;
  animation-delay: calc(var(--i, 0) * -0.3s);
}

/* The well the card sits down inside. The inset shadow is the lip of the case
   above it - that one line is most of what makes it look like an object with a
   thickness rather than a picture with a border. */
.slab-card {
  position: absolute;
  inset: 6.5% 6.5% 7.5% 6.5%;
  border-radius: 3px;
  overflow: hidden;
  background: var(--surface-2);
  box-shadow:
    inset 0 2px 4px -1px rgba(var(--shade), .45),
    inset 0 0 0 1px rgba(var(--shade), .16),
    0 0 0 1px rgba(var(--glass), .5);
}
.slab-card img { width: 100%; height: 100%; object-fit: cover; display: block; }

/*
 * The shop's mark, moulded into the case rather than printed on the card.
 *
 * Low contrast and small on purpose: it should read as embossed plastic that
 * catches the light, which is what a real case's maker mark looks like, not as
 * a label stuck over the card. It sits on the case only - never over a
 * photograph, where the holder in the picture is somebody else's.
 */
.slab-mark {
  position: absolute;
  right: 7%;
  bottom: 2%;
  z-index: 2;
  font-size: clamp(5px, .62vw, 7px);
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: rgba(var(--shade), .3);
  text-shadow: 0 1px 0 rgba(var(--glass), .85);
  pointer-events: none;
}

/*
 * Your own photograph: no case drawn round it.
 *
 * The picture already shows the holder the card is really in - a sleeve, a
 * toploader, a graded slab with its label - so the drawn one comes off
 * entirely, leaving the photograph and the shadow under it. Anything else puts
 * a holder on the picture that the card in the picture is not in, which on a
 * page that tells a buyer what they are getting is the wrong way to be wrong.
 */
.slab-photo {
  background: none;
  aspect-ratio: 5 / 7.1;
  box-shadow: 0 18px 30px -20px rgba(var(--shade), .55);
}
.slab-photo .slab-card {
  inset: 0;
  border-radius: 7px;
  box-shadow: inset 0 0 0 1px rgba(var(--shade), .1);
}

/* An empty case - a new shop, or photos that have not loaded. Plain plastic,
   no artwork. */
.slab-blank .slab-card {
  background: linear-gradient(150deg, #f3f3f3, #e6e6e6 45%, #f6f6f6 72%, #eaeaea);
}

/* The wave the cards ride on: one rhythm, staggered along the row, so it rises
   and falls instead of every card bouncing at once. The ones at the bottom of
   the swing sit back a little, which is where the fade comes from. */
@keyframes bob {
  from { transform: translateY(8%) scale(.97); opacity: .55; }
  to   { transform: translateY(-8%) scale(1); opacity: 1; }
}

/* The reflection on the plastic, over the card rather than under it. */
.slab::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(112deg,
    transparent 38%, rgba(var(--glass), .5) 47%,
    rgba(var(--glass), .18) 54%, transparent 62%);
  animation: gleam 7s ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * -0.6s);
}
@keyframes gleam {
  0%, 100% { transform: translateX(-58%); }
  50%      { transform: translateX(58%); }
}

@media (prefers-reduced-motion: reduce) {
  /* Still a shelf of cards and a drawing of waves, just a still one. You can
     still drag it. */
  .wave-lines *, .slab, .slab::after { animation: none !important; }
  .slab { opacity: .92; transform: none; }
}

/* ------------------------------------------------------------------ filters */

.filterbar {
  position: sticky;
  top: 62px;
  z-index: 50;
  display: flex;
  gap: 9px;
  flex-wrap: wrap;
  align-items: center;
  padding: 14px 0;
  background: var(--page);
  border-bottom: 1px solid var(--line);
  margin-bottom: 26px;
}
.filterbar .search-field { flex: 2 1 240px; }
.filterbar-scroll { display: flex; gap: 9px; flex: 3 1 auto; flex-wrap: wrap; }
.filterbar select { flex: 1 1 140px; }

@media (max-width: 700px) {
  /* Stacked two-up, the dropdowns pushed the first card most of a screen down
     the page. They ride on one swipeable line instead: the search box above,
     everything else in a single row that scrolls sideways, so the shop starts
     where the shop starts. */
  .filterbar {
    position: static;
    display: block;
    padding: 12px 0;
    margin-bottom: 18px;
  }
  .filterbar .search-field { display: flex; margin-bottom: 9px; }
  .filterbar-scroll {
    display: flex;
    flex-wrap: nowrap;
    gap: 8px;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    /* Bleed to the screen edges so the row reads as scrollable rather than
       cut off, and the last control isn't trapped against the margin. */
    margin: 0 calc(var(--pad) * -1);
    padding: 0 var(--pad);
  }
  .filterbar-scroll::-webkit-scrollbar { display: none; }
  .filterbar select { flex: 0 0 auto; width: auto; }
}

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

.grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: clamp(18px, 2.6vw, 30px);
}

@media (max-width: 700px) {
  .grid { grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 16px; }
  .tile-name { font-size: 13.5px; }
  .tile-price { font-size: 14px; }
}

.grid > li { animation: rise .45s var(--ease) both; animation-delay: calc(var(--i, 0) * 30ms); }
@keyframes rise { from { opacity: 0; transform: translateY(12px); } }

.grid > li {
  content-visibility: auto;
  contain-intrinsic-size: auto 360px;
}

.tile { display: block; perspective: 1100px; }

.tile-inner {
  display: flex;
  flex-direction: column;
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform .55s var(--ease);
  will-change: transform;
}
.tile:hover .tile-inner { transition-duration: .1s; }

.tile-art {
  position: relative;
  aspect-ratio: 5 / 7;
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--line);
  transition: border-color .25s;
}
.tile:hover .tile-art { border-color: var(--line-3); }
.tile-art img { width: 100%; height: 100%; object-fit: cover; }
.tile-art.contain img,
.face.contain img {
  object-fit: contain;
  padding: 9%;
  background: var(--surface);
}

/* A soft white highlight that follows the pointer, like a lamp on a sleeve. */
.tile-art::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity .28s;
  mix-blend-mode: soft-light;
  background: radial-gradient(
    circle at var(--mx, 50%) var(--my, 50%),
    rgba(var(--glass), .35),
    rgba(var(--glass), .08) 32%,
    rgba(var(--glass), 0) 62%
  );
}
.tile:hover .tile-art::after { opacity: 1; }

.sold-veil {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(var(--glass), .8);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--grey-1);
}

.tile-body { padding: 14px 0 0; display: flex; flex-direction: column; gap: 5px; }
/* Two lines of name, always - reserved whether the name needs them or not, so
   the price row lands at the same height across the whole shelf. */
.tile-name {
  font-size: 14.5px;
  font-weight: 550;
  letter-spacing: -.012em;
  line-height: 1.3;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  min-height: 2.6em;
}

/* One line, always. A wrapped set name would stagger every price below it. */
/* Reserved like the name above it: a card with no set recorded must not pull
   its price up out of line with the rest of the shelf. */
.tile-body .meta {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-height: 1.55em;
}

.tile-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 4px;
}
/* The price never breaks across two lines to make room for a badge - it is the
   thing the eye is scanning down the column for. */
.tile-price { font-size: 15px; font-weight: 550; letter-spacing: -.015em; white-space: nowrap; }

/* Skeletons: the shape of the answer before the answer arrives. */
.skeleton .tile-art,
.skeleton .sk-line {
  background: linear-gradient(100deg, var(--surface) 30%, var(--surface-3) 50%, var(--surface) 70%);
  background-size: 260% 100%;
  animation: sweep 1.4s linear infinite;
  border-color: transparent;
}
.sk-line { height: 10px; border-radius: 3px; margin-top: 10px; }
.sk-line.short { width: 55%; }
@keyframes sweep { from { background-position: 130% 0; } to { background-position: -130% 0; } }

.empty {
  grid-column: 1 / -1;
  text-align: center;
  padding: 90px 20px;
  color: var(--grey-2);
}
.empty h3 { font-size: 17px; margin-bottom: 6px; color: var(--ink); }
.empty p { margin: 0; font-size: 14px; }

/* ------------------------------------------------------------------ card page */

.crumb { padding: 24px 0 0; }
.crumb a {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--grey-2);
  transition: color .18s;
}
.crumb a:hover { color: var(--ink); }

.detail {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(32px, 5vw, 68px);
  padding: clamp(26px, 4vw, 50px) 0 90px;
  align-items: start;
}
/*
 * The picture holds still while the details scroll past it - but only once
 * there is a second column for them to scroll past it in.
 *
 * The sticky was written for the wide layout and applied at every width. Below
 * 900px the two columns become one, so the picture stuck to the top of the
 * scroller and everything underneath - the name, the chips, the price, the Add
 * to cart button - slid up over the card and sat in the middle of the artwork.
 * On a phone that is not a subtle layout flaw, it is a buy button floating over
 * a Pokemon's face.
 *
 * So the sticky lives inside the same breakpoint as the second column. One
 * rule, one reason, and they cannot come apart again.
 */
@media (min-width: 900px) {
  .detail { grid-template-columns: minmax(0, 420px) minmax(0, 1fr); }
  .viewer { position: sticky; top: 92px; }
}
.stage { perspective: 1600px; }

.flipper {
  position: relative;
  aspect-ratio: 5 / 7;
  cursor: pointer;
  transform-style: preserve-3d;
  transform: rotateX(var(--rx, 0deg)) rotateY(calc(var(--ry, 0deg) + var(--flip, 0deg)));
  transition: transform .7s var(--ease);
  will-change: transform;
}
.flipper.tracking { transition-duration: .1s; }
.flipper.flipped { --flip: 180deg; }

.face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--line);
}
.face img { width: 100%; height: 100%; object-fit: cover; }
.face-back { transform: rotateY(180deg); }

.face::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: soft-light;
  background: radial-gradient(
    circle at var(--mx, 50%) var(--my, 50%),
    rgba(var(--glass), .3),
    rgba(var(--glass), .06) 34%,
    rgba(var(--glass), 0) 64%
  );
}

.face-empty .icon { margin: 0; }
.face-empty {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 12px;
  color: var(--grey-3);
  font-size: 12.5px;
  text-align: center;
  padding: 30px;
}

/* One tab per picture: Artwork, Front, Back. Underlined rather than boxed, so
   a row of three does not read as three buttons competing with Add to cart. */
.view-strip {
  display: flex;
  gap: 4px;
  justify-content: center;
  margin-top: 20px;
}

.view-tab {
  appearance: none;
  font: inherit;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--grey-2);
  background: transparent;
  border: 0;
  border-bottom: 1px solid transparent;
  padding: 7px 12px;
  cursor: pointer;
  transition: color .18s, border-color .18s;
}
.view-tab:hover { color: var(--ink); }
.view-tab.active { color: var(--ink); border-bottom-color: var(--ink); }

.detail h1 {
  font-size: clamp(28px, 4vw, 40px);
  font-weight: 550;
  letter-spacing: -.032em;
  margin-bottom: 14px;
}

.detail-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 28px; }

.price-block {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  padding: 22px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  margin-bottom: 26px;
}
.price-big {
  font-size: clamp(30px, 4vw, 38px);
  font-weight: 550;
  letter-spacing: -.03em;
  line-height: 1;
}

.buy-row { display: flex; gap: 10px; margin-bottom: 30px; }
.buy-row input { max-width: 82px; text-align: center; font-variant-numeric: tabular-nums; }
.buy-row .btn { flex: 1; }

.spec-block { margin-top: 34px; }
.spec-block > .meta { padding-bottom: 12px; border-bottom: 1px solid var(--line); }

.spec-list { list-style: none; padding: 0; margin: 0; }
.spec-list li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 20px;
  padding: 12px 0;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
}
.spec-list li:last-child { border-bottom: 0; padding-bottom: 0; }
.spec-list li > span:first-child { color: var(--grey-2); flex: none; }
.spec-list li > span:last-child { text-align: right; }

/* ------------------------------------------------------------------ related */

.related { padding-bottom: 90px; }
.related-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  border-top: 1px solid var(--line);
  padding: 34px 0 20px;
}
.related-head h2 { font-size: 17px; }
.related-head a { color: var(--grey-2); transition: color .18s; }
.related-head a:hover { color: var(--ink); }
.related-grid {
  padding-bottom: 0;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}

/* ------------------------------------------------------------------ drawer */

.drawer-backdrop {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: rgba(0, 0, 0, .35);
  backdrop-filter: blur(3px);
  animation: fade-in .25s var(--ease) both;
}
@keyframes fade-in { from { opacity: 0; } }

.drawer {
  position: fixed;
  inset: 0 0 0 auto;
  z-index: 71;
  width: min(430px, 100%);
  display: flex;
  flex-direction: column;
  background: var(--page);
  border-left: 1px solid var(--line-2);
  animation: drawer-in .38s var(--ease) both;
}
@keyframes drawer-in { from { transform: translateX(100%); } }

.drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px var(--pad);
  border-bottom: 1px solid var(--line);
}
.drawer-head h2 { font-size: 17px; }
.drawer-body { flex: 1; overflow-y: auto; padding: 4px var(--pad) 20px; }
.drawer-foot {
  padding: 18px var(--pad) calc(18px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
}

.cart-line {
  display: flex;
  gap: 14px;
  padding: 16px 0;
  border-bottom: 1px solid var(--line);
  align-items: center;
}
.cart-line img { width: 42px; border-radius: 3px; flex: none; }
.cart-line-main { flex: 1; min-width: 0; }
.cart-line-name { font-size: 14px; font-weight: 550; }

.cart-btn { position: relative; }
.cart-count {
  position: absolute;
  top: -6px;
  right: -7px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--ink);
  color: var(--page);
  font-size: 10.5px;
  font-weight: 700;
  display: grid;
  place-items: center;
  animation: pop .3s var(--ease);
}
@keyframes pop { 0% { transform: scale(0); } 60% { transform: scale(1.2); } 100% { transform: scale(1); } }

.total-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 14px;
}
.total-row strong { font-size: 20px; font-weight: 550; letter-spacing: -.02em; }

/* ------------------------------------------------------------------ admin */

.admin-shell { padding: 24px 0 110px; }

.gate { max-width: 348px; margin: min(15vh, 120px) auto; }
.gate .panel { padding: 28px; }
.gate h1 { font-size: 22px; margin-bottom: 6px; }

.tabs {
  display: flex;
  gap: 2px;
  padding: 3px;
  margin-bottom: 24px;
  border: 1px solid var(--line);
  border-radius: 8px;
}
.tab {
  flex: 1;
  padding: 8px 10px;
  border: 0;
  border-radius: 5px;
  background: none;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  color: var(--grey-2);
  cursor: pointer;
  transition: color .18s, background .18s;
}
.tab:hover { color: var(--grey-1); }
.tab.active { background: var(--surface-2); color: var(--ink); }

/* One line, scrolled sideways, rather than four tabs of which two wrap onto a
   second line and one does not - which reads as a broken row, not a choice. */
@media (max-width: 620px) {
  .tabs {
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tab { flex: 0 0 auto; white-space: nowrap; padding: 8px 14px; }
}

/* Numbered rail, so the order of operations is never in question. */
.step { position: relative; padding-left: 48px; margin-bottom: 16px; }
.step::before {
  content: attr(data-step);
  position: absolute;
  left: 0;
  top: 0;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--line-2);
  color: var(--grey-3);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  transition: color .25s, border-color .25s, background .25s;
}
.step::after {
  content: "";
  position: absolute;
  left: 14px;
  top: 34px;
  bottom: -16px;
  width: 1px;
  background: var(--line);
}
/* No dangling rail: the connector stops at the last step that is on screen,
   not the last one in the markup. Step 3 stays hidden until a card is picked. */
.step:last-child::after,
.step:has(+ .step.hidden)::after { display: none; }
.step.active::before { color: var(--ink); border-color: var(--grey-3); }
.step.done::before {
  content: "";
  background: var(--ink);
  border-color: var(--ink);
}

@media (max-width: 620px) {
  /* The numbered rail is desktop wayfinding. Stacked on a phone it becomes a
     stray circle floating above each panel, and the panels are already in
     order with a heading each. */
  .step { padding-left: 0; }
  .step::before, .step::after { display: none; }
}

.shot-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-start; }
.shot { width: 152px; flex: none; }

/* Front and back side by side on a phone.
   At a fixed 152px each they came to 318px inside a 306px panel and wrapped -
   twelve pixels turning one glance into a screen and a half of scrolling, on
   the device this page is actually used from. Letting them share the row
   instead makes the width whatever is available. */
/* The status message beside the two frames. A class rather than an inline
   style, so the mobile rule below can actually move it: an inline width wins
   over any stylesheet and there is nothing a media query can do about it. */
.shot-note { flex: 1 1 210px; min-width: 210px; }

/* Form rows.
   One rule instead of a flex-basis written inline on every field. Flex rather
   than grid because these rows must fill the width they are given: grid columns
   are fixed fractions, so four short fields in a wide panel leave a column of
   dead space on the right, where flex just grows them.

   `flex-end` keeps a button on the row sitting on the inputs' baseline rather
   than floating at the top of the tallest cell. */
.field-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: flex-end;
  margin-bottom: 16px;
}
.field-grid .field { flex: 1 1 150px; min-width: 0; margin: 0; }
.field-grid .field-wide { flex: 2 1 220px; }
.field-grid .btn { flex: 0 0 auto; }

/* On a phone the long field takes the whole line and the short ones pair up
   underneath, which is the shape these forms actually want: one thing to read,
   then two small ones side by side, then the button. */
@media (max-width: 620px) {
  .field-grid .field-wide { flex-basis: 100%; }
  .field-grid .btn { flex: 1 1 100%; }
}

@media (max-width: 620px) {
  .shot { width: auto; flex: 1 1 0; min-width: 0; max-width: 200px; }
  /* Its own row, under the frames, so the two frames get the full width. */
  .shot-note { flex: 1 1 100%; min-width: 0; }
}

.shot-frame {
  position: relative;
  aspect-ratio: 5 / 7;
  border-radius: 7px;
  overflow: hidden;
  border: 1px dashed var(--line-2);
  background: var(--surface);
  display: grid;
  place-items: center;
  color: var(--grey-3);
  font-size: 11.5px;
  text-align: center;
  padding: 10px;
  transition: border-color .25s;
}
.shot-frame.filled { border-style: solid; border-color: var(--line-2); }
.shot-frame img { width: 100%; height: 100%; object-fit: cover; }

/* Your photo pinned beside the options, so identifying a card is a comparison
   rather than a memory test - which is what makes this work for languages the
   database has no pictures for. */
.match-row { display: flex; gap: 20px; align-items: flex-start; }
.match-mine { width: 132px; flex: none; position: sticky; top: 78px; }
.match-options { flex: 1; min-width: 0; }

@media (max-width: 620px) {
  .match-row { flex-direction: column; }
  .match-mine { width: 118px; position: static; }
}

/* A card the database has no picture of still has to be pickable. */
.candidate-blank {
  aspect-ratio: 5 / 7;
  border-radius: 4px;
  margin-bottom: 8px;
  border: 1px dashed var(--line-2);
  display: grid;
  place-items: center;
  align-content: center;
  gap: 6px;
  color: var(--grey-3);
  text-align: center;
  padding: 8px;
}
.candidate-blank .icon { margin: 0; }
.candidate-blank span { font-size: 15px; font-weight: 600; color: var(--grey-1); }

/* A scan line, while it is literally scanning. */
.shot-frame.scanning::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 14%;
  background: linear-gradient(
    180deg,
    rgba(var(--shade), 0),
    rgba(var(--shade), .5),
    rgba(var(--shade), 0)
  );
  animation: scanline 1.5s var(--ease) infinite;
}
@keyframes scanline {
  0%   { transform: translateY(-14%); opacity: 0; }
  15%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { transform: translateY(700%); opacity: 0; }
}

.shot > .btn { margin-top: 9px; width: 100%; font-size: 12px; padding: 7px; }

.progress { height: 2px; background: var(--line); border-radius: 1px; overflow: hidden; margin-top: 12px; }
.progress-bar { height: 100%; width: 0; background: var(--ink); transition: width .3s var(--ease); }

.candidates {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(114px, 1fr));
  gap: 12px;
}
.candidate {
  border: 1px solid var(--line);
  border-radius: 7px;
  background: transparent;
  padding: 8px;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
  transition: border-color .18s, background .18s;
}
.candidate:hover { border-color: var(--line-3); background: var(--surface); }
.candidate.selected { border-color: var(--ink); background: var(--surface-2); }

/* The one whose picture matches the photograph, when the pictures were able to
   pick a clear winner. A darker outline rather than a colour, so it reads as
   "start here" and not as "already chosen" - it is still a suggestion, and the
   selected state above has to stay the stronger of the two. */
.candidate-likely { border-color: var(--line-3); box-shadow: inset 0 0 0 1px var(--line-3); }
.candidate-flag {
  font-size: 10px;
  font-weight: 650;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--grey-1);
  margin-bottom: 6px;
}
.candidate img { border-radius: 4px; margin-bottom: 8px; aspect-ratio: 5/7; object-fit: cover; }
.candidate-name { font-size: 12.5px; font-weight: 550; line-height: 1.3; }

/*
 * The line that says whose photograph this is when it is not this printing's.
 *
 * It wrapped to three lines inside a tile and got clipped, which left it
 * reading "Japanese scan -" and answering nothing. Given room to wrap and left
 * out of the tile's fixed height instead.
 */
.candidate-borrowed {
  font-size: 11px;
  line-height: 1.35;
  margin-top: 3px;
  opacity: .75;
  text-wrap: pretty;
}
.candidate-borrowed:empty { display: none; }

.price-hero { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.price-hero strong {
  font-size: 34px;
  font-weight: 550;
  letter-spacing: -.03em;
  line-height: 1;
}

.breakdown {
  border: 1px solid var(--line);
  border-radius: var(--r);
  padding: 4px 15px;
  font-size: 13px;
}
.breakdown-row {
  display: flex;
  justify-content: space-between;
  gap: 14px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line);
  color: var(--grey-1);
}
.breakdown-row:last-child { border-bottom: 0; }
.breakdown-row.total {
  border-bottom: 0;
  border-top: 1px solid var(--line-2);
  color: var(--ink);
  font-weight: 600;
  font-size: 14px;
}

.inv-row {
  display: flex;
  gap: 15px;
  align-items: center;
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
}
.inv-row:last-child { border-bottom: 0; }
.inv-row img { width: 40px; border-radius: 3px; flex: none; }
.inv-main { flex: 1; min-width: 0; }
.inv-name { font-size: 14.5px; font-weight: 550; }
.inv-tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.inv-actions { display: flex; gap: 7px; flex: none; }

/*
 * The panel that opens under a listing you are changing.
 *
 * Inset and on a slightly different ground, so it reads as belonging to the row
 * above rather than as another row in the list - the difference matters when
 * you are about to overwrite a price.
 */
.inv-edit {
  border-bottom: 1px solid var(--line);
  padding: 16px 0 20px 55px;   /* clears the thumbnail, so it lines up with the name */
  animation: inv-edit-in .18s ease-out both;
}
.inv-edit .field-grid { margin-bottom: 4px; }
.inv-edit textarea { resize: vertical; min-height: 68px; }
.inv-edit .check-row { margin-top: 12px; }
.inv-edit .notice { margin-top: 12px; }
.inv-edit-actions { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; }

@keyframes inv-edit-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

@media (max-width: 620px) {
  /* No thumbnail to clear once the row stacks. */
  .inv-edit { padding-left: 0; }
}

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

@media (max-width: 640px) {
  .inv-row { flex-wrap: wrap; }
  .inv-actions { width: 100%; margin-top: 4px; }
  .inv-actions .btn { flex: 1; }
}

/* --------------------------------------------------------------- catalogue

   Every card in the databases, on the shop. Each one says whether it is in
   stock, which is the whole difference between a catalogue and a claim - so
   that label is the one thing here that is never subtle. */

.catalogue { margin-top: clamp(40px, 6vw, 76px); }

/* The catalogue runs to tens of thousands of cards, and it is browsed by
   scanning down it rather than by reading each one. Narrower columns than the
   shelf uses: more of them on screen at once, and a smaller slot makes the same
   scan look sharper, because fewer of its pixels are being stretched. The shelf
   keeps its own larger tiles - those are the things actually for sale. */
#cat-grid { grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); }
@media (max-width: 700px) {
  #cat-grid { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 14px; }
}
.catalogue .filterbar { margin-bottom: 18px; }

/* Cards below the fold cost nothing until they reach it.

   A catalogue page is fifty cards and it keeps loading more, so by the time
   somebody has scrolled a little way the browser is laying out and painting
   several hundred of them - every frame, including the ones nowhere near the
   window. content-visibility lets it skip all of that until a card is close to
   being seen, which is the single biggest thing on this page.

   contain-intrinsic-size is what stands in for the real height while it is
   skipped. The `auto` keyword means "and remember the real one once you have
   measured it", so the scrollbar stops moving under the thumb after the first
   pass rather than jumping every time something comes into view. */
.cat-item {
  list-style: none;
  content-visibility: auto;
  contain-intrinsic-size: auto 320px;
}
.cat-art {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--surface);
  aspect-ratio: 5 / 7;
  /* Room at the top for the stock pill to sit in.

     A card scan is about 5:7 and the box is 5:7, so the artwork filled it edge
     to edge and the pill sat directly on the card's own top border - over the
     HP and the type symbol, which are the two things you look at first. The
     padding is inside the ratio box (box-sizing is border-box), so the tile
     does not grow; the picture shrinks into a frame instead, and the band above
     it belongs to the pill. */
  padding: 27px 13px 13px;
}
.cat-art img { width: 100%; height: 100%; object-fit: contain; }

.cat-flag {
  position: absolute;
  left: 7px;
  top: 6px;
  padding: 3px 8px;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 650;
  letter-spacing: .08em;
  text-transform: uppercase;
  /* Solid, not translucent: it has to be legible over any artwork at all. */
  background: rgba(var(--veil), .94);
  color: var(--grey-1);
  box-shadow: 0 0 0 1px rgba(var(--shade), .1);
}
.cat-flag-in { color: var(--good); }

.cat-name { font-size: 13.5px; font-weight: 550; margin-top: 9px; line-height: 1.3; }

.cat-ask { margin-top: 8px; display: grid; gap: 7px; }
.cat-ask input {
  font: inherit;
  font-size: 13px;
  padding: 8px 11px;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--page);
  color: inherit;
  width: 100%;
}

/* The marker that asks for the next fifty when it comes into view. Full width
   of the grid so it is always below the last card rather than beside it. */
.cat-more {
  grid-column: 1 / -1;
  list-style: none;
  display: grid;
  place-items: center;
  padding: 26px 0;
}

/* ------------------------------------------------------------------ utility */

.hidden { display: none !important; }

.spinner {
  width: 13px; height: 13px;
  border: 1.5px solid rgba(0, 0, 0, .18);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: spin .7s linear infinite;
  display: inline-block;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Anyone who asked their system to calm down gets a calm site. Delays matter
   as much as durations here: with `both` fill, a delayed item sits invisible
   until its turn. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .tile-inner, .flipper { transform: none !important; }
}

/* ------------------------------------------------------------------ camera */

/* The viewfinder covers everything while it is open. Fixed rather than
   absolute, and the body is locked, because a page that scrolls underneath a
   camera is disorienting on a phone held at arm's length over a card. */
body.cam-open { overflow: hidden; }

.cam {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: #000;
  display: flex;
  flex-direction: column;
}

.cam-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The card-shaped window. The surround is dimmed with an enormous spread
   shadow rather than four separate panels, so there is one element to place. */
.cam-mask {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}

/* max-height as well as width, so the guide still fits when the phone is
   turned sideways - and matches the region the frame check actually measures. */
.cam-guide {
  width: 82%;
  max-width: 460px;
  max-height: 94%;
  aspect-ratio: 5 / 7;
  border-radius: 12px;
  border: 2px solid rgba(var(--glass), .85);
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .55);
  transition: border-color .2s, box-shadow .2s;
}
/* Green only at the moment it is worth reading, so the colour means something
   rather than being decoration. */
.cam-guide.ready { border-color: #46d17f; }
.cam-guide.done {
  border-color: #46d17f;
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .55), inset 0 0 0 100vmax rgba(70, 209, 127, .18);
}

.cam-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 18px var(--pad) calc(18px + env(safe-area-inset-bottom));
  background: linear-gradient(to top, rgba(0, 0, 0, .82), rgba(0, 0, 0, 0));
  display: flex;
  flex-direction: column;
  gap: 14px;
  align-items: center;
}

.cam-status {
  color: #fff;
  font-size: 15px;
  font-weight: 550;
  letter-spacing: -.01em;
  text-align: center;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .6);
  min-height: 1.4em;
}

.cam-buttons { display: flex; gap: 10px; width: 100%; max-width: 460px; }
.cam-buttons .btn { flex: 1; }
/* The outline button sits on video, not on paper, so it needs its own colours
   rather than the page's near-black on near-white. */
.cam-buttons .btn:not(.btn-primary) {
  color: #fff;
  border-color: rgba(var(--glass), .5);
  background: rgba(0, 0, 0, .25);
}
.cam-buttons .btn:not(.btn-primary):hover {
  background: rgba(0, 0, 0, .45);
  border-color: #fff;
}

/* ------------------------------------------------------------------ AI read
   What a vision model read off the photograph, shown between the search boxes
   and the candidates from the card database.

   Its whole design job is to not look like the rest of the form. Everything
   else on this screen is either something you typed or something the card
   database returned; this is a machine's reading of a photograph, and the one
   mistake that costs real money here is mistaking it for a fact. So it gets a
   dashed edge rather than the solid one every settled panel has, its own
   heading, and a confidence badge - and the price never appears in it at all,
   because the price is never in the answer. */
.ai-read {
  border: 1px dashed var(--line-3);
  border-radius: var(--r-lg);
  background: var(--surface);
  padding: 14px 16px;
  margin-bottom: 16px;
}
.ai-read-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
  font-size: 13.5px;
}
.ai-conf {
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 999px;
  border: 1px solid var(--line-2);
  color: var(--grey-2);
  white-space: nowrap;
}
/* Low confidence is the one that has to interrupt you. High is not celebrated:
   a machine being sure is not the same as it being right, and a green badge
   would invite exactly the nod-through this panel exists to prevent. */
.ai-conf-low { border-color: #f0d8d5; color: var(--bad); }

.ai-read-list {
  display: grid;
  grid-template-columns: minmax(96px, auto) 1fr;
  gap: 5px 14px;
  margin: 0;
  font-size: 13.5px;
}
.ai-read-list dt { color: var(--grey-2); }
.ai-read-list dd { margin: 0; color: var(--ink); }
.ai-read-foot { margin: 12px 0 0; font-size: 12.5px; line-height: 1.5; }

@media (max-width: 520px) {
  .ai-read-list { grid-template-columns: 1fr; gap: 2px; }
  .ai-read-list dt { margin-top: 7px; font-size: 12px; }
}

/* ------------------------------------------------------- folding the matches
   Once a card has been chosen, the boxes that were searched on and the grid of
   candidates that came back are working-out rather than information. On a phone
   they are most of the screen, and every one of them is a card you have already
   rejected. Folded away, the page goes from "confirm which card, then price it"
   to "here it is, post it".

   Hidden, not removed: #unfold-matching brings the whole thing back with the
   candidates still in it, so the escape hatch costs one tap. */
#step-2.folded .field-grid,
#step-2.folded .match-row,
#step-2.folded #candidates-hint,
#step-2.folded #manual-toggle,
#step-2.folded #manual-form { display: none; }
#step-2.folded .panel-head h2 { font-size: 15px; }
#step-2.folded .panel { padding-bottom: 14px; }
#unfold-matching { margin-bottom: 4px; }
#step-2.folded #unfold-matching,
#step-2.folded .ai-read { display: block; }

/* A quieter button than .btn: this one is an escape hatch, not an instruction,
   and it should not compete with the thing you actually came here to press. */
.btn-quiet {
  background: none;
  border: none;
  padding: 6px 0;
  color: var(--grey-2);
  text-decoration: underline;
  text-underline-offset: 3px;
  font-size: 13px;
}
.btn-quiet:hover { color: var(--ink); }

/* ------------------------------------------------------------- card window
   Tapping a card on the shelf opens it over the shelf rather than navigating
   away. Browsing is a loop - look, open one, decide, come back - and a page
   load in the middle of it loses your place in a grid you may have scrolled a
   long way down. The window closes back onto exactly where you were.

   The contents are the same markup the card page uses, so there is one layout
   to keep right rather than two. */
.card-window-veil {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(var(--shade), .55);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: veil-in .22s var(--ease) both;
}
.card-window-veil.going { animation: veil-in .18s var(--ease) reverse forwards; }
@keyframes veil-in { from { opacity: 0 } to { opacity: 1 } }

.card-window {
  position: fixed;
  z-index: 61;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(1000px, calc(100vw - 2 * var(--pad)));
  max-height: min(90vh, 1000px);
  overflow: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: var(--page);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: 0 30px 80px rgba(var(--shade), .3);
  animation: card-window-in .22s var(--ease) both;
}
@keyframes card-window-in {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 12px)) scale(.985) }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1) }
}
.card-window-body { padding: clamp(18px, 3vw, 34px); }

/* Inside the window there is no page header above it, so the picture sticks to
   the window's own top edge rather than 92px below where one would have been. */
.card-window .viewer { top: 18px; }

/* Never bigger than the picture actually is.

   Both card databases stop at about 745px across - it is the largest scan
   either of them publishes, and there is nothing sharper to fetch. Stretched
   across a 900px column on a wide screen that is a 20% upscale, and an upscale
   is the one kind of softness that is entirely self-inflicted: the same file
   shown at its own size is sharp.

   So the frame may be as wide as the card is and no wider. On a phone it is the
   column that is smaller, which costs nothing. */
.card-window .face img,
.card-window .shot-frame img { max-width: 745px; margin-inline: auto; }
.card-window .card-face,
.card-window .face { max-width: 745px; }

.card-window-close {
  position: sticky;
  top: 10px;
  float: right;
  margin: 10px 10px 0 0;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 34px; height: 34px;
  border: 1px solid var(--line-2);
  border-radius: 50%;
  background: rgba(var(--veil), .92);
  color: var(--grey-1);
  cursor: pointer;
}
.card-window-close:hover { color: var(--ink); border-color: var(--line-3); }

.card-window-permalink {
  display: block;
  padding: 0 clamp(18px, 3vw, 34px) clamp(18px, 3vw, 30px);
  font-size: 12.5px;
  color: var(--grey-2);
}
.card-window-permalink:hover { color: var(--ink); }

/* The page behind must not scroll while a window is over it - on a phone that
   reads as the window itself refusing to move. */
body.window-open { overflow: hidden; }

@media (max-width: 700px) {
  .card-window {
    width: 100vw;
    max-height: 92vh;
    top: auto;
    bottom: 0;
    transform: translate(-50%, 0);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
  }
  @keyframes card-window-in {
    from { opacity: 0; transform: translate(-50%, 24px) }
    to   { opacity: 1; transform: translate(-50%, 0) }
  }
}

/* ---------------------------------------------------------- currency picker
   Sits with the theme switch, because both are "how I want to read this shop"
   rather than anything to do with what is in it. */
.currency-pick {
  font: inherit;
  font-size: 12.5px;
  /* Selects on this site are form fields and are styled to fill their column.
     This one is a chip in a toolbar, so it has to opt out of that - left as it
     was it stretched to nine hundred pixels and pushed the whole header
     sideways. */
  width: auto;
  flex: none;
  font-variant-numeric: tabular-nums;
  padding: 7px 9px;
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  background: var(--page);
  color: var(--grey-1);
  cursor: pointer;
}
.currency-pick:hover { color: var(--ink); border-color: var(--line-3); }

/* On a phone the header is already carrying a logo, a cart and a theme switch
   in a row 62px tall. A select with default padding pushed that to 189px -
   it wrapped, and the whole page moved down to make room. Small enough to sit
   in the row it was added to. */
@media (max-width: 700px) {
  /* The header row on a phone was already full: a logo, Log in, a cart and a
     theme switch across 390px. Adding a select wrapped it onto a second line
     and the header went from 62px to 189px - a fifth of the screen, permanently
     stuck to the top of it.

     So on a phone this is a chip the width of its own three letters, and the
     row it sits in is tightened to match. */
  /* Out of the button row, onto the row below.

     The header on a phone is already two lines: the brand and the buttons, then
     the Main/Shop/Orders nav. Measured, the buttons come to within 40px of the
     screen, so a select in that row tipped it onto a third line and took the
     header from 102px to 141px - a sixth of the screen, stuck to the top of it.

     Lifted out of the flow it sits at the end of the nav line, which had room
     to spare, and the header goes back to the two rows it had before. */
  .currency-pick {
    position: absolute;
    right: 0;
    bottom: 9px;
    padding: 4px 2px 4px 5px;
    font-size: 11px;
    width: 46px;
    -webkit-appearance: none;
    appearance: none;
  }
  /* Room for it at the end of the line - taken from both ends, because the nav
     is centred inside this line and padding on one side alone moves it off
     centre by half of it. */
  .glass-nav-line { padding-inline: 52px; }
  .topbar-inner { gap: 9px; }
}

/* ------------------------------------------------------------- drop zones
   Drag a picture from a computer at the window and the page opens down the
   middle: front on the left, back on the right. The two halves are the target,
   which is about as forgiving as aiming gets - and it means nobody has to find
   a button, then find a file in a dialog, twice for every card.

   Only ever on screen while something with files in it is over the window. */
.drop-zone {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: grid;
  grid-template-columns: 1fr 1fr;
  opacity: 0;
  transition: opacity .16s var(--ease);
}
.drop-zone.open { opacity: 1; }

.drop-half {
  display: grid;
  place-items: center;
  background: rgba(var(--shade), .72);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 2px dashed transparent;
  transition: background .14s, border-color .14s, transform .18s var(--ease);
}
/* The two halves are mirrored, so the gap between them reads as a seam the
   page opened along rather than as two panels that happen to be adjacent. */
.drop-half[data-side="front"] { border-right-width: 1px; }
.drop-half[data-side="back"]  { border-left-width: 1px; }
.drop-half.over {
  background: rgba(var(--shade), .86);
  border-color: rgba(var(--glass), .55);
}
.drop-half.over .drop-mark { transform: scale(1.04); }

.drop-mark {
  display: grid;
  justify-items: center;
  gap: 6px;
  color: #fff;
  text-align: center;
  padding: 20px;
  transition: transform .18s var(--ease);
  pointer-events: none;   /* or the label swallows the drop */
}
.drop-mark strong { font-size: clamp(15px, 2.4vw, 21px); font-weight: 600; }
.drop-mark .meta { color: rgba(255, 255, 255, .62); }

/* An arrow drawn rather than fetched: the sprite may not have loaded, and a
   drop zone with a missing icon in it looks broken at the worst moment. */
.drop-arrow {
  width: 34px; height: 34px;
  margin-bottom: 4px;
  border: 2px solid rgba(255, 255, 255, .8);
  border-radius: 8px;
  position: relative;
}
.drop-arrow::before {
  content: "";
  position: absolute;
  left: 50%; top: 7px;
  width: 2px; height: 13px;
  background: rgba(255, 255, 255, .8);
  transform: translateX(-50%);
}
.drop-arrow::after {
  content: "";
  position: absolute;
  left: 50%; top: 14px;
  width: 9px; height: 9px;
  border-right: 2px solid rgba(255, 255, 255, .8);
  border-bottom: 2px solid rgba(255, 255, 255, .8);
  transform: translateX(-50%) rotate(45deg);
}

/* A touch screen has no drag, and the halves would only ever be in the way. */
@media (hover: none) and (pointer: coarse) {
  .drop-zone { display: none !important; }
}
