/* ==========================================================================
   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);
    backdrop-filter: blur(18px) saturate(180%);
    -webkit-backdrop-filter: blur(18px) saturate(180%);
  }
}

/* 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); } }

.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;
}
@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; }

.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; }

@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); }
.catalogue .filterbar { margin-bottom: 18px; }

.cat-item { list-style: none; }
.cat-art {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--surface);
  aspect-ratio: 5 / 7;
}
.cat-art img { width: 100%; height: 100%; object-fit: contain; }

.cat-flag {
  position: absolute;
  left: 8px;
  top: 8px;
  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; }
}
