/* ==========================================================================
   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          */
  /* #767676 is exactly 4.5:1 on white and so fails everywhere the page is not
     white - which is most places it gets used, because faint text lives on
     cards and in frames sitting on --surface. Measured at 4.20:1 there. This
     clears the floor against the greyest ground it is ever put on. */
  --grey-3: #6b6b6b;   /* faint, placeholders - 5.3:1 on white, 4.6:1 on --surface-3 */

  --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 */

/*
 * The most expensive thing on screen during a scroll, until now.
 *
 * A backdrop-filter is recomputed every frame the content behind it moves, and
 * a sticky bar means every frame of every scroll - across the full width of the
 * window. That reasoning is already written a few hundred lines up, where the
 * floating nav was dropped from eighteen pixels of blur to twelve and given its
 * own layer for exactly this reason. The bar it sits inside got neither: a
 * wider blur than the nav, and no promotion, so the whole strip was
 * re-rasterised against fresh content on every frame.
 *
 * Twelve to match, and its own layer so scrolling the page underneath does not
 * drag the bar through rasterisation with it. One extra compositor layer, for
 * one element that genuinely changes every frame - which is what the property
 * is for, and the opposite of putting it on three hundred tiles that do not.
 */
.topbar {
  position: sticky;
  top: 0;
  z-index: 60;
  background: rgba(var(--veil), .85);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  will-change: backdrop-filter;
  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;
}

/* min-height because on a phone the mark alone is 23px tall and the name is
   hidden - a 23px target is under the floor for a thumb, and this is the link
   that gets you home. */
.brand { display: flex; align-items: center; gap: 10px; min-height: 26px; }

.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;
}
/*
 * Sixteen pixels on a touch screen, and not a fraction less.
 *
 * Safari on iPhone zooms the whole page in the moment you tap a field whose
 * text is under 16px - and it does not zoom back out when you leave the field.
 * The visitor is left on a shop that is suddenly too wide, scrolled sideways,
 * with the rest of the checkout off the edge of the screen. It is the most
 * common way a checkout gets abandoned on a phone and it is invisible on a
 * desk, because it only happens on a touch device.
 *
 * Only on coarse pointers, so the fields keep their intended size everywhere a
 * mouse is being used.
 */
@media (pointer: coarse) {
  input[type="text"], input[type="number"], input[type="search"],
  input[type="password"], input[type="email"], input[type="tel"],
  select, textarea {
    font-size: 16px;
  }
}

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.

   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.

   The tint is a fixed light colour in both themes, so the text on it has to be
   fixed too. It was var(--bad), which flips to a pale red in dark mode: 5.0:1
   in the light theme and 2.6:1 in the dark one, on a panel that only ever
   appears when something has gone wrong. Now 6.6:1 and 6.3:1 in both. */
.notice-error {
  border-color: #f0d8d5;
  border-left-color: #c0392b;
  background: #fdf4f3;
  color: #a32b1f;
}
.notice-ok {
  border-color: #cfe8db;
  border-left-color: #157a4c;
  background: #f2faf6;
  color: #15694a;
}
.notice-info { border-left-color: var(--line-3); }

/* A button inside a notice takes the notice's colour, not the page's.
   .btn sets color: var(--ink), which in the dark theme is very nearly white -
   and the tinted panels above are very nearly white in both themes. The Try
   again button on a failed catalogue page measured 1.03:1 against the panel it
   sat on: present, outlined, and unreadable. */
.notice .btn {
  color: inherit;
  border-color: currentColor;
  background: transparent;
}
.notice .btn:hover { border-color: currentColor; background: rgba(0, 0, 0, .055); }
.notice .btn:active { background: rgba(0, 0, 0, .1); }

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

/*
 * will-change only while something is about to change.
 *
 * This said `will-change: transform` on every tile, always. That is not a hint
 * - it is an instruction, and the browser obeys it by giving each tile its own
 * compositor layer and holding a rasterised copy of it in GPU memory for the
 * whole life of the page. Measured on a shelf of three hundred cards: three
 * hundred and one layers. On a desktop that is wasteful; on a phone it is the
 * classic way to run out of layer memory, at which point the browser starts
 * throwing layers away and re-rasterising them mid-scroll, which is exactly
 * what stuttering scroll feels like.
 *
 * A tile only moves when the pointer is on it, and there is one pointer. So the
 * promotion happens on hover and is given back on the way out - one layer at a
 * time instead of three hundred. Touch devices never hover at all, so a phone
 * now allocates none of these, which is where it mattered most.
 */
.tile-inner {
  display: flex;
  flex-direction: column;
  transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transition: transform .55s var(--ease);
}
@media (hover: hover) {
  .tile:hover .tile-inner { transition-duration: .1s; will-change: transform; }
}

.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;
  /* Sealed product is photographed against whatever was behind it, so it gets
     a margin and a backing. A card scan does not - it fills its own frame. */
  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;
}
/*
 * The pointer tilt follows fast; the flip does not.
 *
 * `tracking` drops the transition to a tenth of a second so the card leans
 * under the pointer without lagging behind it - which is right for the tilt and
 * wrong for the turn. And you click with the pointer on the card, so tracking
 * was always on at the moment of a flip: every turn snapped through 180 degrees
 * in 100ms instead of gliding. `turning` wins while the card is going over.
 */
.flipper.tracking { transition-duration: .1s; }
.flipper.turning { transition-duration: .72s; }

/*
 * Negative, so it turns to the right.
 *
 * A positive rotateY sends the card's right edge away from the viewer, which
 * reads as turning to the left. Negative brings the right edge forward. Both
 * land on the same face - it is the path between them that differs, and the
 * path is the whole point of an animation.
 */
.flipper.flipped { --flip: -180deg; }

/*
 * No corners of our own, and nothing cropped.
 *
 * A card scan already has the card's corners in it - rounded, and drawn at
 * whatever radius that printing actually uses. Putting a 10px radius and a
 * border round the outside gave every card two sets of corners slightly out of
 * step with each other, and it was worst on the cards where it matters most:
 * a full-art with a coloured border, where the corner the eye follows is the
 * printed one and the drawn one cuts across it.
 *
 * object-fit was `cover`, which crops. A scan is close to 5:7 but not exactly,
 * so cover was quietly shaving a sliver off two edges of every card - including
 * the bottom edge, where the copyright line and the set code live. `contain`
 * shows the card that was scanned, whole, at whatever shape it really is.
 */
.face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  overflow: hidden;
}
.face img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.face-back { transform: rotateY(180deg); }

/*
 * The light that follows the pointer, cut to the shape of the card.
 *
 * A card scan is a picture with rounded corners and transparency outside them,
 * sitting in a rectangular frame. This highlight covers the frame, so it was
 * painting into those empty corners and drawing a hard-edged rectangle across
 * the window's dark ground: a sharp corner appearing under the pointer, on a
 * card whose own corners are round.
 *
 * --face-mask is the card's own picture, set on the face by store.js, and it is
 * used here as a mask. The light can then only fall where the card has pixels.
 * Nothing invents a corner radius: whatever that printing actually has is what
 * the light follows, and the alignment is exact because it is the same image
 * fitted the same way.
 *
 * The fallback is an opaque gradient, which masks nothing - so a face with no
 * picture set behaves exactly as it did.
 */
.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%
  );
  -webkit-mask-image: var(--face-mask, linear-gradient(#000, #000));
  mask-image: var(--face-mask, linear-gradient(#000, #000));
  /* contain / center, to match object-fit: contain on the picture itself. */
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

/* An empty frame draws its own box, so the light belongs on the box. */
.face:has(.face-empty)::after { -webkit-mask-image: none; mask-image: none; }

.face-empty .icon { margin: 0; }
.face-empty {
  /* The one face that still needs a box drawn round it: there is no card in it
     to draw its own edges. */
  border-radius: 10px;
  background: var(--surface);
  border: 1px solid var(--line);
  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); }
/* Under the number: why the box is empty. A dashed box with "002" in it reads
   as a picture that failed to load, and somebody reloads the page over it -
   when the truth is that nobody has ever photographed that printing. */
.candidate-blank .candidate-blank-why {
  font-size: 11px;
  font-weight: 450;
  letter-spacing: .02em;
  color: var(--grey-3);
}

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

/* It is a button now, because it opens something. Buttons bring a border, a
   default background and centred bold text with them; none of that belongs on
   a picture, so it is all taken back off and only the pointer and the focus
   ring are kept - the two things that say it can be pressed. */
.cat-art {
  display: block;
  width: 100%;
  appearance: none;
  border: 0;
  cursor: zoom-in;
  font: inherit;
  color: inherit;
  text-align: left;
}
.cat-art:hover { background: var(--surface-2); }

.cat-name-btn {
  display: block;
  width: 100%;
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.cat-name-btn:hover { text-decoration: underline; text-underline-offset: 2px; }

/* The catalogue window has one picture and no flip, so the frame does not need
   the flipper's 3D box - but it does need the same size and rounding. */
/* .cat-face is gone: a catalogue card now uses the same flipper, faces and
   stage as one on the shelf, so it needs no frame of its own. */
.cat-detail-more { margin-top: 18px; min-height: 40px; }

.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 .face { max-width: 745px; }

/*
 * It has to look like a panel you are scrolling inside, not a page.
 *
 * The window is its own scroll container, but nothing on screen said so:
 * content ran straight to the top and bottom edges and simply appeared and
 * disappeared there, which is exactly what a page does behind a fixed header.
 * Scrolling it felt like scrolling the site, and the fact that it was a window
 * with a top and a bottom only became clear when you hit the end.
 *
 * The fades are drawn on the window itself and pinned with position: sticky, so
 * they sit over the scrolling content without being part of it. Only the one at
 * the end you are not at is visible: at the very top there is nothing above to
 * hint at, and at the very bottom nothing below.
 */
.card-window::before,
.card-window::after {
  content: "";
  position: sticky;
  left: 0;
  display: block;
  height: 26px;
  margin-bottom: -26px;
  z-index: 1;
  pointer-events: none;
}
.card-window::before {
  top: 0;
  background: linear-gradient(var(--page), rgba(var(--veil), 0));
}
.card-window::after {
  bottom: 0;
  margin: -26px 0 0;
  background: linear-gradient(rgba(var(--veil), 0), var(--page));
}
@supports (animation-timeline: scroll()) {
  /* Where it is supported, each fade is only drawn once there is something in
     that direction to fade into. */
  .card-window::before,
  .card-window::after {
    animation: fade-edge linear both;
    animation-timeline: scroll(self);
  }
  .card-window::before { animation-range: 0 26px; }
  .card-window::after { animation-direction: reverse; animation-range: calc(100% - 26px) 100%; }
  @keyframes fade-edge { from { opacity: 0 } to { opacity: 1 } }
}

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

/* ------------------------------------------------------- card backs, per language

   One row per language: what is set now on the left, the address field and its
   two buttons on the right. Laid out as a row rather than a grid because the
   thumbnail is the thing you check at a glance - "is that the Japanese back or
   did I paste the English one twice" - and it should sit next to its own name. */
.cardback-row {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  padding: 16px 0;
  border-bottom: 1px solid var(--line);
}
.cardback-row:last-child { border-bottom: 0; }

.cardback-shown { flex: none; width: 84px; }
.cardback-thumb {
  width: 84px;
  aspect-ratio: 5 / 7;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--line);
  display: block;
}
.cardback-empty {
  width: 84px;
  aspect-ratio: 5 / 7;
  display: grid;
  place-items: center;
  border-radius: 6px;
  border: 1px dashed var(--line-2);
  color: var(--grey-2);
  font-size: 11.5px;
}

.cardback-fields { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.cardback-actions { display: flex; gap: 8px; flex-wrap: wrap; }

@media (max-width: 560px) {
  .cardback-row { flex-direction: column; }
}

/* The row lights up while a file is over it, so it is obvious which language
   the picture is about to become the back for. */
.cardback-row.dropping {
  outline: 2px dashed var(--accent, var(--line-3));
  outline-offset: 6px;
  border-radius: 8px;
}

/* Price, and the money it is in. The select is a suffix on the field rather
   than a field of its own: they are one answer, and splitting them across two
   columns invites you to fill one and not the other. */
.price-entry { display: flex; gap: 8px; align-items: stretch; }
.price-entry input { flex: 1; min-width: 0; }
.price-entry select { width: auto; flex: none; font-variant-numeric: tabular-nums; }
#final-converted { display: block; margin-top: 6px; font-size: 12px; }

/* ------------------------------------------------------- the filter pill

   A pill with a pill inside it. The outer strip is what you are filtering by -
   rarity, set, type, in stock - and the inner one is that category's values.
   Two rounded rows rather than four dropdowns, because a dropdown hides its
   options and the whole point here is seeing what is available.

   The nesting is the affordance: the inner pill sits on a slightly different
   ground, inset from the outer one, so it reads as "inside" and it is obvious
   which strip a chip belongs to. Both scroll sideways on a phone rather than
   wrapping, so the bar is always one line tall and the row you are reading
   never reflows under your thumb. */

.filter-pill {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  min-width: 0;
}

.filter-cats,
.filter-values {
  display: flex;
  align-items: center;
  gap: 4px;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.filter-cats::-webkit-scrollbar,
.filter-values::-webkit-scrollbar { display: none; }

/* The inner pill: its own ground, its own rounding, inset from the outer. */
.filter-values {
  flex: 1 1 auto;
  min-width: 0;
  padding: 4px;
  border-radius: 999px;
  background: var(--page);
  border: 1px solid var(--line);
}
.filter-values:empty { display: none; }

.filter-cat,
.filter-value {
  flex: 0 0 auto;
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--grey-1);
  font: inherit;
  font-size: 13px;
  line-height: 1;
  /* 30px tall, so every chip clears the tap-target floor on a phone. */
  padding: 9px 14px;
  border-radius: 999px;
  cursor: pointer;
  white-space: nowrap;
  transition: background .15s, color .15s;
}
.filter-cat:hover,
.filter-value:hover { background: var(--surface-2); color: var(--ink); }

/* The chosen one is filled rather than outlined: an outline on a chip inside a
   pill inside a pill is three concentric borders and nothing reads. */
.filter-cat[aria-selected="true"] {
  background: var(--ink);
  color: var(--page);
}
.filter-value[aria-pressed="true"] {
  background: var(--ink);
  color: var(--page);
}

.filter-clear {
  flex: 0 0 auto;
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--grey-2);
  font: inherit;
  font-size: 12.5px;
  padding: 9px 12px;
  border-radius: 999px;
  cursor: pointer;
}
.filter-clear:hover { color: var(--ink); background: var(--surface-2); }

/* On a phone the two strips stack: side by side there is not enough width for
   either to show more than two chips, and a strip you cannot see the second
   item of is not a strip. */
@media (max-width: 620px) {
  .filter-pill {
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
    border-radius: var(--r-lg);
    padding: 8px;
  }
  .filter-values { width: 100%; }
  .filter-clear { align-self: flex-start; }
}

/* ------------------------------------------- the back photo, added later

   A card listed without a back photo could never get one: the back was only
   offered while scanning, and there is no way back to that screen for a card
   already saved. In the shop that card does not turn over, because there is
   nothing on the other side of it. This row is where a picture gets added to a
   listing that already exists. */

.inv-back {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  margin-bottom: 16px;
}
.inv-back-preview {
  flex: 0 0 auto;
  width: 74px;
  aspect-ratio: 5 / 7;
  border-radius: 7px;
  overflow: hidden;
  border: 1px solid var(--line-2);
  background: var(--surface);
}
.inv-back-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.inv-back-empty {
  height: 100%;
  display: grid;
  place-items: center;
  text-align: center;
  padding: 8px;
  font-size: 11px;
  color: var(--grey-3);
}
.inv-back-side { flex: 1 1 auto; min-width: 0; display: grid; gap: 6px; align-content: start; }
.inv-back-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 2px; }

/* ------------------------------------------------------------------ finishes

   Two copies of one card, one normal and one reverse holo, were identical on
   the shelf down to the pixel and priced differently. The finish was on the
   listing the whole time and had nowhere to be seen. Two things fix that: the
   words, and the light.

   The words first, because they work everywhere and they are what somebody
   actually reads before buying. */
.tile-finish {
  margin-top: 2px;
  font-size: 11px;
  font-weight: 550;
  letter-spacing: .02em;
  color: var(--grey-2);
}
.chip-finish { border-color: var(--line-3); }

/* And the light.

   A foil card does not have a colour - it has a behaviour. What the eye reads
   as "that one is shiny" is a narrow bright band that travels across the print
   as the card moves, picking up colour as it goes. So this is a moving sweep
   rather than a rainbow tint: a tinted card just looks like a badly colour
   balanced photograph, which is the opposite of the intended effect.

   Painted on a pseudo-element above the picture and clipped to the frame, with
   mix-blend-mode doing the work - it brightens what is under it rather than
   covering it, so the artwork stays the artwork. */
.shine { position: relative; overflow: hidden; isolation: isolate; }
.shine::after {
  content: "";
  position: absolute;
  inset: -40%;
  pointer-events: none;
  z-index: 2;
  background: linear-gradient(
    115deg,
    transparent 38%,
    rgba(255, 255, 255, .34) 45%,
    rgba(150, 220, 255, .30) 49%,
    rgba(255, 210, 150, .28) 53%,
    rgba(255, 255, 255, .30) 57%,
    transparent 64%
  );
  mix-blend-mode: screen;
  transform: translateX(-55%);
  animation: foil-sweep 5.5s ease-in-out infinite;
}

/* A reverse holo is foiled everywhere except the art window, so the whole card
   catches light at once and more evenly. A holo is foiled in the art box only,
   where the band is tighter and brighter. One rule, two moods. */
.shine-reverse::after { animation-duration: 7s; opacity: .85; }
.shine-holo::after { animation-duration: 4.5s; opacity: 1; filter: saturate(1.3); }

@keyframes foil-sweep {
  0%   { transform: translateX(-55%); }
  55%  { transform: translateX(55%); }
  100% { transform: translateX(55%); }
}

/* A card window is one card, chosen deliberately and looked at closely, so its
   sheen follows the pointer rather than a clock - trackPointer is already doing
   that. The sweep would fight it. */
.card-window .shine::after { animation: none; opacity: .5; transform: none; }

/* Somebody who has asked their system for less movement is asking for this too.
   The words below the name still say which printing it is, so nothing is lost
   except the motion. */
@media (prefers-reduced-motion: reduce) {
  .shine::after { animation: none; transform: none; opacity: .4; }
}

/* Checkout steps in the cart panel: Cart -> Details -> Pay. */
.mini-steps { list-style: none; margin: 0; padding: 4px 24px 14px; display: flex; position: relative; --p: 0; }
.mini-steps::before, .mini-steps::after { content: ''; position: absolute; top: 17px; left: calc(24px + (100% - 48px) / 6); height: 2px; border-radius: 2px; }
.mini-steps::before { right: calc(24px + (100% - 48px) / 6); background: var(--line-2); }
.mini-steps::after { width: calc((100% - 48px) * 2 / 3 * var(--p)); background: var(--ink); transition: width .45s var(--ease); }
.mini-steps li { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; position: relative; z-index: 1; }
.mini-dot { width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center; font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; background: var(--page); color: var(--line-3); box-shadow: inset 0 0 0 2px var(--line-2); transition: background .25s var(--ease), color .25s var(--ease), box-shadow .25s var(--ease); }
.mini-dot .icon { width: 14px; height: 14px; margin: 0; color: inherit; stroke-width: 2.6; }
.mini-label { font-size: 12px; font-weight: 600; color: var(--line-3); }
.mini-steps li.now .mini-dot { color: var(--ink); box-shadow: inset 0 0 0 2px var(--ink), 0 0 0 4px var(--line); }
.mini-steps li.done .mini-dot { background: var(--ink); color: var(--page); box-shadow: none; }
.mini-steps li.now .mini-label, .mini-steps li.done .mini-label { color: var(--ink); }
.foot-pair { display: flex; gap: 10px; }
.foot-pair .btn-primary { flex: 1; }
@media (prefers-reduced-motion: reduce) { .mini-steps::after { transition: none; } }

/* Price that follows the market, and "About this card" on a listing. */
.price-note { margin: -6px 0 18px; font-size: 13px; }
.about-card .moves { display: grid; gap: 12px; margin-top: 14px; }
.about-card .move { padding: 12px 14px; border-radius: 12px; box-shadow: inset 0 0 0 1px var(--line); }
.about-card .move-head { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; }
.about-card .move p { margin: 6px 0 0; font-size: 14px; line-height: 1.45; }
.about-card .flavor { margin-top: 14px; font-style: italic; font-size: 14px; }
