:root, [data-theme="dark"] {
  --bg: #0b0b0d;
  --bg-raised: #14161b;
  --bg-hover: #1d2026;
  --line: #262932;
  --fg: #f5f5f5;
  --fg-dim: #9a9a9a;
  --fg-faint: #5a5a5a;
  --accent: #ffffff;
  --accent-fg: #000000;
  --tile-a: #1a1a1a;
  --tile-b: #0e0e0e;
  --shadow: rgba(0,0,0,.6);
  --scroll-thumb: #3a3a3a;
  --scroll-thumb-hover: #565656;
  --slab-a: rgba(255,255,255,.20); --slab-b: rgba(255,255,255,.09); --slab-c: rgba(255,255,255,.05);
  --slab-edge: rgba(255,255,255,.16);
  /* Owned markers on the Discover shelf - the safe in each tile's line.
     Gold and silver rather than two hues: they rank themselves, so the one
     that means "this exact slab is already yours" is legible as the stronger
     of the two without a legend. */
  --own-exact: #e3b23c;   /* gold   - same card, same grade */
  --own-other: #b9bec7;   /* silver - same card, another grade */
  /* 2026-08 style guide: primary gradient stops */
  --brand-orange: #FF6A00; --brand-pink: #FF2DAA; --brand-purple: #7A2CFF;
  --brand-blue: #0062FF; --brand-cyan: #00C8FF;
  --brand-gradient: linear-gradient(45deg,#FF6A00 0%,#FF2DAA 33%,#7A2CFF 66%,#0062FF 85%,#00C8FF 100%);
  /* loading skeletons: the resting fill and the crest of the sweep */
  --skel-a: #171920; --skel-b: #23262f;
}
[data-theme="light"] {
  --bg: #fafafa;
  --bg-raised: #ffffff;
  --bg-hover: #f0f0f0;
  --line: #e4e4e4;
  --fg: #0d0d0d;
  --fg-dim: #666;
  --fg-faint: #aaa;
  --accent: #000000;
  --accent-fg: #ffffff;
  --tile-a: #ececec;
  --tile-b: #f7f7f7;
  --shadow: rgba(0,0,0,.15);
  --scroll-thumb: #c9c9c9;
  --scroll-thumb-hover: #a2a2a2;
  --own-exact: #9a7415;   /* both darkened for a light background */
  --own-other: #7b818c;
  --slab-a: rgba(90,92,104,.26); --slab-b: rgba(90,92,104,.13); --slab-c: rgba(90,92,104,.09);
  --slab-edge: rgba(0,0,0,.13);
  --skel-a: #ececef; --skel-b: #f7f7f9;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
  font-family: 'Satoshi', 'Inter', -apple-system, sans-serif;
  background: var(--bg); color: var(--fg);
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}
#app { display: flex; height: 100vh; }

/* ---------- sidebar ---------- */
#sidebar {
  width: 264px; flex-shrink: 0; display: flex; flex-direction: column;
  border-right: 1px solid var(--line); background: var(--bg);
  padding: 22px 14px 14px;
}
/* ONE WORDMARK, ONE SHAPE.
 *
 * The sidebar was set at `letter-spacing: 3px` while Settings used -.5px and
 * the login screen -.02em, so the same six characters were drawn three
 * different widths depending on which page you were on - S L A B S . in the
 * sidebar and SLABS. everywhere else.  A logo is not a typographic decision per
 * screen; it is one shape.
 *
 * Expressed in `em` rather than pixels so it holds at 19px, 26px and 30px
 * alike - a fixed pixel track is a different look at every size, which is how
 * these drifted apart to begin with. */
.brand { font-weight: 900; font-size: 26px; letter-spacing: -.02em; cursor: pointer; padding: 0 10px 18px; }
nav { display: flex; flex-direction: column; gap: 2px; margin-bottom: 22px; }
.nav-item {
  display: flex; align-items: center; gap: 12px; padding: 9px 10px;
  border-radius: 8px; color: var(--fg-dim); font-weight: 600; font-size: 14px;
  cursor: pointer; transition: .15s;
}
.nav-item:hover, .nav-item.active { color: var(--fg); background: var(--bg-hover); }
/* The row holds the destination and the one action that fills it.  The link
   takes the width so the + sits hard right, and the + only colours up on
   hover: it is the rarest thing in the sidebar and should not compete with the
   shelves for attention. */
.nav-row { display: flex; align-items: center; gap: 4px; }
.nav-row .nav-item { flex: 1; min-width: 0; }
.nav-add {
  flex: none; width: 26px; height: 26px; margin-right: 4px; padding: 0;
  border: 1px solid transparent; border-radius: 7px; background: none;
  color: var(--fg-faint); font-family: inherit; font-size: 17px; line-height: 1;
  cursor: pointer; transition: .15s;
}
.nav-add:hover { color: var(--fg); border-color: var(--line); background: var(--bg-hover); }
.lib-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0 10px 8px; color: var(--fg-dim); font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 1.2px;
}
/* Room under the last shelf.  The list scrolls, so without it the final row
   ends flush against whatever heading comes next and reads as cut off rather
   than as scrollable. */
#lib-list { overflow-y: auto; flex: 1; padding-bottom: 8px; }
.lib-item {
  display: flex; align-items: center; gap: 11px; padding: 7px 10px;
  border-radius: 8px; cursor: pointer; transition: .15s;
}
.lib-item:hover { background: var(--bg-hover); }
.lib-item .swatch {
  width: 38px; height: 38px; border-radius: 6px; flex-shrink: 0;
  background: linear-gradient(135deg, var(--tile-a), var(--tile-b));
  border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 15px; color: var(--fg-dim);
  background-size: cover; background-position: center;
}
.lib-item .t { font-size: 13.5px; font-weight: 600; line-height: 1.25; }
.lib-item .s { font-size: 11.5px; color: var(--fg-faint); }
/* A built-in shelf carries a drawn mark instead of a borrowed card picture. */
.lib-item .swatch.mark { color: var(--fg-dim); padding: 7px; position: relative; }
.lib-item .swatch.mark svg { width: 100%; height: 100%; display: block; }
.lib-item:hover .swatch.mark { color: var(--fg); }

/* TWO WAYS TO WEAR THE SAME COLOUR.
 *
 * A word can be painted by clipping the gradient to the TEXT.  A drawn shape
 * cannot, because there is no text to clip to - so it is masked instead: the
 * element is a rectangle of the same moving gradient with the glyph punched
 * out of it.  Different mechanism, identical background and identical
 * animation, so NEW, 10 and the diamond drift together.
 *
 * Sold keeps its plain grey banknote.  What a card fetched is a fact, and the
 * shimmer is for the shelves that are about wanting things. */
.mark-text { font-size: 12px; font-weight: 800; }
.mark-text.ten { font-size: 17px; letter-spacing: -.02em; }
.mark-ink {
  position: absolute; inset: 4px;
  background-color: var(--fg-dim);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}
.mark-ink[data-mark="gem"] { -webkit-mask-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2038%2038'><g%20fill='none'%20stroke='%23000'%20stroke-width='2.2'%20stroke-linejoin='round'><path%20d='M11%209h16l6%207-14%2014L5%2016z'/><path%20d='M5%2016h28'/><path%20d='M11%209l3.5%207%204.5-7%204.5%207%203.5-7'/><path%20d='M14.5%2016%2019%2030l4.5-14'/></g></svg>"); mask-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2038%2038'><g%20fill='none'%20stroke='%23000'%20stroke-width='2.2'%20stroke-linejoin='round'><path%20d='M11%209h16l6%207-14%2014L5%2016z'/><path%20d='M5%2016h28'/><path%20d='M11%209l3.5%207%204.5-7%204.5%207%203.5-7'/><path%20d='M14.5%2016%2019%2030l4.5-14'/></g></svg>"); }
.mark-ink[data-mark="star"] { -webkit-mask-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2038%2038'><path%20fill='%23000'%20stroke='%23000'%20stroke-width='2.4'%20stroke-linejoin='round'%20d='M19%207l3.2%208.1%208.2.4-6.9%205.7%202.6%208.8L19%2025.2%2011.9%2030l2.6-8.8-6.9-5.7%208.2-.4z'/></svg>"); mask-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2038%2038'><path%20fill='%23000'%20stroke='%23000'%20stroke-width='2.4'%20stroke-linejoin='round'%20d='M19%207l3.2%208.1%208.2.4-6.9%205.7%202.6%208.8L19%2025.2%2011.9%2030l2.6-8.8-6.9-5.7%208.2-.4z'/></svg>"); }
.mark-ink[data-mark="heart"], .wish-ink { -webkit-mask-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2038%2038'><path%20fill='%23000'%20d='M19%2030c-.4%200-.8-.15-1.1-.44C13%2025.2%208%2021.1%208%2016.2%208%2012.8%2010.7%2010%2014%2010c2%200%203.8%201%205%202.6C20.2%2011%2022%2010%2024%2010c3.3%200%206%202.8%206%206.2%200%204.9-5%209-9.9%2013.36-.3.29-.7.44-1.1.44z'/></svg>"); mask-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2038%2038'><path%20fill='%23000'%20d='M19%2030c-.4%200-.8-.15-1.1-.44C13%2025.2%208%2021.1%208%2016.2%208%2012.8%2010.7%2010%2014%2010c2%200%203.8%201%205%202.6C20.2%2011%2022%2010%2024%2010c3.3%200%206%202.8%206%206.2%200%204.9-5%209-9.9%2013.36-.3.29-.7.44-1.1.44z'/></svg>"); }
.mark-ink[data-mark="trend"] { -webkit-mask-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2038%2038'><g%20fill='none'%20stroke='%23000'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'><path%20d='M9%2026l7.5-7.5%204.5%204.5L29%2013'/><path%20d='M22.5%2012.5H29V19'/></g></svg>"); mask-image: url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2038%2038'><g%20fill='none'%20stroke='%23000'%20stroke-width='2.4'%20stroke-linecap='round'%20stroke-linejoin='round'><path%20d='M9%2026l7.5-7.5%204.5%204.5L29%2013'/><path%20d='M22.5%2012.5H29V19'/></g></svg>"); }
/* SHINY ON HOVER, PLAIN AT REST.
 *
 * Four shelves shimmering permanently down the side of every page was the whole
 * sidebar asking for attention at once - and a sidebar is furniture: you look
 * at it to get somewhere else.  So the colour is a response to the pointer
 * rather than a state of the room, and at rest these are the same quiet grey
 * marks they were before.
 *
 * The AI gem keeps its shimmer: there is one of it, it means something specific
 * about that shelf, and it is not a navigation control. */
.lib-item:hover .mark-text,
.lib-item:hover .mark-ink {
  background-image:
    radial-gradient(closest-side, #FF6A00, rgba(0,0,0,0)),
    radial-gradient(closest-side, #00C8FF, rgba(0,0,0,0)),
    radial-gradient(closest-side, #FF2DAA, rgba(0,0,0,0)),
    linear-gradient(115deg, #7A2CFF, #0062FF);
  background-repeat: no-repeat;
  background-size: 3.5em 3.5em, 4em 4em, 3em 3em, 100% 100%;
  animation: ai-flow 12s linear infinite, ai-glow 5s ease-in-out infinite;
}
.lib-item:hover .mark-text {
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
/* The mask is already on the element; only the paint behind it changes. */
.lib-item:hover .mark-ink { background-color: transparent; font-size: 9px; }
@media (prefers-reduced-motion: reduce) {
  .lib-item:hover .mark-text, .lib-item:hover .mark-ink { animation: none; }
}
/* A collector's face fills its swatch and is round, because it is a person. */
.lib-item .swatch.face { border-radius: 50%; font-size: 13px; }

/* MADE BY THE MODEL.
 *
 * The same treatment in two sizes: a gem in the corner of the cover, and the
 * words on the shelf's own page.  Colour comes from four stacked gradients
 * clipped to the glyph - three drifting blobs over a base sweep - so it reads
 * as light moving across a surface rather than as a flat accent colour, which
 * is the point: nothing else in this app is iridescent, so nothing else can be
 * mistaken for it. */
.lib-item .swatch { position: relative; }
/* The disc and the gem are two elements on purpose.  `background-clip: text`
   clips EVERY background layer to the glyph, so an element wearing the gradient
   cannot also paint the dark circle it needs to sit on - the first version drew
   a ring via box-shadow and the middle stayed empty, which on a dark cover was
   very nearly invisible. */
.ai-gem {
  position: absolute; right: -4px; bottom: -4px;
  width: 18px; height: 18px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--bg); border: 1px solid var(--line);
}
.ai-gem .ai-flow { font-size: 10px; line-height: 1; font-style: normal; letter-spacing: 0; }
.ai-flow, .ai-ink, .brand-dot {
  background-image:
    radial-gradient(closest-side, #FF6A00, rgba(0,0,0,0)),
    radial-gradient(closest-side, #00C8FF, rgba(0,0,0,0)),
    radial-gradient(closest-side, #FF2DAA, rgba(0,0,0,0)),
    linear-gradient(115deg, #7A2CFF, #0062FF);
  background-repeat: no-repeat;
  background-size: 3.5em 3.5em, 4em 4em, 3em 3em, 100% 100%;
  animation: ai-flow 12s linear infinite, ai-glow 5s ease-in-out infinite;
}
/* Clipping to the glyph is what the TEXT ones do; the masked one must not, or
   it would clip its gradient to text it does not have and vanish. */
.ai-flow, .brand-dot {
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
.ai-flow { display: inline-block; letter-spacing: .12em; }
/* The full stop after the name: the one grey character in the logo, and a good
   place for the app's one piece of colour.  Small enough that the drift reads
   as a shimmer rather than as something moving. */
.brand-dot { display: inline-block; }

@keyframes ai-flow {
  0%, 100% { background-position: 220% 50%, -35% 85%, -35% 15%, 0 0; }
  25%      { background-position: 50% 90%, -97% 30%, 197% 30%, 0 0; }
  50%      { background-position: -120% 50%, 135% 15%, 135% 85%, 0 0; }
  75%      { background-position: 50% 10%, 197% 70%, -97% 70%, 0 0; }
}
@keyframes ai-glow {
  0%, 100% { filter: drop-shadow(0 0 7px rgba(127,150,255,.34)); }
  50%      { filter: drop-shadow(0 0 15px rgba(127,150,255,.52))
                     drop-shadow(0 0 30px rgba(255,152,91,.18)); }
}
/* Movement is decoration here, and decoration is the first thing to go. */
@media (prefers-reduced-motion: reduce) {
  .ai-flow, .ai-ink, .brand-dot { animation: none; filter: drop-shadow(0 0 7px rgba(127,150,255,.34)); }
}

/* The end of a paged grid: a line that says where you are in it, never a
   button - the next page is already on its way by the time you read this. */
/* Set matching.  One row per set string: what PSA prints, what it resolved to,
   and the control that overrides it - in that order, because that is the order
   the question is asked in. */
.tcgm-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 0; border-bottom: 1px solid var(--line);
}
.tcgm-set { flex: 1 1 320px; min-width: 0; font-size: 13px; }
.tcgm-set b { color: var(--fg); font-weight: 700; }
.tcgm-set i { display: block; font-style: normal; font-size: 11.5px; color: var(--fg-faint); margin-top: 2px; }
.tcgm-row select {
  flex: 0 1 260px; padding: 7px 10px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--bg-raised); color: var(--fg); font: inherit; font-size: 12.5px; cursor: pointer;
}
/* The row has to be able to show the open list, so it cannot clip it. */
.tcgm-row { overflow: visible; }
/* The searchable set chooser.  Anchored to its row rather than to the page, so
   opening one deep in a long list does not jump you somewhere else. */
.combo { position: relative; flex: 0 1 300px; }
.combo-in {
  width: 100%; padding: 7px 10px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--bg-raised); color: var(--fg); font: inherit; font-size: 12.5px; outline: none;
}
.combo-in:focus { border-color: var(--fg-faint); }
.combo-list {
  position: absolute; z-index: 40; left: 0; right: 0; top: calc(100% + 4px);
  max-height: 46vh; overflow-y: auto;
  background: var(--bg-raised); border: 1px solid var(--line); border-radius: 9px;
  box-shadow: 0 12px 32px rgba(0,0,0,.5); padding: 4px;
}
.combo-list.hidden { display: none; }
.combo-opt {
  display: block; width: 100%; text-align: left; padding: 7px 9px; border: 0;
  border-radius: 6px; background: none; color: var(--fg); font: inherit; font-size: 12.5px;
  font-weight: 600; cursor: pointer; line-height: 1.3;
}
.combo-opt i { display: block; font-style: normal; font-weight: 500; font-size: 11px; color: var(--fg-faint); }
.combo-opt:hover { background: var(--bg-hover); }
.combo-opt.none { color: var(--fg-faint); font-weight: 500; }
.combo-none { padding: 8px 9px; font-size: 11.5px; color: var(--fg-faint); }

.tcgm-cards:not(:empty) { padding: 10px 0 18px; }
.tcgm-cards select { max-width: 260px; }

/* The set catalogue on Discover: the same tiles as the picker, on a shelf. */
/* Filing the card from the player.  Full width, because it is an action rather
   than a fact - every other row on the back is a label and a value. */
.back-acts { margin: 14px 0 4px; }
.back-acts .ghost-btn { width: 100%; padding: 10px; font-size: 12.5px; color: var(--fg); }
.back-acts .ghost-btn:hover { border-color: var(--fg-faint); background: var(--bg-hover); }

/* ---------- what the app says back ----------
 *
 * Bottom right, above everything, and never over the sidebar: a report is not
 * a modal and must not take the pointer.  The stack itself ignores clicks - only
 * the toasts in it are hittable - so a message sitting over a button does not
 * eat the press. */
/* Clear of the player's controls: the reel's buttons live in the bottom-right
   corner, and a toast landing on top of them hid the one thing the user might
   want to press next. */
#player:not(.hidden) ~ #toasts { bottom: 108px; }
#toasts {
  position: fixed; right: 22px; bottom: 22px; z-index: 400;
  transition: bottom .18s;
  display: flex; flex-direction: column; gap: 10px; align-items: flex-end;
  pointer-events: none;
}
.toast {
  pointer-events: auto; display: flex; align-items: flex-start; gap: 10px;
  max-width: min(420px, calc(100vw - 44px));
  padding: 12px 12px 12px 14px; border-radius: 11px;
  background: var(--bg-raised); border: 1px solid var(--line);
  box-shadow: 0 14px 40px rgba(0,0,0,.45);
  font-size: 13px; line-height: 1.5; color: var(--fg);
  animation: toast-in .22s cubic-bezier(.2,.7,.3,1);
}
.toast.out { opacity: 0; transform: translateY(6px); transition: .2s ease; }
/* The update notice is an ACTION, not news: it carries its own button and it
   does not time out, because a page that has quietly gone stale should not
   stop mentioning it after four seconds. */
.update-toast { align-items: center; gap: 12px; }
.update-toast .solid-btn.tiny { width: auto; padding: 7px 13px; font-size: 12px; margin: 0; }
/* A hairline down the leading edge carries the kind.  Colour on the text itself
   would fight the message; a rule beside it is legible and quiet. */
.toast::before {
  content: ''; position: absolute; left: 0; top: 10px; bottom: 10px; width: 2px;
  border-radius: 2px; background: var(--fg-faint);
}
.toast { position: relative; overflow: hidden; }
.toast.err::before { background: #e5484d; }
.toast.ok::before { background: var(--fg-dim); }
.toast-msg { white-space: pre-line; padding-left: 8px; }
.toast-x {
  background: none; border: none; color: var(--fg-faint); cursor: pointer;
  font-size: 16px; line-height: 1; padding: 0 2px; margin-left: auto;
}
.toast-x:hover { color: var(--fg); }
@keyframes toast-in { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: none } }

/* The question. Same card, buttons and type as every other dialog here. */
/* HIDDEN MEANS HIDDEN, and it has to be said here.
 *
 * There is no blanket `.hidden { display: none }` in this stylesheet - every
 * element that hides declares it for itself, and an id selector beats a class
 * anyway.  Without this line `#ask { display: flex }` won against `.hidden`
 * and the backdrop sat over the whole app, permanently, swallowing every
 * click. */
#ask {
  position: fixed; inset: 0; z-index: 410; background: rgba(0,0,0,.62);
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
#ask.hidden { display: none; }
.ask-card { width: min(440px, 100%); }
.ask-card h2 { font-size: 18px; font-weight: 800; margin-bottom: 8px; line-height: 1.35; }
.ask-body {
  color: var(--fg-dim); font-size: 13px; line-height: 1.6; white-space: pre-line;
  max-height: 40vh; overflow-y: auto;
}
.ask-in {
  width: 100%; margin-top: 14px; padding: 10px 12px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--bg); color: var(--fg);
  font: inherit; font-size: 13px; outline: none;
}
.ask-in:focus { border-color: var(--fg-faint); }
.solid-btn.danger { background: #e5484d; color: #fff; }

/* THE CATALOGUE AT THE FOOT OF DISCOVER, and it is not `.tcg-grid`.
 *
 * It was, for one deploy, and that class already belonged to the set PICKER in
 * the album dialog - which is a 46vh box with its own scrollbar.  The catalogue
 * inherited the box: a little scrolling window inside the page, and because its
 * sentinel never left that window every remaining set loaded at once.  Its own
 * name, then, and it grows the page like the rest of it. */
.sets-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(178px, 1fr));
}
/* The carousel on Discover: the same mark tiles, in a row you push sideways
   rather than a grid you fall into.  The catalogue is a page of its own. */
.row.sets { grid-auto-columns: 186px; }
.row.sets .tcg-set { height: 100%; }

.tcg-row { gap: 12px; }
.tcg-row .tcg-set { flex: 0 0 172px; }

/* Admin: one set opened for curation.  Two columns, because the two decisions
   are independent - what a stranger SEES, and what we PAY to draw. */
.dset-body:not(:empty) { padding: 14px 0 20px; }
.dset-cols { display: grid; gap: 26px; grid-template-columns: minmax(240px, 320px) 1fr; }
.dset-cols h4 { font-size: 12px; text-transform: uppercase; letter-spacing: 1px;
  color: var(--fg-dim); margin: 0 0 10px; }
.dset-grid {
  display: grid; gap: 8px; max-height: 46vh; overflow-y: auto; padding-right: 4px;
  grid-template-columns: repeat(auto-fill, minmax(74px, 1fr));
}
.dset-card {
  position: relative; padding: 0; border: 2px solid transparent; border-radius: 7px;
  background: var(--bg-raised); cursor: pointer; overflow: hidden; line-height: 0;
}
.dset-card img { width: 100%; aspect-ratio: 0.715; object-fit: cover; opacity: .55; transition: .15s; }
.dset-card:hover img { opacity: .85; }
/* Chosen is a border and full colour, not a tick in a corner: at this size a
   tick is a smudge and the picture is the thing being judged. */
.dset-card.on { border-color: var(--fg); }
.dset-card.on img { opacity: 1; }
.dset-card span {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 2px 4px;
  font-size: 9.5px; font-weight: 700; line-height: 1.4; color: var(--fg);
  background: color-mix(in srgb, var(--bg) 78%, transparent);
}
.dset-sub {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 10px; font-size: 12px; color: var(--fg-faint);
}
.dset-actions { display: flex; align-items: center; gap: 12px; margin-top: 16px; flex-wrap: wrap; }
.dsets-head { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; flex-wrap: wrap; }
#dsets-q {
  flex: 1 1 260px; max-width: 340px; padding: 9px 14px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bg-raised); color: var(--fg); font: inherit; font-size: 13px;
}
/* A set kept off the shelf reads as dimmed rather than as missing: it is still
   in the list, still openable, still an album - it just is not featured. */
.tcgm-row.off .tcgm-set b { color: var(--fg-dim); }
.tcgm-row.off .tcgm-set i { color: var(--fg-faint); }

/* ---------- coverage, before anything is written ----------
 *
 * One number, big, because the whole point of the read-only pass is that a
 * percentage decides whether the id is worth keying artwork off at all. */
.fill-sum { display: flex; align-items: center; gap: 18px; margin: 14px 0 4px; }
.fill-big {
  flex: 0 0 auto; font-size: 34px; font-weight: 800; letter-spacing: -.03em;
  line-height: 1; color: var(--fg);
}
.fill-txt { min-width: 0; font-size: 13px; }
.fill-txt b { color: var(--fg); font-weight: 700; }
.fill-txt i { display: block; font-style: normal; font-size: 11.5px; color: var(--fg-faint); margin-top: 3px; }
.fill-note {
  margin-top: 12px; padding: 10px 12px; border-radius: 9px; font-size: 12.5px; line-height: 1.55;
  color: var(--fg-dim); background: var(--bg-raised); border: 1px solid var(--line);
}
.fill-note b { color: var(--fg); }
.fill-note.ok { color: var(--fg-dim); }
.fill-note.warn { border-color: color-mix(in srgb, #e5a23d 45%, var(--line)); }
.fill-note.warn span {
  display: block; margin-top: 3px; font-size: 11.5px; color: var(--fg-faint);
  font-variant-numeric: tabular-nums;
}
.fill-det { margin-top: 12px; }
.fill-det summary {
  cursor: pointer; font-size: 12px; color: var(--fg-dim); padding: 6px 0;
}
.fill-det summary:hover { color: var(--fg); }
.fill-row { padding: 7px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
.fill-row b { color: var(--fg); font-weight: 700; }
.fill-row i { display: block; font-style: normal; font-size: 11.5px; color: var(--fg-faint); margin-top: 2px; }
/* ---------- the card list ----------
 *
 * A table, not a grid of tiles: the job here is going down a column filling in
 * what you paid, and tiles put four of those columns side by side.  The picture
 * is small and present only so you know which card the row is - it is the one
 * thing that makes a table of cert numbers legible. */
.cl-wrap { overflow-x: auto; margin-top: 4px; }
table.cl { width: 100%; border-collapse: collapse; font-size: 12.5px; }
table.cl th {
  text-align: left; padding: 8px 10px 8px 0; font-size: 10.5px; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--fg-faint);
  border-bottom: 1px solid var(--line); white-space: nowrap; position: sticky; top: 0;
  background: var(--bg); z-index: 2;
}
table.cl td { padding: 6px 10px 6px 0; border-bottom: 1px solid var(--line); vertical-align: middle; }
table.cl th.num, table.cl td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* The grade is two short words and must never become two lines. */
table.cl td:nth-child(4) { white-space: nowrap; color: var(--fg-dim); }
#cl-q {
  flex: 1 1 240px; max-width: 320px; padding: 9px 14px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bg-raised); color: var(--fg);
  font: inherit; font-size: 13px; outline: none;
}
#cl-sort {
  padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line);
  background: var(--bg-raised); color: var(--fg); font: inherit; font-size: 12.5px; cursor: pointer;
}
.cl-thumb {
  width: 30px; height: 42px; border-radius: 3px; background-size: cover;
  background-position: center; background-color: var(--bg-raised);
}
.cl-thumb.empty { border: 1px dashed var(--line); }
.cl-name { min-width: 170px; }
.cl-name b { color: var(--fg); font-weight: 700; }
.cl-name i { display: block; font-style: normal; font-size: 11px; color: var(--fg-faint); margin-top: 1px; }
.cl-set { color: var(--fg-dim); max-width: 210px; font-size: 11.5px; }
.cl-in {
  width: 86px; padding: 5px 8px; border-radius: 7px; border: 1px solid var(--line);
  background: var(--bg-raised); color: var(--fg); font: inherit; font-size: 12px; outline: none;
}
.cl-in:focus { border-color: var(--fg-faint); }
.cl-in.date { width: 132px; }
.cl-in.wide { width: 100%; min-width: 130px; }
/* An unsaved row is marked, because the whole point of holding edits is that
   you can see how many are still in your hands. */
table.cl tr.dirty td { background: color-mix(in srgb, var(--fg) 6%, transparent); }
table.cl tr.dirty .cl-in { border-color: color-mix(in srgb, var(--fg) 35%, var(--line)); }
.grow { flex: 1; }

.key-chip {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10.5px;
  padding: 1px 5px; border-radius: 4px; color: var(--fg-dim);
  background: var(--bg-raised); border: 1px solid var(--line);
}
/* A row keyed on our own id reads as a different KIND of row, not a worse one. */
.fill-row.own b { color: var(--fg-dim); }
.lang-chip {
  display: inline-block; margin-left: 7px; padding: 1px 6px; border-radius: 5px;
  font-size: 10px; font-weight: 700; letter-spacing: .02em; vertical-align: 1px;
  color: var(--fg-dim); background: var(--bg-raised); border: 1px solid var(--line);
}
.fill-over {
  display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--fg-faint); cursor: pointer;
}

.qrow { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; font-size: 13px; color: var(--fg-dim); }
.qrow b { color: var(--fg); font-size: 15px; }

/* ---------- set albums ----------
 *
 * A sticker album: every card in the set gets a slot, empty ones drawn faint
 * from TCGdex, yours filling in.  The empty slots are the content - a grid
 * that only showed what you had would be the shelf this replaced. */
.tcg-bar { display: flex; gap: 10px; margin-bottom: 14px; }
#tcg-q { flex: 1; padding: 9px 14px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--bg-raised); color: var(--fg); font: inherit; font-size: 13px; outline: none; }
#tcg-lang { padding: 9px 12px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--bg-raised); color: var(--fg); font: inherit; font-size: 13px; cursor: pointer; }
.tcg-grid {
  display: grid; gap: 10px; max-height: 46vh; overflow-y: auto; padding-right: 4px;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.tcg-set {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 14px 10px 10px; border: 1px solid var(--line); border-radius: 10px;
  background: var(--bg-raised); cursor: pointer; color: var(--fg-dim);
  font: inherit; text-align: center; transition: .15s;
}
.tcg-set:hover { border-color: var(--fg-faint); background: var(--bg-hover); }
/* SET LOGOS NEED THE BACKGROUND THEY WERE DRAWN FOR.
 *
 * I first assumed most would survive a dark tile.  They do not - they load
 * perfectly and are simply invisible, because a set logo is print artwork in
 * dark ink with a transparent background, and this app is near-black.  So the
 * logo gets a light plate of its own: it is what the artwork expects, it works
 * for all 218 of them, and it reads as the front of a booster rather than as a
 * hole in the tile.  Inverting would have ruined the colourful ones. */
.tcg-set .tcg-logo {
  width: 100%; height: 56px; border-radius: 7px;
  /* Not white.  A set logo is print artwork with a transparent background, and
     the modern ones are drawn in WHITE for a booster's dark wrapper - so on a
     white plate they vanish exactly as they did on the black tile, which is how
     I got this wrong twice.  A light grey plate plus a dark outline on the
     artwork itself is the only combination that carries both: dark logos read
     against the plate, white ones read against their own outline. */
  background: #dfe0e4; display: grid; place-items: center; padding: 6px;
  overflow: hidden;
}
.tcg-set .tcg-logo img {
  /* WHY min-height: 0 MATTERS HERE.
   *
   * The logo is a grid item, and a grid item's `min-height` defaults to `auto`
   * - meaning it refuses to shrink below its own intrinsic size.  So a tall
   *   logo kept its full height, overflowed the 56px plate, and was cut off at
   *   the bottom by `overflow: hidden`: Ascended Heroes and Phantasmal Flames
   *   both lost their lower half.  `object-fit: contain` never got a chance,
   *   because the BOX was never allowed to be small enough to contain into.
   *
   * The tile is the right size; the image simply has to be told it may be
   * smaller than it wants to be. */
  min-width: 0; min-height: 0; max-width: 100%; max-height: 100%;
  width: 100%; height: 100%; object-fit: contain; display: block;
  filter: drop-shadow(0 0 1px rgba(0,0,0,.85)) drop-shadow(0 1px 2px rgba(0,0,0,.45));
}
.tcg-set-name { font-size: 13px; font-weight: 700; color: var(--fg); min-height: 52px; display: grid; place-items: center; }
.tcg-set-meta { font-size: 11.5px; font-weight: 600; color: var(--fg); line-height: 1.3; }
.tcg-set-meta i { display: block; font-style: normal; font-size: 10.5px; color: var(--fg-faint); font-weight: 500; }

.tcg-chosen { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; }
.tcg-chosen .tcg-logo { width: auto; height: 46px; min-width: 120px; max-width: 190px; }
.tcg-chosen .tcg-logo img { min-width: 0; min-height: 0; width: 100%; height: 100%; object-fit: contain; }

/* The same plate, small, in the admin list - so a set is recognised by its
   logo there too rather than by reading its name. */
.tcg-logo.row {
  flex: none; width: 84px; height: 38px; border-radius: 6px; padding: 4px;
  background: #dfe0e4; display: grid; place-items: center; overflow: hidden;
}
.tcg-logo.row.empty { background: var(--bg-hover); border: 1px solid var(--line); }
.tcg-logo.row img {
  min-width: 0; min-height: 0; width: 100%; height: 100%; object-fit: contain; display: block;
  filter: drop-shadow(0 0 1px rgba(0,0,0,.85));
}
/* A set can print a dozen rarities, so this list scrolls rather than pushing
   the create button off the bottom of the dialog. */
.tcg-rar { display: flex; flex-direction: column; gap: 6px; max-height: 44vh; overflow-y: auto; }
.tcg-pick {
  display: flex; align-items: center; gap: 11px; width: 100%;
  padding: 11px 13px; border: 1px solid var(--line); border-radius: 9px;
  background: var(--bg-raised); color: var(--fg); font: inherit; font-size: 13.5px;
  cursor: pointer; text-align: left; transition: .15s;
}
.tcg-pick:hover { border-color: var(--fg-faint); }
.tcg-pick.on { border-color: var(--fg); background: var(--bg-hover); }
.tcg-pick.empty { opacity: .4; cursor: default; }
.tcg-pick .tick {
  flex: none; width: 18px; height: 18px; border-radius: 5px; border: 1px solid var(--fg-faint);
  display: grid; place-items: center; font-size: 11px;
}
.tcg-pick.on .tick { background: var(--fg); color: var(--bg); border-color: var(--fg); }
.tcg-pick-name { flex: 1; font-weight: 600; }
.tcg-pick-name i { font-style: normal; font-weight: 500; color: var(--fg-faint); font-size: 12px; }
.tcg-pick-n { color: var(--fg-faint); font-variant-numeric: tabular-nums; font-size: 12.5px; }
.tcg-total { margin-top: 14px; font-size: 12.5px; color: var(--fg-dim); }

.album-bar {
  height: 5px; width: min(420px, 100%); margin: 16px 0 8px;
  border-radius: 3px; overflow: hidden; background: var(--bg-hover); border: 1px solid var(--line);
}
.album-bar i { display: block; height: 100%; background: var(--fg); transition: width .6s ease; }
.album-count { font-size: 12.5px; color: var(--fg-faint); }
.album-count b { color: var(--fg); font-size: 14px; }
.album {
  display: grid; gap: 14px; margin-top: 26px;
  /* Wider than a plain thumbnail grid would need: a slab carries a printed
     label, and below about 150px the set line stops being readable. */
  grid-template-columns: repeat(auto-fill, minmax(152px, 1fr));
}
/* A SLOT HOLDS A SLAB, empty or not.
 *
 * Everything else in this app draws a card as a graded slab; an album of bare
 * scans beside a vault of slabs read as a different product.  So the empty ones
 * wear the same case, labelled with the card that belongs there and graded 10 -
 * the one you are hoping to find - and simply faded, so nothing about them
 * claims to be yours. */
.slot { position: relative; }
.slot .slab { transition: .18s; }
.slab.ghost { opacity: .3; filter: saturate(.55); }
.slot:hover .slab.ghost { opacity: .5; filter: saturate(.8); }
/* EXCEPT IN THE CATALOGUE.
 *
 * The fade means one thing: you do not have this one.  That is the whole point
 * of an album and it is meaningless on Discover's set pages, which are not
 * about owning anything - they are what the set IS.  Fading every card there
 * would say "you are missing all 295 of these", which is true of a card
 * catalogue in the same way it is true of a shop window. */
.album.catalogue .slab.ghost,
.album.catalogue .slot:hover .slab.ghost { opacity: 1; filter: none; }
.slab.ghost .window img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* No cert to print on an empty one, and an empty cert line leaves a gap where
   the eye expects a number. */
.slab.ghost .psa-r .cert { visibility: hidden; }
.slot.got { cursor: pointer; }
.slot.got:hover .slab { transform: translateY(-2px); }
/* A catalogue slot that HAS a Discover card behind it: openable, so it behaves
   like one.  The ones with nothing behind them stay inert rather than offering
   a click that opens an empty player. */
.slot.live { cursor: pointer; }
.slot.live:hover .slab { transform: translateY(-2px); }
/* The vault mark, on the slab rather than beside it - there is no tile line
   here to put it on.  Same glyph and the same two colours the shelves use. */
.slot-own {
  position: absolute; right: 7px; top: 7px; z-index: 2;
  display: inline-flex; line-height: 0; padding: 3px; border-radius: 5px;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
}
.slot-own.exact { color: var(--own-exact); }
.slot-own.other { color: var(--own-other); }

.pg-note {
  padding: 26px 0 8px; text-align: center;
  color: var(--fg-faint); font-size: 12px; letter-spacing: .3px;
}
.side-foot { padding-top: 12px; border-top: 1px solid var(--line); }
#theme-toggle {
  width: 100%; padding: 9px; background: none; border: 1px solid var(--line);
  border-radius: 8px; color: var(--fg-dim); font-weight: 600; font-size: 13px; cursor: pointer;
}
#theme-toggle:hover { color: var(--fg); border-color: var(--fg-faint); }

/* ---------- main ---------- */
#main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
#topbar {
  display: flex; align-items: center; gap: 18px;
  padding: 16px 32px; border-bottom: 1px solid var(--line);
}
#search {
  width: 340px; padding: 10px 16px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bg-raised); color: var(--fg);
  font-size: 14px; outline: none; font-family: inherit;
}
#search:focus { border-color: var(--fg-faint); }
.stats { margin-left: auto; color: var(--fg-faint); font-size: 13px; font-weight: 500; letter-spacing: .3px; }
.stats b { color: var(--fg-dim); font-weight: 700; }
#content { flex: 1; overflow-y: auto; padding: 28px 32px 60px; }

h1.page { font-size: 30px; font-weight: 800; letter-spacing: -.5px; margin-bottom: 4px; }
.sub { color: var(--fg-dim); font-size: 14px; margin-bottom: 26px; }
.shelf { margin-bottom: 38px; }
.shelf-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
.shelf-head h2 { font-size: 20px; font-weight: 800; letter-spacing: -.3px; }
.shelf-head .tagline { color: var(--fg-faint); font-size: 13px; font-weight: 500; letter-spacing: 0; margin-left: 8px; }
.shelf-head a { color: var(--fg-dim); font-size: 12.5px; font-weight: 700; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; }
.shelf-head a:hover { color: var(--fg); }

/* rows */
.row { display: grid; grid-auto-flow: column; gap: 16px; overflow-x: auto; padding-bottom: 8px; scrollbar-width: thin; }
.row.arrivals { grid-auto-columns: 250px; }
.row.std { grid-auto-columns: 168px; }

/* ---------- arrival tiles (big, extension preview) ---------- */
.arrival {
  position: relative; border-radius: 12px; overflow: hidden; cursor: pointer;
  aspect-ratio: 4/5; background: linear-gradient(160deg, var(--tile-a), var(--tile-b));
  border: 1px solid var(--line); transition: transform .2s, box-shadow .2s;
}
.arrival:hover { transform: translateY(-4px); box-shadow: 0 18px 44px var(--shadow); }
.arrival .ext-bg { position: absolute; inset: 0; background-size: cover; background-position: center; }
.arrival .ext-pending {
  position: absolute; inset: 0;
  background:
    radial-gradient(90% 70% at 30% 20%, rgba(255,255,255,.07), transparent),
    radial-gradient(80% 60% at 75% 80%, rgba(255,255,255,.05), transparent),
    linear-gradient(160deg, var(--tile-a), var(--tile-b));
}
/* the generated art is a backdrop, not the subject: hold it back so the slab
   reads first and the footer text stays legible */
.arrival .ext-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(74% 64% at 50% 42%, transparent, rgba(0,0,0,.22) 80%),
    linear-gradient(to top, rgba(0,0,0,.55), transparent 30%);
}
.arrival .badge {
  position: absolute; left: 12px; bottom: 40px;
  background: var(--accent); color: var(--accent-fg);
  font-weight: 800; font-size: 12.5px; padding: 4px 10px; border-radius: 3px;
  letter-spacing: .3px; max-width: 80%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.arrival .foot {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 8px 12px;
  background: linear-gradient(transparent, rgba(0,0,0,.72));
  color: #eee; font-size: 11.5px; font-weight: 500;
}
[data-theme="light"] .arrival .foot { color: #f4f4f4; }
/* A catalogue slot whose picture does not exist anywhere - TCGdex ships some
   sets imageless - shows the card's name in the frame instead of a browser's
   broken-image glyph.  The span is always in the markup; only .noart shows it. */
.window .noart-name { display: none; }
.window.noart {
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(160deg, var(--tile-a), var(--tile-b));
}
.window.noart .noart-name {
  display: block; text-align: center; padding: 8px;
  font-size: 11px; font-weight: 700; line-height: 1.35; color: var(--fg-dim);
}
.window.noart .noart-name i {
  display: block; font-style: normal; font-weight: 600;
  font-size: 10px; color: var(--fg-faint); margin-top: 3px;
}

/* ---------- the paid door ---------- */
/* The one button on the demo home: white, big, and wearing PSA's red so the
   promise ("this connects to the real vault") is visible before the words. */
/* This class sets its own display, which at equal specificity beats the
   global `.hidden` declared earlier - so hidden must win explicitly, or the
   Connect button greets people who already connected. */
.psa-connect.hidden { display: none; }
.psa-connect {
  display: inline-flex; align-items: center; gap: 12px;
  background: #fff; color: #000; border: 0; cursor: pointer;
  font-weight: 800; font-size: 17px; letter-spacing: -.01em;
  padding: 16px 30px; border-radius: 14px;
  box-shadow: 0 14px 44px rgba(0,0,0,.45);
  transition: transform .15s, box-shadow .15s;
}
.psa-connect:hover { transform: translateY(-1px); box-shadow: 0 18px 54px rgba(210,38,48,.35); }
.psa-chip {
  display: inline-block; background: #d22630; color: #fff;
  font-weight: 900; font-size: 13px; letter-spacing: .06em;
  padding: 4px 8px; border-radius: 5px;
}
.psa-chip.big { font-size: 15px; padding: 6px 11px; margin-bottom: 14px; }
/* The real mark, wherever PSA is spoken of. Red on white is its home; the
   white keyline in the art keeps it legible on dark. */
.psa-logo { height: 22px; display: inline-block; }
.psa-logo.big { height: 34px; display: block; margin-bottom: 14px; }
.psa-connect.side .psa-logo { height: 18px; }
.demo-hero h1.page { line-height: 1.12; }
.demo-hero h1.page em { font-style: normal; color: #8b6bff; }
.promo-perks { list-style: none; margin: 16px 0 20px; padding: 0; }
.promo-perks li {
  padding: 7px 0 7px 26px; position: relative;
  font-size: 13.5px; color: var(--fg-dim, #b9b9c0); line-height: 1.5;
}
.promo-perks li b { color: var(--fg); }
.promo-perks li::before {
  content: '✓'; position: absolute; left: 2px; top: 7px;
  color: #35c489; font-weight: 800;
}
.plan-row { display: flex; gap: 12px; margin: 6px 0 12px; }
.plan-card {
  flex: 1; text-align: left; cursor: pointer;
  background: var(--bg-raised); border: 1px solid var(--line);
  border-radius: 12px; padding: 14px 16px; color: var(--fg);
  transition: border-color .15s, transform .15s;
}
.plan-card:hover { border-color: var(--fg-faint); transform: translateY(-1px); }
.plan-card.best { border-color: #8b6bff; }
.plan-card .plan-name { font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--fg-faint); }
.plan-card .plan-price { font-size: 24px; font-weight: 850; margin: 4px 0 2px; }
.plan-card .plan-price span { font-size: 13px; font-weight: 600; color: var(--fg-faint); }
.plan-card .plan-note { font-size: 12px; color: var(--fg-faint); }
.plan-card .plan-note.ok { color: #35c489; font-weight: 600; }
.plan-card:disabled { opacity: .6; cursor: default; }

/* Trending shelf: the 30-day climb, worn on the tile.  Green because it is
   only ever shown for a rise - the shelf has no falling members. */
.arrival .climb-chip {
  position: absolute; top: 10px; right: 10px;
  background: rgba(18, 46, 28, .82); color: #6fe3a1;
  font-weight: 800; font-size: 12px; letter-spacing: .3px;
  padding: 4px 9px; border-radius: 999px;
  border: 1px solid rgba(111, 227, 161, .35);
  backdrop-filter: blur(6px);
}
.arrival .play-hover {
  position: absolute; right: 12px; bottom: 14px; width: 44px; height: 44px; border-radius: 50%;
  background: var(--accent); color: var(--accent-fg); display: flex; align-items: center; justify-content: center;
  font-size: 17px; opacity: 0; transform: translateY(6px); transition: .2s; box-shadow: 0 8px 22px rgba(0,0,0,.5);
}
.arrival:hover .play-hover { opacity: 1; transform: translateY(0); }

/* ---------- standard card tile ---------- */
.tile {
  cursor: pointer; border-radius: 10px; padding: 12px;
  background: var(--bg-raised); border: 1px solid var(--line); transition: .18s;
}
.tile:hover { background: var(--bg-hover); transform: translateY(-3px); }
.tile .face {
  aspect-ratio: 0.715; border-radius: 6px; margin-bottom: 10px;
  background-size: cover; background-position: center; position: relative; overflow: hidden;
}
.tile .face.ph {
  background: linear-gradient(165deg, var(--tile-a), var(--tile-b));
  border: 1px solid var(--line);
  display: flex; flex-direction: column; justify-content: space-between; padding: 10px;
}
.ph .ph-set { font-size: 8.5px; letter-spacing: 1px; text-transform: uppercase; color: var(--fg-faint); font-weight: 700; }
.ph .ph-name { font-size: 15px; font-weight: 800; line-height: 1.1; letter-spacing: -.2px; }
.ph .ph-grade {
  align-self: flex-end; width: 30px; height: 30px; border-radius: 50%;
  border: 1.5px solid var(--fg-dim); color: var(--fg);
  display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 13px;
}
.tile .t { font-size: 13px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tile .s {
  font-size: 11.5px; color: var(--fg-faint); margin-top: 2px;
  display: flex; align-items: baseline; gap: 6px;
}
.tile .s .txt { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tile .s .mv { flex: 0 0 auto; }

/* collection page */
.col-hero { display: flex; align-items: flex-end; gap: 24px; margin-bottom: 30px; }
.col-hero .cover {
  width: 190px; height: 190px; border-radius: 10px; flex-shrink: 0;
  background: linear-gradient(145deg, var(--tile-a), var(--tile-b));
  border: 1px solid var(--line); background-size: cover; background-position: center;
  display: flex; align-items: center; justify-content: center; font-size: 46px; font-weight: 900; color: var(--fg-faint);
}
.col-hero .k { text-transform: uppercase; letter-spacing: 1.6px; font-size: 11px; font-weight: 800; color: var(--fg-dim); }
.col-hero h1 { font-size: 44px; font-weight: 900; letter-spacing: -1px; margin: 6px 0; }
.col-hero .meta { color: var(--fg-dim); font-size: 13.5px; }
.play-big {
  margin-top: 16px; display: inline-flex; align-items: center; gap: 10px;
  background: var(--accent); color: var(--accent-fg); border: none; cursor: pointer;
  font-weight: 800; font-size: 14px; padding: 12px 26px; border-radius: 999px;
  font-family: inherit; letter-spacing: .4px;
}
.play-big:hover { transform: scale(1.04); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; }

/* dashboard / hero actions */
.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
.hero-actions.top { margin: 0 0 26px; }
.ghost-big {
  display: inline-flex; align-items: center; gap: 8px;
  background: none; color: var(--fg); border: 1px solid var(--line); cursor: pointer;
  font-weight: 700; font-size: 14px; padding: 12px 22px; border-radius: 999px;
  font-family: inherit; letter-spacing: .3px;
}
.ghost-big:hover { border-color: var(--fg-faint); background: var(--bg-hover); }
.hero-actions .play-big { margin-top: 0; }

/* price movement */
.mv { font-weight: 800; font-size: .92em; white-space: nowrap; }
.mv.flat { color: var(--fg-faint); }
.mv .abs { font-weight: 700; opacity: .72; font-size: .92em; }
.stats .mv { margin: 0 2px; }
/* The arrow that rides the value.  Smaller than the .mv chips and lifted a
   little, so it reads as punctuation on the number rather than as a second
   figure competing with it. */
.vm { font-style: normal; font-size: .78em; margin-left: 5px; vertical-align: 1px; }
.vm.up { color: #1f9d63; }
.vm.down { color: #d8564f; }
.unpriced { font-style: italic; opacity: .7; }
.mv.up { color: #1f9d63; }
.mv.down { color: #d8564f; }
/* Population on the Rarities shelf: a fact, not a movement - silver, not
   green or red, because scarcity has no direction. */
.mv.pop { color: var(--fg-faint); font-weight: 700; }
[data-theme="dark"] .mv.up { color: #35c489; }
[data-theme="dark"] .mv.down { color: #ef7b74; }
.arrival .foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.arrival .foot .mv { color: #4ee0a1; }
.arrival .foot .mv.down { color: #ff9d95; }
#player-meta .mv.up { color: #35c489; }
#player-meta .mv.down { color: #ef7b74; }
.back-rows .mv { font-size: 1em; }

/* ---------- player ---------- */
#player { position: fixed; inset: 0; background: #000; z-index: 100; }
#player.hidden, #import-modal.hidden { display: none; }

/* The chosen import file, shown in place of the textarea.
   `.hidden` is not a global rule in this stylesheet - it is declared per
   element - so both of these need saying explicitly or the card and the
   textarea end up on screen at the same time. */
.file-card {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 14px 14px 16px;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--bg-raised); color: var(--fg-dim);
  min-height: 64px;
}
.file-card.hidden { display: none; }
#import-input.hidden { display: none; }
.file-card-body { flex: 1; min-width: 0; }
.file-card-name {
  color: var(--fg); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.file-card-meta { font-size: .85em; margin-top: 2px; }
/* ---------- player backdrop ----------
   Only RAW artwork reaches here - never `wide`/`ext`, which are composites
   with the slab already pasted in and which put a second slab behind the real
   one.  See showSlide().  --art is set from JS.
 *
 * DRAWN TWICE, and it has to be.
 *
 * `cover` alone is a crop, and the size of that crop is the difference between
 * the two aspect ratios.  A 16:9 wallpaper on a 16:10 screen loses a few
 * percent - invisible.  A 4:5 background on a 16:9 screen keeps 44% of its
 * height and magnifies it 2.3x: the extended scenery is cropped away and what
 * is left is a blow-up of the middle.  That is the "zoomed in, not extended
 * any more" look, and it appeared the moment 16:9 generation was switched off
 * to stop the crawler running out of memory - every card built since then has
 * only a 4:5 background to offer.
 *
 * So: the artwork itself is drawn `contain`, whole and uncropped, over a
 * blurred blow-up of itself that fills whatever the viewport ratio leaves
 * over.  Nothing is ever cropped, at any ratio, whether or not a 16:9
 * background exists.                                                          */
#player-bg, #player-bg2 {
  position: absolute; inset: 0;
  transition: opacity 1.1s ease; opacity: 0;
  filter: brightness(.86) saturate(1.0);   /* calmed, but the art is the point */
}
/* the wings: same image, blown up to fill, blurred back out of the way */
#player-bg::before, #player-bg2::before {
  content: ''; position: absolute; inset: -8%;
  background-image: var(--art);
  background-size: cover; background-position: center;
  filter: blur(38px) saturate(1.25) brightness(.72);
}
/* The artwork itself.  --fit is decided per image by setArt() in app.js:
   `cover` when the stage is close enough to the artwork's own ratio that the
   crop is not worth noticing, `contain` when it is not.  The default here is
   `contain`, so an image whose size is not measured yet is never hard-cropped
   on the way in. */
#player-bg::after, #player-bg2::after {
  content: ''; position: absolute; inset: 0;
  background-image: var(--art);
  background-size: var(--fit, contain); background-position: center;
  background-repeat: no-repeat;
}
#player-bg.on, #player-bg2.on { opacity: 1; }
/* No generated artwork yet - ambient blur only, so the slab cut-out never
   appears sharp behind the slab itself. */
#player-bg.blurred::after, #player-bg2.blurred::after { display: none; }
#player-bg.blurred::before, #player-bg2.blurred::before {
  filter: blur(46px) saturate(1.15) brightness(.55); transform: scale(1.18);
}
.player-vignette {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(80% 80% at 50% 44%, transparent 0%, rgba(0,0,0,.20) 62%, rgba(0,0,0,.52) 100%),
    linear-gradient(to top, rgba(0,0,0,.62), transparent 32%);
}
#player-card-wrap { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
/* ---------- player stage: parallax + flip ---------- *
   Pointer position drives one rotation on #player-tilt; the label and the card
   sit at different translateZ, so they separate as the slab turns.  The flip
   lives inside the same 3D context, so the two transforms compose instead of
   fighting.                                                                */
#player-stage {
  width: min(49vh, 40vw);
  perspective: 1300px;
  transition: opacity .6s ease;
}
#player-tilt {
  position: relative;
  transform-style: preserve-3d;
  transition: transform .5s cubic-bezier(.2,.8,.25,1);
  will-change: transform;
  --sx: 0px; --sy: 0px;
}
#player-tilt.live { transition: transform .12s ease-out; }
/* the cast shadow leans the opposite way to the tilt */
#player-tilt::after {
  content: ''; position: absolute; left: 6%; right: 6%; top: 12%; bottom: -4%;
  background: rgba(0,0,0,.72); filter: blur(38px); border-radius: 40px;
  transform: translate3d(var(--sx), var(--sy), -60px);
  transition: transform .5s cubic-bezier(.2,.8,.25,1);
}
#player-flip {
  position: relative; transform-style: preserve-3d;
  transition: transform .75s cubic-bezier(.3,.8,.25,1);
}
#player-flip.flipped { transform: rotateY(180deg); }
#player-flip .face { backface-visibility: hidden; -webkit-backface-visibility: hidden; }
/* backface-visibility hides the away-facing side but Chromium still hit-tests
   it, so the chart underneath never sees a pointer - gate hits explicitly */
#player-flip.flipped .front, #player-flip .back { pointer-events: none; }
#player-flip.flipped .back { pointer-events: auto; }
#player-flip .back {
  position: absolute; inset: 0; transform: rotateY(180deg);
  display: flex; flex-direction: column;   /* pinned head + scrolling body */
  background: #08080a; border-radius: 14px;
  border: 1px solid rgba(255,255,255,.13);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.10);
  overflow: hidden;
}
#player-slab .slab {
  border-radius: 14px;
  transform-style: preserve-3d;
  backdrop-filter: none;              /* flattens the 3D context in WebKit */
  -webkit-backdrop-filter: none;
  background: linear-gradient(178deg, rgba(255,255,255,.30), rgba(255,255,255,.13) 34%, rgba(255,255,255,.08));
}
/* depth layers */
#player-slab .psa-label { transform: translateZ(34px); }
#player-slab .window { transform: translateZ(18px); }
#player-hint {
  position: relative; z-index: 2; margin-top: 4.2vh; text-align: center; color: rgba(255,255,255,.5);
  font-size: 12px; letter-spacing: .6px; text-transform: uppercase; font-weight: 700;
  transition: color .2s;
}
#player-stage:hover ~ #player-hint { color: rgba(255,255,255,.8); }

/* ---------- card back ---------- */
.back-in {
  --series-1: #ffffff;             /* one series, so identity needs no hue */
  --ink: #f4f4f5; --ink-dim: #9a9aa4; --hair: rgba(255,255,255,.10);
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  padding: 4.5% 7.5% 6%; color: var(--ink); text-align: left;
  /* The back used to be a fixed panel with `justify-content: space-between`,
     which was fine while it held a chart and five rows.  It now holds editable
     fields, sales and provenance - more than fits - and `overflow: hidden` on
     the face meant the Save button was simply cut off the bottom with no way to
     reach it.  So the back scrolls. */
  overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
  scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.22) transparent;
}
.back-in::-webkit-scrollbar { width: 5px; }
.back-in::-webkit-scrollbar-thumb { background: rgba(255,255,255,.22); border-radius: 999px; }
.back-in::-webkit-scrollbar-track { background: transparent; }
/* A hint that there is more below, drawn on the face so it does not scroll away.
   Fades out once the back is scrolled to the end. */
#player-flip .back::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 46px;
  background: linear-gradient(to top, #08080a, rgba(8,8,10,0));
  pointer-events: none; opacity: 1; transition: opacity .2s;
}
#player-flip .back.at-end::after { opacity: 0; }
/* Pinned: the head is a sibling of the scrolling body now, so the card always
   says which card it is. */
.back-head {
  display: flex; align-items: flex-start; gap: .8em;
  padding: 5.5% 5.5% 3.2%;
  border-bottom: 1px solid rgba(255,255,255,.07);
}
.back-head .bh-text { flex: 1; min-width: 0; }
.back-head .bh-flip {
  flex: none; width: 34px; height: 34px; display: grid; place-items: center;
  border-radius: 10px; cursor: pointer;
  color: rgba(255,255,255,.75);
  background: rgba(255,255,255,.07);
  border: 1px solid rgba(255,255,255,.14);
  transition: .14s;
}
.back-head .bh-flip:hover { color: #fff; background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.3); }
.back-head .bh-flip:active { transform: scale(.94); }
.back-head .bn { font-size: 1.5em; font-weight: 800; letter-spacing: -.3px; line-height: 1.15; }
.back-head .bs { font-size: .82em; color: var(--ink-dim); margin-top: .3em; line-height: 1.35; }
.chart-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.chart-head .ct { font-size: .72em; text-transform: uppercase; letter-spacing: 1px; color: var(--ink-dim); font-weight: 700; }
.chart-head .ct .sample {
  text-transform: none; letter-spacing: 0; font-weight: 600; margin-left: .5em;
  color: #6f6f78; border: 1px solid var(--hair); border-radius: 3px; padding: 0 .35em;
}
.chart-head .cv b { font-size: 1.15em; font-weight: 800; }
.chart-head .cv .delta { font-size: .76em; margin-left: .4em; font-weight: 700; }
.chart-head .cv .delta { color: var(--ink-dim); }
.chart-head .cv .delta.up::before { content: '▲ '; font-size: .8em; }
.chart-head .cv .delta.down::before { content: '▼ '; font-size: .8em; }
.spark { display: block; width: 100%; height: 19vh; margin-top: 3%; overflow: visible; cursor: crosshair; }
.spark .g { stroke: var(--hair); stroke-width: .3; vector-effect: non-scaling-stroke; }
/* the series is drawn as evenly spaced dots: a zero-length dash with round
   caps puts one round dot per gap, so they follow the curve exactly */
.spark .line {
  fill: none; stroke: var(--series-1); stroke-width: 2.6;
  vector-effect: non-scaling-stroke;
  stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 0.01 6.5;
}
.spark .cross { stroke: rgba(255,255,255,.45); stroke-width: 1; vector-effect: non-scaling-stroke; transition: opacity .12s; }
.chart .endpoint, .chart .marker {
  position: absolute; border-radius: 50%; background: var(--series-1);
  transform: translate(-50%, -50%); pointer-events: none;
}
.chart .endpoint { width: 7px; height: 7px; }
.chart .marker {
  width: 11px; height: 11px; box-shadow: 0 0 0 3px #08080a, 0 0 14px rgba(255,255,255,.45);
  transition: opacity .12s;
}
/* value readout that follows the cursor */
.chart .tip {
  position: absolute; transform: translate(-50%, calc(-100% - 14px));
  background: rgba(20,20,22,.94); border: 1px solid rgba(255,255,255,.16);
  border-radius: 8px; padding: 5px 9px; pointer-events: none; white-space: nowrap;
  box-shadow: 0 6px 20px rgba(0,0,0,.6); transition: opacity .12s;
}
.chart .tip.below { transform: translate(-50%, 14px); }
.chart .tip b { display: block; font-size: .9em; font-weight: 800; line-height: 1.1; }
.chart .tip i { display: block; font-style: normal; font-size: .72em; color: var(--ink-dim); margin-top: 1px; }
.chart-foot { display: flex; justify-content: space-between; font-size: .7em; color: var(--ink-dim); margin-top: .3em; }
.chart { margin: 4% 0 2%; position: relative; }
/* BASELINE, NOT STRETCH.
 *
 * A label and a value on one line, except when the value does not fit on one
 * line: "ULTRA-PREMIUM COLLECTION" wraps to two, and with the default stretch
 * alignment the label sat against the top of a double-height row while the
 * value ran below it - the label looked lifted off its own row.  Baseline puts
 * "Variety" on the same line as the first line of its value, which is where a
 * reader expects it, and the wrapped remainder hangs underneath. */
.back-rows .row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 2.6% 0; border-top: 1px solid var(--hair); font-size: .82em;
}
/* The label keeps its line; the value takes the space left and wraps into it. */
.back-rows .row span { flex: 0 0 auto; }
.back-rows .row b { min-width: 0; text-align: right; }
.back-rows .row span { color: var(--ink-dim); }
.back-rows .row span i { font-style: normal; font-size: .82em; opacity: .6; }
.back-rows .row b { font-weight: 700; }
.psa-link {
  display: block; text-align: center; margin-top: 4%; padding: 3.4% 0;
  border: 1px solid var(--hair); border-radius: 8px; color: var(--ink);
  font-size: .8em; font-weight: 700; text-decoration: none; letter-spacing: .3px;
}
.psa-link:hover { background: rgba(255,255,255,.07); }
#player-meta {
  position: absolute; left: 40px; bottom: 40px; color: #fff; text-shadow: 0 2px 14px rgba(0,0,0,.8);
}
#player-meta { max-width: 46vw; }
#player-meta .n { font-size: 26px; font-weight: 800; letter-spacing: -.4px; }
#player-meta .n .x { font-size: 15px; font-weight: 700; opacity: .7; vertical-align: middle; }
#player-meta .d { font-size: 14px; opacity: .8; margin-top: 3px; }
/* bottom-right, clear of the card - centred controls sat on top of it */
#player-controls {
  position: absolute; bottom: 34px; right: 36px;
  display: flex; gap: 14px; align-items: center;
}
#player-controls button {
  width: 52px; height: 52px; border-radius: 50%; border: none; cursor: pointer;
  background: rgba(255,255,255,.12); color: #fff; font-size: 20px; backdrop-filter: blur(10px);
}
#player-controls button:hover { background: rgba(255,255,255,.25); }
#player-controls #player-pause { background: #fff; color: #000; font-size: 15px; }
#player-close {
  position: absolute; top: 26px; right: 30px; width: 44px; height: 44px; border-radius: 50%;
  border: none; background: rgba(255,255,255,.1); color: #fff; font-size: 17px; cursor: pointer; backdrop-filter: blur(8px);
}
#player-progress { position: absolute; top: 0; left: 0; right: 0; height: 3px; background: rgba(255,255,255,.12); }
#player-progress-fill { height: 100%; width: 0%; background: #fff; transition: width .25s linear; }

/* ---------- mosaic ---------- */
#mosaic {
  position: absolute; inset: 0; overflow: hidden; display: none;
  gap: 14px; align-items: flex-start; justify-content: center;
}
#player.mosaic-on #mosaic { display: flex; }
#mosaic .mcol { flex: 0 0 auto; display: flex; flex-direction: column; gap: 14px; }
#player.mosaic-on #player-card-wrap,
#player.mosaic-on #player-meta,
#player.mosaic-on #player-hint { display: none; }
#player.mosaic-on #player-bg,
#player.mosaic-on #player-bg2 { filter: brightness(.28) saturate(.7) blur(26px); transform: scale(1.15); }
#mosaic .cell {
  cursor: pointer; transition: transform .25s ease, opacity .4s ease, filter .25s ease;
  filter: drop-shadow(0 12px 26px rgba(0,0,0,.6));
}
#mosaic .shelf-fade {
  position: absolute; left: 0; right: 0; height: 90px; pointer-events: none; z-index: 2;
}
#mosaic .cell:hover { transform: translateY(-5px) scale(1.02); filter: drop-shadow(0 18px 36px rgba(0,0,0,.75)); }
#mosaic .cell.swapping { opacity: 0; transform: scale(.94) rotate(-1.2deg); }
#mosaic .cell .slab {
  --slab-a: rgba(255,255,255,.26); --slab-b: rgba(255,255,255,.11); --slab-c: rgba(255,255,255,.07);
  --slab-edge: rgba(255,255,255,.28);
}
#player-controls #player-mode { font-size: 18px; }

/* import modal */
#import-modal { position: fixed; inset: 0; background: rgba(0,0,0,.65); z-index: 90; display: flex; align-items: center; justify-content: center; }

/* ---------- the PSA guide ----------
 * Stacked ON the import dialog (z 90), because it is a detour from it rather
 * than a replacement for it. */
#psa-guide-modal {
  position: fixed; inset: 0; background: rgba(0,0,0,.72); z-index: 130;
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
#psa-guide-modal.hidden { display: none; }
.guide-card { max-width: 560px; }
.guide-head { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
.guide-head h2 { margin: 0; }
.psa-logo.big { height: 26px; }
.guide-steps { list-style: none; counter-reset: gs; margin: 18px 0 4px; padding: 0; }
.guide-steps li {
  counter-increment: gs; position: relative;
  padding: 0 0 18px 42px; border-left: 1px solid var(--line); margin-left: 13px;
}
.guide-steps li:last-child { border-left-color: transparent; padding-bottom: 4px; }
/* The number sits ON the rule, which is what makes four steps read as one
   sequence rather than as four unrelated boxes. */
.guide-steps li::before {
  content: counter(gs); position: absolute; left: -14px; top: -2px;
  width: 27px; height: 27px; display: grid; place-items: center;
  border-radius: 50%; background: var(--bg-raised); border: 1px solid var(--line);
  font-size: 12px; font-weight: 800; color: var(--fg);
}
.gs-t { font-size: 14px; font-weight: 700; letter-spacing: -.01em; }
.gs-b { font-size: 12.5px; color: var(--fg-dim); line-height: 1.55; margin-top: 3px; }
.gs-go { display: inline-flex; align-items: center; width: auto; margin-top: 10px; padding: 9px 15px; }
.fine { font-size: 11.5px; color: var(--fg-faint); line-height: 1.6; margin-top: 12px; }
/* The same white PSA button as the front page, at dialog scale. */
.psa-connect.small {
  font-size: 14px; padding: 10px 18px; border-radius: 10px; gap: 9px;
  box-shadow: 0 8px 24px rgba(0,0,0,.35);
}
.psa-connect.small .psa-logo { height: 17px; }
.modal-card {
  width: 460px; background: var(--bg-raised); border: 1px solid var(--line);
  border-radius: 14px; padding: 28px;
}
.modal-card h2 { font-size: 20px; font-weight: 800; margin-bottom: 6px; }
.muted { color: var(--fg-dim); font-size: 13px; margin-bottom: 16px; }
#import-input {
  width: 100%; height: 130px; background: var(--bg); color: var(--fg);
  border: 1px solid var(--line); border-radius: 8px; padding: 12px; font-family: monospace; font-size: 13px; resize: none; outline: none;
}
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 16px; }
.ghost-btn {
  background: none; border: 1px solid var(--line); color: var(--fg-dim);
  padding: 9px 18px; border-radius: 999px; font-weight: 700; cursor: pointer; font-family: inherit; font-size: 13px;
}
.solid-btn {
  background: var(--accent); color: var(--accent-fg); border: none;
  padding: 9px 22px; border-radius: 999px; font-weight: 800; cursor: pointer; font-family: inherit; font-size: 13px;
}
/* ---------- scrollbars (theme-aware, both engines) ---------- */
* { scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) transparent; }
::-webkit-scrollbar { height: 10px; width: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb); border-radius: 99px;
  border: 3px solid var(--bg); background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); background-clip: padding-box; }
.row::-webkit-scrollbar { height: 12px; }
.row { padding-bottom: 4px; }

/* ---------- PSA cert label (CSS-rendered, authentic red/white) ---------- */
.psa-label {
  background: #d22630; border-radius: 3px; padding: 3px;
  box-shadow: 0 2px 8px rgba(0,0,0,.35);
  container-type: inline-size;
}
.psa-label-in {
  background: #fbfbf7; border-radius: 1.5px; color: #111;
  display: flex; justify-content: space-between; gap: 4%;
  padding: 3.4% 5.5%; font-family: 'Inter', sans-serif;
}
.psa-l { min-width: 0; display: flex; flex-direction: column; }
.psa-l .l1 { font-size: 5.2cqw; font-weight: 700; letter-spacing: .2px; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.psa-l .l2 { font-size: 6cqw; font-weight: 800; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.psa-l .l3 { font-size: 4.6cqw; font-weight: 600; color: #333; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.psa-barcode {
  margin-top: auto; height: 4.4cqw; width: 46%;   /* just tall enough to read as a barcode */
  background: repeating-linear-gradient(90deg, #111 0 2px, transparent 2px 4px, #111 4px 5px, transparent 5px 8px, #111 8px 11px, transparent 11px 13px);
}
.psa-r { text-align: right; flex-shrink: 0; display: flex; flex-direction: column; align-items: flex-end; }
.psa-r .num { font-size: 5cqw; font-weight: 700; line-height: 1.15; }
.psa-r .gw { font-size: 5.4cqw; font-weight: 800; line-height: 1.15; }
.psa-r .g { font-size: 7.6cqw; font-weight: 900; line-height: 1.02; }
.psa-r .cert {
  font-size: 4.2cqw; font-weight: 600; color: #333; line-height: 1.2;
  display: flex; align-items: center; gap: .5em;
}
/* duplicate count, next to the cert number where a second slab would be */
.psa-r .cert .copies {
  background: #111; color: #fff; font-weight: 800;
  font-size: .92em; padding: .1em .45em; border-radius: 3px; letter-spacing: .2px;
}
.psa-label.img { padding: 0; background: none; box-shadow: 0 2px 8px rgba(0,0,0,.35); }
.psa-label.img img { display: block; width: 100%; border-radius: 3px; }

/* ---------- full slab (case + label + card window) ---------- */
/* the case is acrylic: let the artwork behind it come through */
.slab {
  background: linear-gradient(178deg, var(--slab-a), var(--slab-b) 34%, var(--slab-c));
  border: 1px solid var(--slab-edge);
  -webkit-backdrop-filter: blur(2px) saturate(1.06);
  backdrop-filter: blur(2px) saturate(1.06);
  border-radius: 10px; padding: 6.5% 7% 8.5%;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.26), inset 0 -2px 8px rgba(0,0,0,.22);
  position: relative;
}

/* ---------- already in your vault ----------
   Marked in the tile's line by ownMark(), not on the slab: see slabHTML().
   The two colours below are the whole vocabulary -
     --own-exact  same card AND same grade
     --own-other  same card, a different grade                                */
.own-key { display: inline-flex; align-items: center; gap: 6px; margin-right: 14px; }
.own-key i {
  width: 10px; height: 10px; border-radius: 3px; display: inline-block;
  background: transparent; box-shadow: 0 0 0 2px currentColor;
}
.own-key.slab-key { color: var(--own-exact); }
.own-key.card-key { color: var(--own-other); }

/* The safe, in the tile line.  Baseline-aligned with the text beside it so it
   reads as part of the sentence rather than as a badge stuck on the front. */
.own-mark { display: inline-flex; margin-right: 5px; vertical-align: -2px; line-height: 0; }
.own-mark.exact { color: var(--own-exact); }
.own-mark.other { color: var(--own-other); }
/* over artwork the case always reads as clear acrylic, whatever the theme */
.arrival .slab, #player-slab .slab {
  --slab-a: rgba(255,255,255,.26); --slab-b: rgba(255,255,255,.11); --slab-c: rgba(255,255,255,.07);
  --slab-edge: rgba(255,255,255,.28);
}
.slab::before {
  content: ''; position: absolute; top: 2.5%; left: 42%; right: 42%; height: 1.4%;
  background: rgba(255,255,255,.24); border-radius: 99px;
}
.slab .psa-label { margin-bottom: 5.5%; }
.slab .window {
  /* The cut-out is a rectangular crop of a round-cornered card, so its
     corners are the slab's black well showing through. Clip to the card's
     TRUE printed corner radius (~3mm = 4.7% of width = 3.36% of height at
     0.715) and nothing more - a zoom that also hid the edge fringe shaved
     real card off the top and bottom. Only the corners get cut. */
  aspect-ratio: 0.715; border-radius: 4.7% / 3.36%; overflow: hidden;
  background-size: cover; background-position: center;
  box-shadow: 0 3px 12px rgba(0,0,0,.45);
}
.slab .window.ph {
  background: linear-gradient(165deg, #232326, #121214);
  border: 1px solid #3d3d42;
  display: flex; flex-direction: column; justify-content: space-between; padding: 8%;
}
.slab .window.ph .ph-set { color: #9a9aa2; }
.slab .window.ph .ph-name { color: #ececf0; }
.slab .window.ph .ph-grade { border-color: #77777e; color: #ececf0; }
/* duplicate count */


/* arrival tile: one slab, floating over the extended art */
.arrival .float-slab {
  position: absolute; left: 15%; right: 15%; top: 4%;
  filter: drop-shadow(0 16px 30px rgba(0,0,0,.62));
}

/* player label */
#player-card-wrap { flex-direction: column; }

/* ---------- sign-in gate ---------- */
#login {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  background:
    radial-gradient(1200px 700px at 50% -10%, rgba(255,255,255,.07), transparent 60%),
    var(--bg);
}
#login.hidden { display: none; }
.login-card {
  width: min(380px, calc(100vw - 48px));
  padding: 40px 32px 32px;
  border: 1px solid var(--line); border-radius: 20px;
  background: var(--bg-raised);
  box-shadow: 0 30px 80px var(--shadow);
  text-align: center;
}
.login-brand { font-size: 30px; font-weight: 900; letter-spacing: -.02em; }
.login-sub { margin: 6px 0 26px; color: var(--fg-dim); font-size: 13px; }
.sso-btn {
  width: 100%; margin-bottom: 10px; padding: 12px 16px;
  display: flex; align-items: center; justify-content: center; gap: 10px;
  font: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--bg); color: var(--fg);
  transition: background .15s, transform .1s;
}
.sso-btn:hover { background: var(--bg-hover); }
.sso-btn:active { transform: translateY(1px); }
.sso-btn.apple { background: var(--fg); color: var(--bg); border-color: var(--fg); }
.login-err { min-height: 16px; margin-top: 12px; font-size: 12px; color: #e5484d; }

/* ---------- account + PSA quota ---------- */
/* Top right, and only a face.  It is the settings button as well as the "which
   account is this" answer, which is all that row in the sidebar ever did. */
.acct-btn {
  flex: none; margin-left: 14px; width: 30px; height: 30px; padding: 0;
  border-radius: 50%; border: 1px solid var(--line); background: var(--bg-raised);
  color: var(--fg-dim); font-family: inherit; font-size: 12px; font-weight: 700;
  cursor: pointer; overflow: hidden; display: grid; place-items: center;
  transition: .15s;
}
.acct-btn:hover { border-color: var(--fg-faint); color: var(--fg); }
.acct-btn.hidden { display: none; }
.acct-btn #acct-photo { width: 100%; height: 100%; border: 0; border-radius: 50%; display: block; }
.acct-row { display: flex; align-items: center; gap: 9px; margin-bottom: 12px; }
#acct-photo { width: 26px; height: 26px; border-radius: 50%; flex: none; border: 1px solid var(--line); }
#acct-photo[hidden] { display: none; }
.acct-id { flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; }
#acct-name {
  font-size: 12px; font-weight: 600; color: var(--fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.role-badge {
  flex: none; padding: 2px 6px; border-radius: 5px;
  font-size: 9px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  background: var(--accent); color: var(--accent-fg);
}
.role-badge.hidden { display: none; }
#acct-out { width: auto; padding: 4px 7px; font-size: 12px; line-height: 1; flex: none; }

.quota { margin-bottom: 12px; }
.quota-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 10px; font-weight: 600; color: var(--fg-dim);
  letter-spacing: .04em; text-transform: uppercase; margin-bottom: 5px;
}
#quota-num { font-variant-numeric: tabular-nums; color: var(--fg); }
.quota-bar {
  height: 4px; border-radius: 3px; overflow: hidden;
  background: var(--bg-hover); border: 1px solid var(--line);
}
#quota-fill {
  height: 100%; width: 0;
  background: var(--fg);
  transition: width .5s ease;
}
#quota-fill.warn { background: #f5a524; }
#quota-fill.full { background: #e5484d; }
.quota-reset { margin-top: 4px; font-size: 10px; color: var(--fg-faint); }

/* ---------- import ---------- */
.modal-actions .spacer { flex: 1; }
.file-btn { cursor: pointer; width: auto; padding: 9px 14px; }
#drop-zone { position: relative; border-radius: 12px; }
#drop-zone.over { outline: 2px dashed var(--fg-faint); outline-offset: 3px; }
#drop-hint {
  position: absolute; inset: 0; display: none; align-items: center; justify-content: center;
  border-radius: 12px; background: var(--bg-raised); color: var(--fg-dim);
  font-size: 13px; pointer-events: none;
}
#drop-zone.over #drop-hint { display: flex; }
.import-summary { min-height: 18px; margin: 8px 2px 0; font-size: 12px; color: var(--fg-dim); }
#key-input {
  width: 100%; margin-top: 4px; padding: 11px 12px;
  border: 1px solid var(--line); border-radius: 10px;
  background: var(--bg); color: var(--fg); font: inherit; font-size: 13px;
}
.key-state { min-height: 18px; margin: 10px 2px 0; font-size: 12px; color: var(--fg-dim); }

/* ---------- first import: the app waits ---------- */
#first-run {
  position: fixed; inset: 0; z-index: 320;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(1000px 600px at 50% -10%, rgba(255,255,255,.06), transparent 60%), var(--bg);
}
#first-run.hidden { display: none; }
.first-run-card { width: min(420px, calc(100vw - 48px)); text-align: center; }
.first-run-card h3 { margin: 22px 0 6px; font-size: 19px; font-weight: 700; }
.first-run-card .muted { font-size: 13px; }
.fr-bar {
  height: 6px; margin: 26px 0 10px; border-radius: 4px; overflow: hidden;
  background: var(--bg-hover); border: 1px solid var(--line);
}
#fr-fill { height: 100%; width: 0; background: var(--fg); transition: width .6s ease; }
.fr-count { font-size: 12px; color: var(--fg-dim); font-variant-numeric: tabular-nums; }
.fr-foot { margin-top: 26px; font-size: 11px; color: var(--fg-faint); }

/* The build, shown INSIDE the vault page rather than across the whole window.
   An import is building your cards; it is not a reason to take away Discover,
   your wishlists or settings, so the panel is the size of what it is waiting
   for and the rest of the app carries on around it. */
.vault-lock { max-width: 520px; padding: 6vh 0 0; }
.vault-lock .fr-bar { margin: 30px 0 10px; }
.fr-fill-lock { height: 100%; width: 0; background: var(--fg); transition: width .6s ease; }
.lock-aside {
  margin-top: 28px; padding-top: 16px; border-top: 1px solid var(--line);
  font-size: 12px; color: var(--fg-faint);
}

/* ---------- sold ---------- */
.tile.sold .slab, .lib-item.sold .swatch { opacity: .55; filter: saturate(.5); }
.sold-tag {
  display: inline-block; margin-left: 6px; padding: 1px 5px; border-radius: 4px;
  font-size: 9px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase;
  background: var(--bg-hover); color: var(--fg-dim); border: 1px solid var(--line);
  vertical-align: 1px;
}

/* ---------- cards waiting on the crawler ---------- */
#pending-note {
  display: flex; align-items: center; gap: 8px; margin-bottom: 12px;
  padding: 8px 10px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--bg-hover);
  font-size: 11px; color: var(--fg-dim);
}
#pending-note.hidden { display: none; }
#pending-text { flex: 1; }
#pending-retry { width: auto; padding: 4px 9px; font-size: 11px; }

/* ---------- settings ---------- */
.set-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
.set-head h2 { margin: 0; }
.set-close { width: auto; padding: 4px 9px; line-height: 1; }
.set-account {
  display: flex; align-items: center; gap: 11px;
  padding: 12px; margin-bottom: 6px;
  border: 1px solid var(--line); border-radius: 11px; background: var(--bg);
}
#set-photo { width: 38px; height: 38px; border-radius: 50%; border: 1px solid var(--line); }
.set-account > div { flex: 1; min-width: 0; }
#set-name { font-size: 13px; font-weight: 700; }
#set-email { font-size: 11px; }
.set-block { padding: 18px 2px; border-bottom: 1px solid var(--line); }
.set-block.last { border-bottom: none; padding-bottom: 4px; }
.set-block h3 { font-size: 12px; font-weight: 700; letter-spacing: .03em; text-transform: uppercase; margin-bottom: 5px; }
.set-block .muted { font-size: 12px; line-height: 1.5; }
.set-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.set-row .ghost-btn { width: auto; padding: 7px 12px; font-size: 12px; }
.set-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
.set-actions .ghost-btn, .set-actions .solid-btn { width: auto; padding: 8px 14px; font-size: 12px; }
.ghost-btn.danger { color: #e5484d; border-color: rgba(229,72,77,.35); }
.ghost-btn.danger:hover { background: rgba(229,72,77,.08); }

/* account row is now the way into settings */
.acct-row {
  width: 100%; display: flex; align-items: center; gap: 9px; margin-bottom: 12px;
  padding: 7px 8px; border-radius: 9px; cursor: pointer;
  background: none; border: 1px solid transparent; color: inherit; font: inherit; text-align: left;
  transition: background .15s, border-color .15s;
}
.acct-row:hover { background: var(--bg-hover); border-color: var(--line); }
.acct-gear { color: var(--fg-faint); flex: none; }
.acct-row:hover .acct-gear { color: var(--fg-dim); }
/* #theme-toggle carried width:100% from when it lived in the sidebar; inside a
   settings row it has to size to its label. */
.set-row #theme-toggle { width: auto; padding: 7px 12px; font-size: 12px; }
#dev-token {
  width: 100%; margin-top: 8px; padding: 9px 10px;
  border: 1px solid var(--line); border-radius: 9px;
  background: var(--bg); color: var(--fg);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;
}

/* ---------- card back: real figures, honest gaps ---------- */
.chart.empty .chart-blank {
  height: 62px; display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--line); border-radius: 9px;
  font-size: 11px; color: var(--fg-faint); text-align: center; padding: 0 14px;
}
.back-rows .row i, .chart .ct i { font-style: normal; color: var(--fg-faint); font-weight: 500; }
.back-sales { margin-top: 14px; }
.bs-head {
  font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--fg-dim); margin-bottom: 6px;
}
.bs-row {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: 4px 0; font-size: 11px; color: var(--fg-dim);
  border-bottom: 1px solid var(--line);
}
.bs-row:last-child { border-bottom: none; }
.bs-row b { color: var(--fg); font-variant-numeric: tabular-nums; }

/* collection hero without the cover: the grid below already shows the art */
.col-hero.bare { grid-template-columns: 1fr; }
.col-hero.bare .hero-actions { margin-top: 16px; }

/* sidebar import slot: an action, or a status - never an idle meter */
#side-import-live.hidden { display: none; }
.side-foot.hidden { display: none; }
#side-import-live.hidden, #side-import-btn.hidden { display: none; }
#sil-fill { height: 100%; width: 0; background: var(--fg); transition: width .5s ease; }
#side-import-live .quota-bar { height: 4px; border-radius: 3px; background: var(--bg-hover); border: 1px solid var(--line); overflow: hidden; }
.set-block.admin-only.hidden { display: none; }
.set-block h3 .role-badge { margin-left: 6px; vertical-align: 1px; }

/* PriceCharting line with PSA's estimate marked on the same axis */
.chart .ct .src {
  margin-left: 6px; padding: 1px 5px; border-radius: 4px;
  font-size: 9px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  background: var(--bg-hover); color: var(--fg-dim); border: 1px solid var(--line);
}
.psa-dot {
  position: absolute; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px;
  border-radius: 50%; background: var(--bg); border: 2px solid var(--fg);
  pointer-events: none;
}

/* ---------- the collector's own record, on the back of the slab ----------
   Sized in em like everything else on the back: the card scales with the
   viewport, so a fixed px input would drift out of proportion at other sizes. */
.back-mine { margin-top: 4%; padding-top: 3%; border-top: 1px solid var(--hair); }
.mine-head {
  display: flex; align-items: center; justify-content: space-between;
  font-size: .72em; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-dim); margin-bottom: 1.5%;
}
.ghost-btn.tiny, .solid-btn.tiny { padding: .35em .9em; font-size: .82em; border-radius: 999px; }
.mine-head .ghost-btn.tiny { color: var(--ink-dim); border-color: var(--hair); letter-spacing: 0; text-transform: none; }
.mine-head .ghost-btn.tiny:hover { color: var(--ink); }
/* Italic text needs room on the right: the slant carries the last glyph past
   its own advance width, and inside a tight cell the 'd' of "not recorded" was
   being cut in half. */
.back-mine .unset {
  color: var(--ink-dim); opacity: .65; font-weight: 500; font-style: italic;
  display: inline-block; padding-right: .18em;
}
/* THE CERT IS THE LINK.  A separate "View on PSA" button repeated a number
   already on the row; the number itself goes to PSA now. */
.cert-link {
  color: var(--ink); text-decoration: none;
  border-bottom: 1px dashed rgba(255,255,255,.28);
}
.cert-link:hover { border-bottom-color: rgba(255,255,255,.7); }
.back-rows .row b i.up { color: #1f9d63; font-style: normal; font-size: .85em; }
.back-rows .row b i.down { color: #d8564f; font-style: normal; font-size: .85em; }

.tag-row { display: flex; flex-wrap: wrap; gap: .4em; margin-top: 3%; font-size: .78em; }
.tag {
  border: 1px solid var(--hair); background: none; color: var(--ink);
  padding: .25em .7em; border-radius: 999px; cursor: pointer; font-family: inherit;
  font-weight: 600; letter-spacing: .01em;
}
.tag::before { content: '#'; opacity: .45; margin-right: .15em; }
.tag:hover { border-color: var(--ink-dim); }

.back-mine .fld { display: flex; align-items: center; gap: .6em; margin-top: 2.2%; font-size: .82em; }
.back-mine .fld span { color: var(--ink-dim); min-width: 5.2em; }
.back-mine .fld input {
  flex: 1; min-width: 0; font-family: inherit; font-size: 1em; font-weight: 600;
  color: var(--ink); background: none; border: 1px solid var(--hair);
  border-radius: 6px; padding: .45em .6em;
}
.back-mine .fld input:focus { outline: none; border-color: var(--ink-dim); }
.fld-note { font-size: .68em; color: var(--ink-dim); margin-top: 1.5%; }
.fld-err { font-size: .72em; color: #d8564f; margin-top: 1%; min-height: 1em; }
.mine-actions { display: flex; justify-content: flex-end; gap: .5em; margin-top: 3%; }


/* ---------- settings as a full page ----------
   A dialog was fine for a key and a theme toggle.  The admin panes below are
   lists that page, and a paging list inside a 460px popup is a scroll trap. */
#settings {
  position: fixed; inset: 0; z-index: 60; display: flex;
  background: var(--bg); color: var(--fg);
}
#settings.hidden { display: none; }
.set-nav {
  width: 244px; flex: none; padding: 22px 16px;
  border-right: 1px solid var(--line); background: var(--bg);
  display: flex; flex-direction: column; gap: 4px;
}
.set-brand { font-size: 19px; font-weight: 900; letter-spacing: -.02em; margin-bottom: 14px; padding: 0 8px; }
.set-back {
  background: none; border: none; color: var(--fg-dim); font: inherit; font-size: 12px;
  text-align: left; padding: 8px; cursor: pointer; border-radius: 8px; margin-bottom: 10px;
}
.set-back:hover { background: var(--bg-hover); color: var(--fg); }
.set-nav-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  background: none; border: none; color: var(--fg-dim); font: inherit; font-size: 13px; font-weight: 600;
  text-align: left; padding: 9px 10px; border-radius: 9px; cursor: pointer;
}
.set-nav-item:hover { background: var(--bg-hover); color: var(--fg); }
.set-nav-item.active { background: var(--bg-raised); color: var(--fg); }
.set-nav-item.hidden { display: none; }
.nav-badge {
  margin-left: auto; min-width: 20px; text-align: center;
  background: var(--bg-hover); border: 1px solid var(--line); border-radius: 999px;
  font-size: 10px; font-weight: 800; padding: 1px 6px; color: var(--fg-dim);
}
.nav-badge.hidden { display: none; }

.set-main { flex: 1; overflow-y: auto; padding: 34px 40px 80px; }
.set-pane { max-width: 860px; }
.set-pane.hidden { display: none; }
.set-pane h1 { font-size: 26px; font-weight: 800; letter-spacing: -.4px; margin-bottom: 6px; }
.set-pane > .muted { font-size: 13px; line-height: 1.55; margin-bottom: 18px; max-width: 68ch; }
.set-pane h1 .role-badge { vertical-align: middle; margin-left: 6px; }
.set-actions.left { justify-content: flex-start; }
.empty-note {
  border: 1px dashed var(--line); border-radius: 11px; padding: 22px;
  color: var(--fg-dim); font-size: 13px; text-align: center;
}

/* ---------- loading skeletons ----------
 *
 * Drawn at the real sizes so nothing jumps when the cards land: the tile is a
 * .tile with its 0.715 face, the shelf head sits where a shelf head sits.  One
 * sweep animation on a shared gradient, so a page of them moves as one surface
 * rather than as forty independent flickers. */
.skel-line, .skel-face {
  display: block;
  background: linear-gradient(100deg, var(--skel-a) 20%, var(--skel-b) 40%, var(--skel-a) 60%);
  background-size: 240% 100%;
  animation: skel-sweep 1.5s linear infinite;
  border-radius: 5px;
}
@keyframes skel-sweep { from { background-position: 150% 0; } to { background-position: -50% 0; } }

.skel-shelf .shelf-head { display: flex; align-items: center; justify-content: space-between; }
.skel-line.h { height: 15px; width: 190px; border-radius: 6px; }
.skel-line.a { height: 10px; width: 64px; }
.skel-line.meta { height: 11px; width: 220px; max-width: 60%; margin-top: 4px; }
.skel-tile {
  border-radius: 10px; padding: 12px;
  background: var(--bg-raised); border: 1px solid var(--line);
}
.skel-tile .skel-face { aspect-ratio: .715; border-radius: 6px; margin-bottom: 10px; }
.skel-tile .skel-line.n { height: 10px; width: 76%; margin-bottom: 7px; }
.skel-tile .skel-line.s { height: 8px; width: 46%; }
/* The rows do not scroll while they are pretending - there is nothing to
   scroll to, and a scrollbar under a skeleton invites a drag that does
   nothing. */
.skel-shelf .row { overflow-x: hidden; }
.skel-grid { pointer-events: none; }
.skel-actions .skel-line.btn { height: 41px; border-radius: 999px; }

/* the sidebar's binder rail, same shape as .lib-item */
.skel-item { display: flex; align-items: center; gap: 10px; padding: 7px 8px; cursor: default; }
.skel-item .skel-face.sw { width: 34px; height: 34px; border-radius: 8px; flex: none; margin: 0; }
.skel-item .skel-col { flex: 1; min-width: 0; }
.skel-item .skel-line.n { height: 9px; width: 66%; margin-bottom: 6px; }
.skel-item .skel-line.s { height: 7px; width: 38%; }

/* A shimmer is decoration; for anyone who has asked motion to stop it is just
   a flat grey block, which says the same thing. */
@media (prefers-reduced-motion: reduce) {
  .skel-line, .skel-face { animation: none; background: var(--skel-a); }
}

/* ---------- admin: PriceCharting fixes ---------- */
.fix-list { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.fix {
  display: flex; gap: 14px; padding: 14px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--bg-raised);
}
.fix.done { opacity: .45; }
.fix-thumb {
  width: 54px; height: 76px; flex: none; border-radius: 6px;
  border: 1px solid var(--line); background: var(--bg-hover) center/cover;
}
.fix-body { flex: 1; min-width: 0; }
.fix-name { font-size: 14px; font-weight: 700; }
.fix-set { font-size: 11.5px; margin-top: 1px; }
.fix-why { font-size: 12px; color: #d8564f; margin-top: 6px; }
.fix-tried { font-size: 11.5px; margin-top: 3px; word-break: break-all; }
.fix-tried a { color: var(--fg-dim); text-decoration: underline; }
.fix-edit { display: flex; gap: 8px; margin-top: 9px; }
.fix-edit input {
  flex: 1; min-width: 0; font: inherit; font-size: 12px;
  background: var(--bg); border: 1px solid var(--line); color: var(--fg);
  border-radius: 8px; padding: 8px 10px;
}
.fix-edit input:focus { outline: none; border-color: var(--fg-faint); }
.fix-msg { font-size: 11.5px; margin-top: 6px; min-height: 1em; color: var(--fg-dim); }
.fix-msg.ok { color: #1f9d63; }
.fix-msg.err { color: #d8564f; }

/* ---------- admin: AI image review ---------- */
.art-bar { display: flex; align-items: center; gap: 12px; margin: 4px 0 10px; flex-wrap: wrap; }
.art-search {
  flex: 1; min-width: 200px; max-width: 340px;
  background: var(--bg-raised); color: var(--fg);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 6px 11px; font-size: 12.5px; outline: none;
}
.art-search:focus { border-color: var(--fg-faint); }
.art-search::placeholder { color: var(--fg-faint); }
.art-filters { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
/* The FILTERS pick what you are looking at; the ACTIONS spend money.  Dressed
   identically and run together they read as one bag of nine pills, which is
   how "generate all" ended up sitting between two views. */
.art-filters .art-sep {
  width: 1px; align-self: stretch; margin: 2px 6px;
  background: var(--line);
}
.art-filters .ghost-btn.act { color: var(--fg-dim); border-style: dashed; }
.art-filters .ghost-btn.act:hover { color: var(--fg); border-style: solid; }
.art-filters .ghost-btn.active { color: var(--fg); border-color: var(--fg-faint); }
.art-bar .solid-btn { margin-left: auto; }
#art-counts { font-size: 12px; margin-bottom: 12px; }
.art-list { display: flex; flex-direction: column; gap: 10px; }

/* stuck-before-cut-out block */
.stuck-block { margin: 10px 0 14px; border: 1px solid #3d3d42; border-radius: 10px; padding: 10px 14px; }
.stuck-block summary { cursor: pointer; color: #ececf0; font-size: 14px; }
.stuck-list { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.stuck-row { padding: 8px 10px; border-radius: 8px; background: rgba(255,255,255,.03); border-left: 3px solid #55555c; }
.stuck-row.ok { border-left-color: #d9a441; }     /* eligible-yet-here = suspicious, gold */
.stuck-row.err { border-left-color: #d22630; }
.stuck-row.warn { border-left-color: #8a8af0; }
.stuck-head { display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; font-size: 13px; }
.stuck-head a { color: #9ab0ff; text-decoration: none; font-family: ui-monospace, monospace; }
.stuck-tag { margin-left: auto; font-size: 11px; color: #9a9aa2; border: 1px solid #3d3d42;
  border-radius: 99px; padding: 1px 8px; white-space: nowrap; }
.stuck-why { font-size: 12px; margin-top: 3px; }

/* The set's own logo on its album page - print artwork, so it gets a shadow
   to sit on the dark background rather than dissolving into it. */
.album-logo { height: 58px; display: block; margin: 4px 0 10px; filter: drop-shadow(0 4px 14px rgba(0,0,0,.65)); }
.stuck-actions { display: flex; gap: 8px; margin-top: 8px; }
.ghost-btn.danger { color: #d22630; border-color: rgba(210,38,48,.4); }
.menu-danger { color: #d22630; }
.art-item {
  /* Four thumbnails, the facts, and the presses.  A single flex row could not
     hold them: the meta column was crushed to one word per line and the
     buttons walked off the right edge.  Grid gives the pictures their natural
     width, the facts what is left, and drops the actions onto their own line
     when the row cannot hold everything. */
  display: grid;
  grid-template-columns: auto minmax(260px, 1fr) auto;
  align-items: center; gap: 14px; padding: 12px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--bg-raised);
  transition: opacity .15s;
}
.art-shots { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
@media (max-width: 1180px) {
  .art-item { grid-template-columns: auto 1fr; }
  .art-item .art-actions { grid-column: 1 / -1; justify-content: flex-end; }
}
@media (max-width: 720px) {
  .art-item { grid-template-columns: 1fr; }
}
.art-img {
  width: 148px; height: 84px; flex: none; border-radius: 8px;
  border: 1px solid var(--line); background: var(--bg-hover) center/cover;
}
/* The card, beside what was grown from it. Deliberately first: the question
   being asked of this row is "does that background belong to this card", and
   you cannot ask it without the card. */
.art-orig {
  width: 60px; height: 84px; flex: none; border-radius: 5px; object-fit: cover;
  border: 1px solid var(--line); background: var(--bg-hover);
}
.art-orig.none { background: var(--bg); border-style: dashed; }
/* A row in the queue has no background to show, and the empty frame says so
   more plainly than a missing element would: the card is here, the picture is
   the thing that has not been bought yet. */
.art-img.none { background: var(--bg); border-style: dashed; }
/* The failure reason, on the row that reports the failure. The raw text from
   the crawler is kept under the plain-English reading of it - the sentence is
   for deciding what to do, the code is for when the sentence is wrong. */
.art-why { margin-top: 6px; font-size: 11px; line-height: 1.45; }
.art-why code {
  display: block; margin-top: 3px; font-size: 10px; opacity: .75;
  word-break: break-word;
}
.art-why i { font-style: normal; opacity: .6; }
.art-meta { flex: 1; min-width: 0; }
.art-type {
  display: flex; align-items: center; gap: 8px; margin-top: 7px;
  font-size: 11px; color: var(--fg-faint); flex-wrap: wrap;
  /* The label and the provenance note are three-word phrases; letting them
     wrap mid-phrase turned the column into a stack of single words. */
  white-space: nowrap;
}
.art-type i { white-space: nowrap; }
.art-type select {
  padding: 4px 8px; border-radius: 7px; border: 1px solid var(--line);
  background: var(--bg); color: var(--fg); font: inherit; font-size: 11.5px; cursor: pointer;
}
.art-type i { font-style: normal; opacity: .75; }
.art-name { font-size: 14px; font-weight: 700; line-height: 1.3; }
.art-meta .muted { font-size: 11.5px; margin-top: 2px; }
.art-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; align-items: center; }
/* The two shape-presses belong together and to the redraw beside them. */
.art-actions .aspect { padding-left: 8px; padding-right: 8px; opacity: .9; }
.art-flag { font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; padding: 2px 7px; border-radius: 999px; margin-left: 6px; }
.art-flag.regen { background: rgba(216,86,79,.14); color: #d8564f; }
.art-flag.ok { background: rgba(31,157,99,.14); color: #1f9d63; }
.art-more { display: flex; justify-content: center; padding: 18px 0 4px; }

/* ---------- arranging a collection ---------- */
.ghost-big.on { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
.arrange-note {
  margin: 0 0 16px; padding: 9px 13px; border-radius: 9px;
  border: 1px dashed var(--line); background: var(--bg-raised);
  font-size: 12px; color: var(--fg-dim);
}
.arrange-note.hidden { display: none; }
.grid.arranging .tile { cursor: grab; }
.grid.arranging .tile:active { cursor: grabbing; }
.grid.arranging .tile .slab { pointer-events: none; }   /* the tile owns the drag, not its contents */
.grid.arranging .tile.dragging { opacity: .35; }
.grid.arranging .tile { outline: 1px dashed transparent; outline-offset: 4px; transition: outline-color .15s; }
.grid.arranging .tile:hover { outline-color: var(--fg-faint); }

/* ---------- Settings → Portfolio ---------- */
.pstats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  gap: 10px; margin: 14px 0 6px;
}
.pstat { padding: 15px 16px; border: 1px solid var(--line); border-radius: 12px; background: var(--bg-raised); }
.pstat-v { font-size: 24px; font-weight: 800; letter-spacing: -.6px; line-height: 1.1; }
.pstat-v.up { color: #1f9d63; }
.pstat-v.down { color: #d8564f; }
[data-theme="dark"] .pstat-v.up { color: #35c489; }
[data-theme="dark"] .pstat-v.down { color: #ef7b74; }
.pstat-k { font-size: 11px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--fg-dim); margin-top: 6px; }
.pstat-s { font-size: 11.5px; color: var(--fg-faint); margin-top: 3px; }
.set-pane .muted.small { font-size: 11.5px; line-height: 1.55; margin-top: 10px; max-width: 74ch; }

.bars { display: flex; flex-direction: column; gap: 7px; margin-top: 12px; }
.barrow { display: flex; align-items: center; gap: 12px; font-size: 12px; }
.barrow-k { width: 240px; flex: none; color: var(--fg-dim); }
.barrow-t { flex: 1; height: 7px; border-radius: 999px; background: var(--bg-hover); overflow: hidden; }
.barrow-f { height: 100%; background: var(--fg-faint); border-radius: 999px; }
.barrow-n { width: 78px; flex: none; text-align: right; font-weight: 700; }
@media (max-width: 720px) { .barrow-k { width: 150px; } }

.tag-row.wide { font-size: 12px; gap: .5em; }
.tag-row.wide .tag { border-color: var(--line); color: var(--fg); }
.tag-n { opacity: .5; margin-left: .35em; font-weight: 700; }

/* ---------- card back: latest PSA sales (below the fold) ---------- */
.bs-head { display: flex; align-items: baseline; justify-content: space-between; gap: .6em; }
.bs-med { text-transform: none; letter-spacing: 0; opacity: .7; font-weight: 600; white-space: nowrap; }
/* The heading is the way through to PriceCharting, so it has to look like one
   without shouting - it is still a section label, just a live one. */
.bs-title { color: inherit; text-decoration: none; cursor: pointer; }
a.bs-title:hover { color: var(--ink, #f4f4f5); text-decoration: underline; text-underline-offset: 2px; }
.bs-empty { font-size: .74em; color: var(--ink-dim); line-height: 1.5; }
/* The last element needs clearance for the fade drawn over the bottom of the face. */
.back-in > :last-child { margin-bottom: 8%; }

/* ---------- the purchase, on the chart ----------
   A dashed break-even rule at what was paid, a faint tick on the date it was
   bought, and a marker where they meet.  Coloured by outcome: the rule is the
   line the curve has to be above. */
.chart .buyline {
  stroke: var(--buy, #8b8b96); stroke-width: .5; stroke-dasharray: 2.4 2;
  vector-effect: non-scaling-stroke; opacity: .75;
}
.chart .buytick {
  stroke: var(--buy, #8b8b96); stroke-width: .5; stroke-dasharray: 1.2 2.4;
  vector-effect: non-scaling-stroke; opacity: .4;
}
.buy-dot {
  position: absolute; width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px;
  border-radius: 2px; transform: rotate(45deg);        /* a diamond, so it never reads as a reading */
  background: #08080a; border: 2px solid var(--buy, #8b8b96);
  pointer-events: none;
}
.buy-dot.up { border-color: #35c489; }
.buy-dot.down { border-color: #ef7b74; }
.buy-note {
  display: flex; align-items: baseline; justify-content: space-between; gap: .6em;
  margin-top: .5em; font-size: .72em; color: var(--ink-dim);
}
.buy-key { display: inline-flex; align-items: center; gap: .45em; }
.buy-key::before {
  content: ''; width: 7px; height: 7px; border-radius: 1px; transform: rotate(45deg);
  border: 2px solid currentColor; opacity: .8;
}
.chart.empty .buy-note { justify-content: center; margin-top: .8em; }

/* ---------- date picker on the card back ----------
   Inline, not floating: the back scrolls and clips, so an absolutely
   positioned popover would be cut off exactly when the field is near the
   bottom - which is where it lives. */
.dp-wrap { flex: 1; display: flex; gap: .4em; min-width: 0; }
.dp-wrap input { flex: 1; min-width: 0; }
.dp-btn {
  flex: none; width: 2.2em; font: inherit; font-size: 1em; line-height: 1;
  color: var(--ink-dim); background: none; border: 1px solid var(--hair);
  border-radius: 6px; cursor: pointer;
}
.dp-btn:hover { color: var(--ink); border-color: var(--ink-dim); }
.dp {
  margin-top: 2.5%; padding: 3%; border: 1px solid var(--hair);
  border-radius: 9px; background: rgba(255,255,255,.03); font-size: .78em;
}
.dp.hidden { display: none; }
.dp-head { display: flex; align-items: center; justify-content: space-between; font-weight: 700; margin-bottom: .5em; }
.dp-nav {
  background: none; border: none; color: var(--ink-dim); font: inherit; font-size: 1.4em;
  line-height: 1; padding: 0 .5em; cursor: pointer; border-radius: 5px;
}
.dp-nav:hover { color: var(--ink); }
.dp-dow, .dp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.dp-dow { color: var(--ink-dim); font-size: .82em; text-align: center; margin-bottom: 2px; }
.dp-day {
  aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
  background: none; border: 1px solid transparent; border-radius: 6px;
  color: var(--ink); font: inherit; font-size: .95em; cursor: pointer;
}
.dp-day:hover { border-color: var(--ink-dim); }
.dp-day.dp-today { border-color: var(--hair); font-weight: 700; }
.dp-day.dp-on { background: #f4f4f5; color: #08080a; font-weight: 800; border-color: #f4f4f5; }
.dp-day.dp-off { color: var(--ink-dim); opacity: .3; cursor: default; }
.dp-day.dp-off:hover { border-color: transparent; }
.dp-pad { aspect-ratio: 1; }
.dp-foot { display: flex; justify-content: flex-end; gap: .5em; margin-top: .6em; }
.dp-quick {
  background: none; border: 1px solid var(--hair); border-radius: 999px;
  color: var(--ink-dim); font: inherit; font-size: .92em; padding: .25em .8em; cursor: pointer;
}
.dp-quick:hover { color: var(--ink); border-color: var(--ink-dim); }

/* ---------- fullscreen ----------
   #player is already fixed/inset:0, so the fullscreen box needs no new layout -
   only a guarantee that the backdrop covers the whole screen, since the element
   is now the viewport rather than sitting inside one. */
#player:fullscreen, #player:-webkit-full-screen { width: 100vw; height: 100vh; background: #000; }
#player.is-full #player-close { top: 22px; right: 22px; }
#player-full { font-size: 15px; }

/* ---------- Discover ---------- */
.login-or {
  display: flex; align-items: center; gap: 12px; margin: 18px 0 14px;
  color: var(--fg-faint); font-size: 11px; text-transform: uppercase; letter-spacing: .12em;
}
.login-or::before, .login-or::after { content: ''; flex: 1; height: 1px; background: var(--line); }
.ghost-btn.wide { width: 100%; }
.login-foot { margin-top: 10px; font-size: 11.5px; color: var(--fg-faint); text-align: center; }

/* A guest has no vault, so the parts of the sidebar that describe one are not
   hidden individually - the whole region goes. */
/* A guest keeps the SHELVES - the sidebar shows Discover's binders and the
   wishlist door, because an empty rail sells nothing - and loses only what
   describes a vault they do not have. */
body.guest #side-import,
body.guest #pending-note,
body.guest .nav-item[data-view="home"], body.guest .nav-row { display: none; }
/* Search STAYS for guests - Discover is a shop window, and a shop window you
   cannot search is a wall.  The scope is already right: searchScope() answers
   "discover" for a guest.  What a guest cannot do is create binders, so the
   New button goes (the JS hides it too; this survives any render order). */
body.guest #binders-head .new-col-btn { display: none; }

/* The guest topbar's one call to action, where a member's vault totals sit. */
#stats .acct-cta {
  background: var(--brand-gradient); color: #fff; border: 0; cursor: pointer;
  font-weight: 700; font-size: 13px; padding: 8px 16px; border-radius: 99px;
  box-shadow: 0 4px 14px rgba(122,44,255,.35);
}
#stats .acct-cta:hover { filter: brightness(1.08); }

/* ---------- the paywall, dressed like the phone's ----------
   The Connect-my-Vault overlay is the iOS Collection screen: a hero card's
   AI scene edge to edge behind the pitch, the SLABS COLLECTOR eyebrow, the
   two plans with Best value marked in the brand gradient, and one rainbow
   Continue button.  Same argument, same clothes, whichever door you came
   through. */
.modal-card.paywall { position: relative; overflow: hidden; padding: 0; width: min(560px, 94vw); }
.paywall .pw-bg {
  position: absolute; inset: 0; background-size: cover; background-position: center 30%;
}
.paywall .pw-bg::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(8,8,12,.45), rgba(8,8,12,.88) 62%, rgba(8,8,12,.97));
}
.paywall .pw-in { position: relative; padding: 42px 34px 26px; text-align: center; }
.paywall .pw-eyebrow { font-size: 10px; font-weight: 900; letter-spacing: 1.6px; color: rgba(255,255,255,.55); }
.paywall .pw-title { font-size: 26px; font-weight: 900; letter-spacing: -.01em; margin: 8px 0 6px; color: #fff; }
.paywall .pw-sub { font-size: 13.5px; color: rgba(255,255,255,.75); max-width: 400px; margin: 0 auto; }
.paywall .pw-chips { display: flex; gap: 8px; justify-content: center; margin: 14px 0 20px; }
.paywall .pw-chips span {
  font-size: 11.5px; font-weight: 600; color: rgba(255,255,255,.78);
  padding: 6px 10px; border-radius: 99px; background: rgba(255,255,255,.09);
}
.paywall .pw-plans { display: flex; gap: 12px; margin-bottom: 14px; }
.paywall .pw-plan {
  position: relative; flex: 1; cursor: pointer; text-align: left;
  background: rgba(255,255,255,.06); color: #fff;
  border: 1.5px solid rgba(255,255,255,.16); border-radius: 14px; padding: 14px 14px 12px;
}
.paywall .pw-plan.sel { border-color: transparent; background:
  linear-gradient(rgba(24,22,32,.92), rgba(24,22,32,.92)) padding-box,
  var(--brand-gradient) border-box; }
.paywall .pw-plan .pw-name { display: block; font-size: 12.5px; font-weight: 700; color: rgba(255,255,255,.7); }
.paywall .pw-plan .pw-price { display: block; font-size: 21px; font-weight: 900; margin: 2px 0; }
.paywall .pw-plan .pw-price i { font-style: normal; font-size: 12px; font-weight: 600; color: rgba(255,255,255,.6); }
.paywall .pw-plan .plan-note { font-size: 11.5px; color: rgba(255,255,255,.6); }
.paywall .pw-plan .pw-best {
  position: absolute; top: -9px; right: 10px; font-size: 10px; font-weight: 800; letter-spacing: .4px;
  color: #fff; background: var(--brand-gradient); padding: 3px 9px; border-radius: 99px;
}
.paywall .ai-cta {
  width: 100%; border: 0; cursor: pointer; border-radius: 99px; padding: 14px;
  background: var(--brand-gradient); color: #fff; font-size: 15px; font-weight: 800;
  box-shadow: 0 8px 24px rgba(122,44,255,.4);
}
.paywall .ai-cta:hover { filter: brightness(1.07); }
.paywall .ai-cta[disabled] { opacity: .7; cursor: default; }
.paywall .pw-foot { margin-top: 12px; font-size: 11.5px; color: rgba(255,255,255,.55); }

/* "Have a code?" - a text link that unfolds into one small row. */
.paywall .pw-code { margin-top: 10px; text-align: center; }
.paywall .pw-code-toggle {
  background: none; border: 0; padding: 4px 6px; cursor: pointer;
  font: inherit; font-size: 12px; color: rgba(255,255,255,.6); text-decoration: underline dotted;
}
.paywall .pw-code-toggle:hover { color: #fff; }
.paywall .pw-code-row { display: flex; gap: 8px; justify-content: center; margin-top: 4px; }
.paywall .pw-code-row input {
  flex: 0 1 220px; min-width: 0; padding: 9px 12px; border-radius: 10px;
  border: 1px solid rgba(255,255,255,.16); background: rgba(255,255,255,.06); color: #fff;
  font: inherit; font-size: 13px; letter-spacing: .08em; text-transform: uppercase; outline: none;
}
.paywall .pw-code-row input:focus { border-color: rgba(255,255,255,.4); }
.paywall .pw-code-row button {
  padding: 9px 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,.18);
  background: rgba(255,255,255,.1); color: #fff; font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
}
.paywall .pw-code-row button:hover { background: rgba(255,255,255,.16); }
.paywall .pw-code-row button[disabled] { opacity: .6; cursor: default; }
.paywall .pw-code-msg { font-size: 11.5px; margin-top: 6px; min-height: 1em; color: rgba(255,255,255,.6); }
.paywall .pw-code-msg.ok { color: #58d68d; }
.paywall .pw-code-msg.err { color: #ff7b72; }
.paywall .pw-close {
  position: absolute; top: 12px; right: 12px; z-index: 2; cursor: pointer;
  width: 32px; height: 32px; border-radius: 50%; border: 0;
  background: rgba(255,255,255,.12); color: rgba(255,255,255,.85); font-size: 14px; font-weight: 700;
}
.paywall #promo-msg { margin-top: 8px; }

/* The sidebar edition of the connect button: same promise, rail-sized. */
.psa-connect.side {
  width: 100%; justify-content: center;
  font-size: 14px; padding: 11px 14px; border-radius: 11px; gap: 9px;
  margin: 2px 0 10px; box-shadow: 0 8px 26px rgba(0,0,0,.35);
}

.back-note {
  margin-top: 4%; padding: 2.6% 0; text-align: center;
  border: 1px dashed var(--hair); border-radius: 8px;
  color: var(--ink-dim); font-size: .74em; letter-spacing: .02em;
}

/* ---------- admin: the Discover table ---------- */
#disc-input {
  width: 100%; min-height: 90px; resize: vertical; margin-top: 8px;
  background: var(--bg); border: 1px solid var(--line); border-radius: 10px;
  color: var(--fg); font: inherit; font-size: 12.5px; padding: 10px 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
#disc-input:focus { outline: none; border-color: var(--fg-faint); }
.dtable { width: 100%; border-collapse: collapse; font-size: 12.5px; margin-top: 10px; }
.dtable th {
  text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--fg-faint); font-weight: 700; padding: 0 10px 8px 0;
}
.dtable td { padding: 9px 10px 9px 0; border-top: 1px solid var(--line); vertical-align: middle; }
.dtable td .muted { font-size: 11px; }
.dtable .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; }
.dthumb {
  width: 34px; height: 47px; border-radius: 4px;
  border: 1px solid var(--line); background: var(--bg-hover) center/cover;
}
.dtable .ghost-btn.tiny { padding: 5px 11px; font-size: 11px; }

/* Per-card pipeline state in the Discover table. */
.dtable .ok { color: #35c489; font-weight: 800; }
.dtable .no { color: var(--fg-faint); }
.dtable tr.stuck td { background: rgba(216,86,79,.06); }
.dtable td.err { color: #d8564f; }

/* ---------- boot splash ----------
   Shown from the first paint until auth.js has decided between the sign-in
   screen and the app.  An indeterminate bar rather than a spinner: it is
   honest about not knowing how long Firebase will take. */
#boot {
  position: fixed; inset: 0; z-index: 200;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 22px;
  background: var(--bg);
}
#boot.hidden { display: none; }
#boot .login-brand { font-size: 2.1rem; letter-spacing: -.02em; }
.boot-bar {
  width: 168px; height: 3px; border-radius: 3px;
  background: var(--line); overflow: hidden;
}
.boot-bar i {
  display: block; width: 40%; height: 100%; border-radius: 3px;
  background: var(--fg); opacity: .75;
  animation: boot-slide 1.15s ease-in-out infinite;
}
@keyframes boot-slide {
  0%   { transform: translateX(-110%); }
  100% { transform: translateX(360%); }
}
@media (prefers-reduced-motion: reduce) {
  .boot-bar i { animation: none; width: 100%; opacity: .35; }
}

/* ---------- collections ----------
 *
 * The new-collection modal is wider than the import one because it has to hold
 * a grid of your cards; everything else here is the chrome around that grid. */
/* ABOVE THE PLAYER (z-index 100), not under it.  "New binder…" is reachable
   from the card menu inside the player, and at 95 the dialog opened behind
   the black - the click worked, the answer was invisible. */
#col-modal, #promo-modal, #signup-modal { position: fixed; inset: 0; background: rgba(0,0,0,.65); z-index: 120; display: flex; align-items: center; justify-content: center; }
#col-modal.hidden, #promo-modal.hidden, #signup-modal.hidden, #card-menu.hidden { display: none; }
.modal-card.wide { width: min(760px, 92vw); max-height: 86vh; display: flex; flex-direction: column; }
/* :not(.hidden) is load-bearing, not decoration.  `.modal-card.wide > #id` is
   more specific than `#id.hidden`, so without it this rule wins over the hiding
   rule below and the second step is permanently on screen underneath the first. */
.modal-card.wide > #col-step-detail:not(.hidden) { flex: 1; min-height: 0; display: flex; flex-direction: column; }

/* Kind first. Four tiles, because the answer to "what kind of shelf" should be
   readable in one glance rather than hidden behind a dropdown. */
.kind-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.kind-tile {
  text-align: left; background: var(--bg); border: 1px solid var(--line);
  border-radius: 12px; padding: 16px; cursor: pointer; color: var(--fg);
  font-family: inherit; transition: border-color .12s, transform .12s;
}
.kind-tile:hover { border-color: var(--fg-dim); transform: translateY(-1px); }
.kind-ico { font-size: 20px; opacity: .8; margin-bottom: 8px; }
.kind-name { font-weight: 800; font-size: 14px; margin-bottom: 4px; }
.kind-desc { font-size: 12px; color: var(--fg-dim); line-height: 1.45; }

/* A facet is a candidate shelf the vault already implies - the count is the
   whole argument for it, so it sits on the row rather than under it. */
.facet-list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; max-height: 52vh; }
.facet-list.hidden, #col-picker.hidden, #col-name-row.hidden, #col-step-kind.hidden, #col-step-detail.hidden { display: none; }
.facet {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: var(--bg); border: 1px solid var(--line); border-radius: 10px;
  padding: 11px 14px; cursor: pointer; color: var(--fg); font-family: inherit; font-size: 13px;
}
.facet:hover { border-color: var(--fg-dim); }
.facet-name { font-weight: 700; }
.facet-n { color: var(--fg-dim); font-size: 12px; font-variant-numeric: tabular-nums; }

#col-name-row input, #col-search {
  width: 100%; background: var(--bg); color: var(--fg); border: 1px solid var(--line);
  border-radius: 8px; padding: 10px 12px; font-family: inherit; font-size: 13px; outline: none;
}
#col-name-row { margin-bottom: 12px; }
#col-picker { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 10px; }
.picker-grid {
  flex: 1; min-height: 0; overflow-y: auto;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 10px;
  padding: 2px;
}
.pick {
  background: none; border: 1px solid transparent; border-radius: 10px; padding: 4px;
  cursor: pointer; color: var(--fg); font-family: inherit; text-align: center;
}
.pick-img {
  aspect-ratio: 4 / 5; border-radius: 7px; background: var(--bg) center/cover no-repeat;
  border: 1px solid var(--line); margin-bottom: 5px;
}
.pick-t { font-size: 11px; font-weight: 700; line-height: 1.25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-s { font-size: 10px; }
.pick.on { border-color: var(--accent); }
.pick.on .pick-img { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The kind label lives on the library row, so it has to read as an aside to the
   card count rather than compete with the name above it. */
.col-kind {
  margin-left: 8px; padding: 1px 6px; border-radius: 999px;
  border: 1px solid var(--line); font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
}
.col-kind.ai { border-color: var(--accent); color: var(--accent); }

/* ---------- card context menu ---------- */
#card-menu {
  position: fixed; z-index: 150; min-width: 190px; max-width: 260px;
  background: var(--bg-raised); border: 1px solid var(--line); border-radius: 10px;
  padding: 5px; box-shadow: 0 12px 34px rgba(0,0,0,.42);
}
#card-menu button {
  display: block; width: 100%; text-align: left; background: none; border: none;
  color: var(--fg); font-family: inherit; font-size: 13px; padding: 7px 10px;
  border-radius: 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
#card-menu button:hover { background: var(--bg); }
/* A row for a list the card is already on: the word says what the press does,
   and it only turns red when you are actually about to press it. */
#card-menu button.in-list { display: flex; align-items: baseline; gap: 8px; }
#card-menu button.in-list .menu-hint {
  margin-left: auto; font-size: 10px; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; color: var(--fg-faint); flex: none;
}
#card-menu button.in-list:hover .menu-hint { color: #d8564f; }
.menu-head {
  font-size: 11px; font-weight: 800; color: var(--fg-dim); text-transform: uppercase;
  letter-spacing: .04em; padding: 6px 10px 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.menu-empty { font-size: 12px; color: var(--fg-dim); padding: 4px 10px 8px; }
.menu-sep { height: 1px; background: var(--line); margin: 5px 0; }

/* Making a collection is the main thing this sidebar is for, so the control
   says what it does instead of being a lone glyph you have to hover to learn. */
.new-col-btn {
  display: inline-flex; align-items: center; gap: 5px;
  background: none; border: 1px solid var(--line); color: var(--fg-dim);
  padding: 4px 11px 4px 9px; border-radius: 999px; cursor: pointer;
  font-family: inherit; font-size: 11px; font-weight: 800;
  text-transform: uppercase; letter-spacing: .06em;
  transition: color .12s, border-color .12s, background .12s;
}
.new-col-btn .plus { font-size: 14px; font-weight: 700; line-height: 1; letter-spacing: 0; }
.new-col-btn:hover { color: var(--fg); border-color: var(--fg-dim); background: var(--bg-raised); }

/* ---------- theme collections ---------- */
.facet.theme { align-items: flex-start; padding: 12px 14px; }
.facet.theme > span:first-child { display: flex; flex-direction: column; gap: 3px; text-align: left; }
.facet-about { font-size: 12px; color: var(--fg-dim); line-height: 1.45; font-weight: 400; }
.facet-foot { padding: 4px 0 2px; text-align: center; }
/* The wait is stated. Reading a whole vault takes seconds, and a blank panel
   for that long reads as a broken dialog rather than a working one. */
.empty-note.thinking { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 26px 0; }
.empty-note.thinking .boot-bar { width: 140px; }
.theme-note {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  margin: 0 0 18px; font-size: 13px; color: var(--fg-dim); line-height: 1.5;
}
.theme-stale { font-size: 12px; opacity: .8; font-style: italic; }

/* ---------- a run, in the activity pane ---------- */
.run { border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; margin-bottom: 10px; }
.run-head { display: flex; justify-content: space-between; gap: 12px; font-size: 13px; font-weight: 700; margin-bottom: 9px; }
.run-head .muted { font-weight: 400; }
.run-items { display: flex; flex-wrap: wrap; gap: 6px; }
.run-chip {
  font-size: 11px; padding: 3px 9px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--fg-dim); white-space: nowrap;
}
/* Status is carried by the border, not by a coloured blob: the chips sit in a
   dense row and a filled background at this size reads as noise. */
.run-chip.ok { color: var(--fg); border-color: var(--own-exact); }
.run-chip.failed { border-color: #d8564f; color: #d8564f; }
.run-chip.skipped { border-style: dashed; }
.run-chip b { font-weight: 800; font-variant-numeric: tabular-nums; }
.run-errs { margin-top: 9px; font-size: 12px; display: flex; flex-direction: column; gap: 3px; }

/* ---------- buy links on the back of a card ----------
 *
 * eBay is the real mark, inlined as vector paths (EBAY_LOGO in app.js) rather
 * than hotlinked: a third party in the render path of a panel that must appear
 * instantly is a dead logo the day that host reorganises. Same reasoning for
 * PriceCharting below, inlined as a small transparent PNG. */
.buy-links { display: flex; gap: 8px; margin: 14px 0 4px; }
.buy-btn {
  flex: 1; display: flex; align-items: center; justify-content: center;
  height: 38px; border-radius: 8px; text-decoration: none;
  background: #fff; border: 1px solid rgba(0,0,0,.14);
  transition: transform .12s, box-shadow .12s;
}
.buy-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 14px rgba(0,0,0,.28); }

/* The real mark, sized by height so the four glyphs keep their proportions.
   Height not width: the eBay logo is much wider than PriceCharting's, and
   matching widths would make one of them tiny. */
.buy-btn.ebay svg { height: 20px; width: auto; display: block; }

/* PriceCharting's real mark, keyed off its white background and inlined as a
   64-colour PNG - 3 KB, which is less than the HTTP request to fetch it would
   cost, and it cannot rot when a CDN reorganises. Transparent rather than
   flattened, so it composites on whatever the button background becomes. */
.wm-pc {
  width: 79px; height: 20px; display: block;
  background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAL4AAAAwCAMAAACYEDAHAAAAwFBMVEWPs1pdp9nk8uotkNum0OPb7OCYyeTJ2a22+fzK2K9msrJ2ufxznTW0ypO0tLSkwnX6+re19bK3uXW5uv26zZxyuHJ++f2Du90rdLnb6t9/f399fTDb6dR//38Af384vLwA//92sc3//38AAP8Mb997uj1/f/+pvoMADH///wC02egAdgBxcbiqvoXD0qoA/wBusNe6zqD/f38Aqv+JvtaCu+EAAAACddT+/v58pUF4ojsAbdD+/v2yyI76/foWg9nmGkoqAAAAQHRSTlP992v+npfU3wOnAwP82AP8AwQEA6EDAuUDEgID0AICAwFoAgEDBALsAgFzAwOZeQGedAIDfr0A/gf+/Pwt9k3+n7WncQAACxFJREFUeNqdWYdiGzkO5RRqNOolKrYVJxen7u7t3u3dUdA0/f9f3QNITpFGih2kWBpyyEfwAXiklblrZEgPguh8joJBSEO67jEycdC2HfoZM+Smj2YQdNviivBUbGgq+/A3M/Zj4a0C052PbFFwCI15sS2PdDGUG1D9DH0c5bkMdxwUpgf+3vzr6DqwoXMUYAHc8w8TXzQdg0Fll4ZlVxEe5OcYQ1hbGwqDqOl/DOLCyEgYL2iNVPcI1E/QD6LmNUy97YN/vhw2sivdmzi6nPAYhIApng6l8ayxEjdb0Z7N9taCn+Efe+wOfKIPGK/tvuOAvl75/9kMrr1id6oHPnusEocyfB4b3h8731fXEPMophd6M3wafsb/F+PlUe2ptg08N8Tqlc6aleXHdltQvFADP3Lch+89P3LHNI//OzXw8455+LQe74frLVnogrGo4uDKc2EP/Qcu0qxFfqW0HZv43G5yMQl/b6+8/1ijz88c/zsfBIx/XcOP+kKXXjyU7yMbR8C+C8759c73hO/AtVjzaw6EPG7RJE2V5VKOpr8v4BNSi58C6YnwQhhY4uYSLQ6+n8UaGcXexr8wjsOCrPcryZV5H9lACqJ++P5xJV7khe59U+UpubP+DM2sIY/AH7lvAKj9QDaQOf/QvoZ/Mbl4Hy5Dag8iJOYYXse+9WO3rpjd8D7KAmzs3AiMn2v4JDY0WtyPEBp2vU8fPbd51b436SiXetNwP6C2bY2iz0i24mqBfD7fhO5JsaUb3t/aBOISYtyCbxlHVDWIu/BdlmDvDOsi9p3iQBcug3v4H7YfRh/E9iOQ56vRwX3AXcN0o3vwh2TZE13DH25tUwe+kOcPF+THuCnBrhYYyTs1/IudV/Q29LK9dA/+S4/37V77jQF51h3vc3nM3UKHbfAvZkRd78f//Ye3GDVZ2XB6i11Gr+f+eDsej/d1Im1z3wdjiyFd+EUd73Tp/Q58kLs2LuCqv5zdsas5upmH4831GtuqC7g2z+mgNUCbPEOf9HfmhW6olx6Y0YNR1U+4M9hFF6X3UnkOXMKLxQaR36OPDv7x3KloLtO3vb91+3L8Hz2bt8HX0V1XDwoKO0IK3uoXDX5LG2Wz9wHZ0RO8T9SF33ifzOu9nzP8+HyXKJXwYddUggGGCjuzDI69Sot61dyVZIts6Hrud8Nq5pJ0Az9q7Mzcvwefg+yZ9SoOEXYBMnmSVO3aNehDL7KsD77IJjIXVbeJ99lF4I6oBT8PdNgy5P3BHfTIcKZKDoUoiQE0iGg2rU4JPd6Ez+cV7bZn0EhIfxKozAOZS8H81dXj7pEIxSx04G/m/eAO+gNeS5Qq04q3ADGA0wMd5llWtmdpuC97Gux+K3zGa4lRB1/7xN6Bv/bkjw7mPz6FPaAURxBwOHW2qu76YQ0bDvk/ugdfaJ6oLJuoMhEv8HaF88kpU7q1yS5x2syjIfygHKjTxGVr5+io3co65Nl+dEqJ283nNYQJjmTFDvt9ZtHztQV/OGpsqG7mzZw3UqvsdDrxAg629oTlBA9OZSt4B5dacEwXK6vMdydEm6rRFczDuj0SrkrCcGUCPf64TZ78TtLR8+zE+E9ZphK4flmUJ/mqCrO4gG+r7nBNdFXRKtaPXhEPICmvD4tDf7CEu2Muc9oXEDimRZ7wN92y8BZ8Qc9E8cY7oMOp+6JSs+nXPH1yCNkKpS6uC8fQXGUeFto+B56joBHtooN6EyfH2Q34XFs/VmV2ahl2oFT+c1lf2LwOPv/ZHVui+PKovoVsq49I+cX59MZRPb8Fn2srgSiZ/+t24OS+ZPOGPa+DL5o+75dsrTulnnuVr/cuSvrhI+kUyakBfmmZCt8KH/9CNxmntCv43EcH524FQZkY3dQ84v1e9EgPB3ULe4f8z2Z35jNaD3yIBtvk4I+gRrk8YG9pxFn9KJ/rEwrZY+vRySNJmex7Y5l3vrJjL3ybdO6hP50A/9HdLoVWal6f4bdN00d7G1PFzvi+q7AfH1oVcMyXnP7OlC9EfZEj0oO4x3rgO2VQE17sCv/06lKuXyy+zYjvafhaxfpi+LMh1a4PvalsdQJupdQcf9TpYgkN/O2H/Xi070cz5tq4br6vR6P9fvRsVf0zN44vKEfr57r6tcDTzIxb5Xa8xzAwFXcTldyPGUmZgr1M0jCsQq2TuZpkPeTxg7O8tdPNNk8r4frm6WlVd1l8sU9tfzQ9fdnUT1bmydpm896Y9/SwWS5s03uz+cKD02rztKxfXzzxS+jBgjmPgvZNgiQd+BrQddXMWOlS1TvAobtsC9tr5r/OVq+i1I2vxKct+DvcRXknZU7UPAkt9s1sMVsu2NWFLIADIZuUVXtQ0sm0nGqWM59MmL5LNX4+pOk7f2X2SGGaQrj6e4P0HVuqK/tdJ/Y7LCRsJQ/xYP6GU9IEj+Cpige1ry8xXTqdvsPLjyqMmC3kJEa+K4AenNEsHLGNzUrpPS9gqsTKEPh8A4V4KkwLF1Ziq4S+QHIoNbXvfyIqEUPaOnshTWJ4BMrRN1XbO7P6Yv6JD8jM79FywmCivjC8ZIeFqTwITaraxTJFgZKRc8ospqe53G3NLvlATyyaNfyQVuZHfaAzunR7Ah2NNJhYav0IVabmjvgGX07+mOC+TSbQ4nN2LSGsbLBlDH9j3qnMweeOKoWH4B+Bj40oXTZEF/UApKEcarAATjoHaEuOiz4u8w44NvgPS1OUE5WJOxK5jUS1BojfGaOy+Yl+YEtYaWgp1QIfAqr0S+KtYTXFHk25y1Tgrx4FPvBXsmCGD2pPeUVIhhPsCl/RVsk8kQVUrDLVNDQruv07l9lq08oh6ApXZfOkekix7KWDn7BEwIwOPhWciBnSp9r734iLC6sPQKoK/lxWfOlNHr7zPmdp8vBnRl5KwlBjuk/qcYb5Jid3HBmFJYJy8YY6A+djOAlR+TWCwM9KRDLL06njV8r74w+ZAl/BcYWHz+cgyw5rHfjCSg8fQhI5UQ5/BZiqHinlFbKaZ81RCn9fb0s/78Ns47JKIto6a4qbzJmpA8OglSfPN408gIWKmn5Y2mGWfI733H8U+Lzuyfxg4UMp8VEbenHh7vcLW2BlAWGCMFnQL8Anqn+FmXilIbvuMg3CgDsm7H5LHg4WLEjixQ9jQ5szT6b+dNzHunG6Tix8BC4/kRiT3678TokrRkypziH2NbZw8DnUf39fwxepgR8C/9FgcjgeE80rIHCZh6dlGpC5gs/e/3cNX8tiXeZhwmGUFSWlmlZKXvRSYKJCWrxNY/HhnQlc10NOMiopikq7zOP4ap2dIvi89zkXhbZ41xzs8z6i1Z/wuEbwfnB44/ha8T6UjRQ40IzeCJ+mGfBq6KLUzp4IcU2dOFfe2SDphB3sdkwj4TK76BL+Uyfv42BXJJm/3lhxFgDOqij5wkAhULw4zvxWvo091ZzzPrI4Xn8kB5+W5ODLAk/W+/aGyHnfpkAtcq8D33vAwVeFIwg3L35UU1dmLHz6walPmJMUb9fs2C49n7gyqKVsKdajK+/9mSQLBZWTCq0M0ofL+yUSlJ2zB37N/TnEP1c9G5efZDqRw0weGb88gb+l/qUTBwsmkXKSe2deNDD8yQQzbni7T4kIk3KCKQW+NGn8sBd2S//Ikeck6oHho0ch5w+3OvrLOOUISVwoexWafCtZGP2S6MVLVVry0aCiv7gsJmV5oI0J5Sd9okMJ6b1ZLjeU4lPIyTsppyla0KO08AmPytSKkpX5M8EbLLq4B/ZnafSUmyU9P5riINNBzv8fhhIWg9lmSNMAAAAASUVORK5CYII=") center/contain no-repeat;
}

/* A shelf row you can pick up. No arrange mode: unlike a card grid, these are
   not things you click past on the way somewhere else. */
/* A logo is a mark on a background, not a photograph to be cropped: contained,
   centred, with a little air, on the panel colour rather than under it. */
.swatch.logo {
  background-size: 76% auto !important;
  background-repeat: no-repeat !important;
  background-position: center !important;
  background-color: var(--bg-raised);
  border: 1px solid var(--line);
}

/* ---------- the heart on a card ----------
 *
 * Bottom-left, on glass, over the corner of the slab.  Quiet until the card is
 * hovered - a wall of hearts is decoration - but permanent once the card is on
 * a list, because "you already have this" is the whole point of showing it.
 * On a touch screen there is no hover, so it is always there. */
.tile-slab { position: relative; }
.wish-heart {
  /* BIG ENOUGH TO HIT.  At 32px with a 15px glyph this was a decoration you
     could see and not press - and pressing it is the whole point on the
     overview pages, where taking a card off a wishlist should not mean opening
     the card first. 40px is a real target, and the glyph grew with it. */
  position: absolute; left: 8px; bottom: 8px; z-index: 3;
  width: 40px; height: 40px; display: grid; place-items: center;
  border-radius: 999px; cursor: pointer; padding: 0;
  color: #fff; background: rgba(18,18,22,.5);
  /* NO KEYLINE.  A white ring around a small dark disc reads as a UI chrome
     element pasted onto the picture; the blur and the drop shadow are enough to
     lift it off the artwork, and the heart is then the only thing in it. */
  border: 0;
  backdrop-filter: blur(14px) saturate(1.25);
  -webkit-backdrop-filter: blur(14px) saturate(1.25);
  box-shadow: 0 3px 14px rgba(0,0,0,.45);
  opacity: 0; transform: translateY(4px) scale(.94);
  transition: opacity .15s, transform .15s, background .15s, border-color .15s, color .15s;
}
.wish-heart svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linejoin: round; }
.tile:hover .wish-heart, .arrival:hover .wish-heart,
.wish-heart:focus-visible { opacity: 1; transform: none; }
.wish-heart:hover { background: rgba(34,34,40,.72); }
/* SAVED: the app's own iridescence, not a flat accent.
   The gradient is the same four-layer drift the AI marks wear, masked to the
   heart - so "saved" reads as part of the same family as "made by the model"
   rather than as a fifth colour nobody has met before. */
.wish-ink {
  display: none; width: 21px; height: 21px;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
  font-size: 21px;                    /* the gradient blobs are sized in em */
}
.wish-heart.on { opacity: 1; transform: none; }
.wish-heart.on svg { display: none; }
.wish-heart.on .wish-ink { display: block; }
.wish-heart.on:hover { background: rgba(44,44,52,.75); }
.wish-heart:disabled { opacity: .5; cursor: default; }
@media (hover: none) { .wish-heart { opacity: 1; transform: none; } }
/* The arrival tile has no .tile-slab wrapper - it is its own frame. */
.arrival .wish-heart { left: 10px; bottom: 10px; }

/* The player's version says the word as well as showing the state - same
   iridescent heart, on the same glass as every other button down there. */
.glass-btn.wished { display: inline-flex; align-items: center; gap: 9px; color: #fff; }
.glass-btn.wished .wish-ink { display: block; width: 16px; height: 16px; font-size: 16px; }
.glass-btn .gb-sub {
  font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  opacity: .72; padding-left: 8px; margin-left: 2px;
  border-left: 1px solid rgba(255,255,255,.24);
}

.lib-item { cursor: grab; }
.lib-item.dragging { opacity: .4; cursor: grabbing; }
#lib-list.dragging-over { background: var(--bg-raised); border-radius: 8px; }
.ghost-big.danger { color: #d8564f; border-color: rgba(216,86,79,.45); }
.ghost-big.danger:hover { border-color: #d8564f; }

/* Keeping a proposed collection. The un-kept state invites, the kept state
   just confirms - so only one of them carries the accent. */
.ghost-big.keep { border-color: var(--accent); color: var(--accent); }
.ghost-big.on { color: var(--fg); }

/* Says what the button is FOR. The mark alone told you where you were going,
   not what you would be doing when you got there.

   A stray `h1` was left on the line above this rule when the old info-dot
   styles were deleted, which silently turned the selector into `h1 .buy-btn` -
   so the gap never applied, the label sat flush against the logo, and the pair
   wrapped onto two lines in a narrow panel.  That is what "the buttons look
   horrible" was. */
/* LABEL ABOVE MARK, not beside it.
 *
 * Side by side, "VALUE AT" and PriceCharting's wordmark are two pieces of
 * typography at different sizes fighting over one line, and in a narrow panel
 * they were pushed into each other. Stacked, the label is a quiet caption and
 * the mark is the thing you actually recognise - and neither has to shrink. */
.buy-btn {
  flex-direction: column;
  justify-content: center;
  gap: 3px;
  height: 46px;
  padding: 0 12px;
  min-width: 0;
}
.buy-at {
  font: 600 9px/1 Inter, system-ui, sans-serif;
  color: #6b6b6b;
  text-transform: uppercase;
  letter-spacing: .09em;
  white-space: nowrap;
  flex: none;
}
.buy-btn .wm-pc, .buy-btn.ebay svg { flex: none; }
.buy-btn.ebay svg { height: 17px; }
.wm-pc { width: 68px; height: 17px; }

/* Wishlists sit under the collections and borrow their row styling; the only
   difference worth drawing is that they are wants, not holdings. */
#wish-list { overflow-y: auto; max-height: 30vh; padding-bottom: 4px; }
#wish-head.hidden, #wish-list.hidden { display: none; }
/* A heading that FOLLOWS a scrolling list needs air above it, or it sits on
   the last row like a lid - which is what made the collection above it look
   sliced in half rather than scrolled past. */
#wish-head { padding-top: 16px; }

/* THE SAME RULE, FOR COLLECTORS - and it is not optional.
 *
 * There is no global `.hidden { display: none }` in this stylesheet; every
 * hidden element states its own rule. `.lib-head` sets `display: flex`, so
 * `class="lib-head hidden"` is flex AND hidden at equal specificity, and source
 * order wins - the heading stayed on screen with nothing under it. That is
 * exactly why the wishlist pair above exists; I copied the markup and not the
 * rule. */
#coll-head.hidden, #coll-list.hidden { display: none; }
#coll-list { padding-bottom: 8px; }
/* Which vault you are standing in, marked the same way the nav marks a page.
   Without this the row you had just clicked looked identical to the rest. */
.lib-item.active { background: var(--bg-hover); }
.lib-item.active .t { color: var(--fg); }
.shelf-head a { cursor: pointer; }

/* The eyebrow now carries the kind. "Collection" alone said nothing you could
   not see; "AI collection" is the one fact about this page worth stating, and
   it belongs where the label already was rather than in a badge below it. */
.col-hero .k.ai { color: var(--accent); }

/* A heading you can click through on. */
.shelf-head h2.linky { cursor: pointer; }
.shelf-head h2.linky:hover { text-decoration: underline; text-underline-offset: 4px; }
/* The arrivals treatment, wrapped instead of scrolled, for a full page of it. */
.row.arrivals.wrap { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 18px; overflow: visible; }
.row.arrivals.wrap .arrival { width: auto; }

/* The model's sentence about an AI shelf: a subtitle, not a footnote.  Sits
   between the title and the meta line, capped so a long one cannot push the
   cards off the screen. */
.col-hero .col-about {
  margin: 6px 0 2px;
  max-width: 62ch;
  font-size: 14px;
  line-height: 1.5;
  color: #b9b9b9;
}
.shelf-head h2.linky { cursor: pointer; }




/* Inviting someone. */
/* An address, then two decisions about it, then the button - down the page in
   the order you make them. Strung along one line they read as a sentence with
   checkboxes in it, and the button ended up a quarter-mile from the field. */
.invite-form { margin-top: 14px; max-width: 520px; }
.invite-form input[type="email"] {
  width: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  border-radius: 9px;
  border: 1px solid #2a2a2a;
  background: #151515;
  color: #eee;
  font: inherit;
  font-size: 13px;
}
.invite-form input[type="email"]:focus {
  outline: none;
  border-color: #4a4a4a;
}
.invite-opts { margin: 10px 0 14px; display: flex; flex-direction: column; gap: 9px; }
.invite-form .check {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  font-size: 13px;
  color: #cfcfcf;
  cursor: pointer;
  line-height: 1.4;
}
.invite-form .check input {
  width: auto;
  margin: 2px 0 0;
  accent-color: #eee;
  flex: none;
}
.invite-form .check span { flex: 1; }
.invite-form .check em {
  display: block;
  font-style: normal;
  font-size: 11.5px;
  color: #888;
}
.invite-form > button { padding: 9px 22px; }

/* One person per row, with their permissions under their address rather than
   run together with it. */
.invite-row {
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 12px 0;
  border-top: 1px solid #1e1e1e;
}
.invite-row .lib-text { flex: 1 1 240px; min-width: 0; }
.invite-row .lib-name { font-size: 13.5px; }
.invite-row .lib-count { font-size: 11.5px; color: #8a8a8a; }
.invite-row .ghost-btn { flex: none; white-space: nowrap; }

/* ---------- collectors ----------
   A person in the sidebar, above your own shelves: it answers a bigger
   question - whose cards - so it sits above the question of which shelf. */
.kol-face {
  flex: none;
  width: 26px; height: 26px;
  border-radius: 50%;
  object-fit: cover;
  background: #2a2a2a;
}
.kol-face.letter {
  display: grid;
  place-items: center;
  font-size: 12px;
  font-weight: 700;
  color: #111;
  background: linear-gradient(140deg, #d8c68a, #b9a05a);
}

/* Where a copied card came from.  Quiet on the tile, spelled out on the back:
   it is provenance, not a status you need to act on. */
.from-badge {
  margin-left: 6px;
  padding: 1px 5px;
  border-radius: 5px;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .02em;
  color: #cbb87d;
  background: rgba(203, 184, 125, .12);
  white-space: nowrap;
}

/* ---------- brand lockup images (2026-08 rebrand: icon + wordmark) ---------- */
.brand img          { height: 30px; display: block; }
.brand              { padding: 4px 10px 18px; }
.set-brand img      { height: 22px; display: block; }
.set-brand          { margin-bottom: 14px; padding: 0 8px; }
.login-brand-img    { height: 40px; display: block; margin: 0 auto 6px; }
#boot .login-brand-img { height: 46px; }

/* ---------- brand loader: the card stack riffles ---------- */
.slabs-loader        { width: 44px; height: 52px; position: relative; display: inline-block; }
.slabs-loader i      { position: absolute; inset: 0; border-radius: 7px;
                       background: var(--brand-gradient); }
.slabs-loader i:first-child { filter: brightness(.55) saturate(1.2);
                       animation: slabshuffle 1.15s ease-in-out infinite; }
.slabs-loader i:last-child  { animation: slabshuffle 1.15s ease-in-out infinite reverse; }
@keyframes slabshuffle {
  0%,100% { transform: rotate(-7deg) translateX(-5px); }
  50%     { transform: rotate(2deg)  translateX(2px); }
}
@media (prefers-reduced-motion: reduce) {
  .slabs-loader i { animation: none; }
}
.load-row { display: flex; justify-content: center; padding: 26px 0; }

/* ---------- the import banner (the lock screen's replacement) ---------- */
.import-banner {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 10px 22px 0; padding: 10px 16px;
  background: var(--bg-raised); border: 1px solid var(--line); border-radius: 12px;
  font-size: 13px;
}
.import-banner.hidden { display: none; }
.import-banner b { font-weight: 700; }
.import-banner .ib-dot {
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
  background: var(--brand-gradient);
  animation: ib-pulse 1.6s ease-in-out infinite;
}
@keyframes ib-pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
.import-banner .ib-count { color: var(--fg-dim); font-variant-numeric: tabular-nums; }
.import-banner .ib-bar {
  flex: 1 1 120px; height: 5px; border-radius: 99px; background: rgba(255,255,255,.09);
  overflow: hidden; min-width: 80px;
}
.import-banner .ib-bar i {
  display: block; height: 100%; border-radius: 99px;
  background: var(--brand-gradient); transition: width .6s ease;
}
.import-banner .ib-note { color: var(--fg-faint); font-size: 11.5px; }
@media (prefers-reduced-motion: reduce) { .import-banner .ib-dot { animation: none; } }

/* The composite thumbnails on the art review rows. */
.art-comp {
  width: 74px; height: 92px; flex-shrink: 0; border-radius: 8px;
  background-size: cover; background-position: center;
  border: 1px solid var(--line); display: block;
}
.art-comp.wide { width: 118px; height: 66px; align-self: center; }
.art-comp.none { background: repeating-linear-gradient(45deg, transparent, transparent 6px, rgba(255,255,255,.04) 6px, rgba(255,255,255,.04) 12px); }
.art-comp:hover { border-color: rgba(255,255,255,.35); }

/* The run bar: the presses that spend a call or a crawl. */
.art-runs { margin-top: 6px; align-items: center; }
.art-runs-label {
  font-size: 10px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: var(--fg-faint); margin-right: 2px;
}

/* ---------- card type as four presses, not a dropdown ----------
   The page's whole job is answering this question over and over; a <select>
   made every answer three actions (open, read, choose) and hid the current
   one until opened. */
.art-type-label { color: var(--fg-faint); }
.type-chips { display: inline-flex; gap: 4px; flex-wrap: wrap; }
.type-chip {
  font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer;
  padding: 4px 10px; border-radius: 99px; white-space: nowrap;
  color: var(--fg-dim); background: transparent;
  border: 1px solid var(--line); transition: .12s;
}
.type-chip:hover:not(:disabled) { color: var(--fg); border-color: var(--fg-faint); }
.type-chip.on { background: var(--fg); color: var(--bg); border-color: var(--fg); }
.type-chip:disabled { opacity: .5; cursor: default; }

/* Rarities reads grade by grade: the tabs above its grid. */
.grade-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 14px; }
.grade-tabs .type-chip i { font-style: normal; opacity: .55; margin-left: 3px; }

/* ---------- the action under the card ----------
 * The back of a slab is a document; buttons inside it competed with the thing
 * being read. One action sits below the card, on the glass, level with the
 * player's own controls. */
.under-card {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 78px; display: flex; gap: 10px; z-index: 6;
}
.under-card.hidden { display: none; }
.glass-btn {
  font: inherit; font-size: 13px; font-weight: 600; letter-spacing: .01em;
  color: #fff; cursor: pointer; white-space: nowrap;
  padding: 10px 20px; border-radius: 999px;
  background: rgba(255,255,255,.11);
  border: 1px solid rgba(255,255,255,.18);
  backdrop-filter: blur(14px) saturate(1.2);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  box-shadow: 0 6px 24px rgba(0,0,0,.35);
  transition: background .15s, border-color .15s, transform .15s;
}
.glass-btn:hover { background: rgba(255,255,255,.18); border-color: rgba(255,255,255,.34); transform: translateY(-1px); }
.glass-btn:active { transform: translateY(0); }
/* THE BUY LINK, as one line of type with a mark at the end of it.
 *
 * The first cut stacked a 8.5px "BUY AT" cap over a shrunken eBay logo inside
 * a pill built for a single line - two typographic systems arguing in a space
 * that fits neither, which is what "really ugly" was.  Now it reads like the
 * sentence it is: bag, "Buy on", mark, all on one baseline at the same height
 * as the button beside it.  The mark keeps its own colours (it is a logo, not
 * an icon) and only the words dim. */
.glass-btn.buy-glass {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 10px 18px 10px 16px; text-decoration: none; line-height: 1;
}
.buy-glass .bg-ic { flex: none; opacity: .72; margin-top: -1px; }
.buy-glass .bg-tx { font-weight: 600; letter-spacing: .01em; color: rgba(255,255,255,.9); }
/* Optically centred: eBay's wordmark has a descender in the "y", so a mark
   aligned on its box sits high against text aligned on its baseline. */
.buy-glass .bg-mk { display: block; margin-top: 1px; }
.buy-glass .bg-mk svg { height: 17px; width: auto; display: block; }
.glass-btn.buy-glass:hover .bg-ic { opacity: 1; }
/* On short windows the row tucks lower - but stays ON screen.  -48px put it
   BELOW the full-viewport wrap, which is why "no buttons on the back" only
   ever reproduced in windows under 780px tall. */
@media (max-height: 780px) { .under-card { bottom: 14px; } }

/* ---------- admin: users, and one user's vault ----------
 *
 * A table that reads like a list of people rather than a database dump, and
 * under it the same vault they see - at a size where a hundred cards fit on
 * one screen, because the question being asked is "did they all come out
 * right", not "what is this card worth".
 */
#users-q {
  flex: 1 1 260px; max-width: 340px; padding: 9px 14px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bg-raised); color: var(--fg); font: inherit; font-size: 13px;
}
.users-table { border: 1px solid var(--line); border-radius: 14px; overflow: hidden; }
.users-row {
  display: grid; grid-template-columns: minmax(0,2.2fr) .8fr .5fr .8fr .8fr auto;
  gap: 12px; align-items: center; padding: 11px 14px; font-size: 13px;
  border-top: 1px solid var(--line);
}
.users-row:first-child { border-top: 0; }
.users-row.head { background: var(--bg-raised); color: var(--fg-faint); font-size: 11.5px;
  text-transform: uppercase; letter-spacing: .06em; }
.users-row .num { font-variant-numeric: tabular-nums; }
.users-row .ok { color: var(--good, #1f9e63); font-weight: 600; }
.u-who { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.u-who i { font-style: normal; font-size: 12px; }
.u-who b, .u-who i { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.u-head { margin: 6px 0 16px; }
.u-head h3 { margin: 0 0 3px; }
.u-chips { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 11px; }
.u-chip {
  padding: 4px 11px; border-radius: 999px; font-size: 12px;
  border: 1px solid var(--line); background: var(--bg-raised); color: var(--fg-dim);
}
/* Every red chip is a person whose cards are not finished. */
.u-chip.bad { border-color: rgba(217,87,76,.45); color: #d9574c; }
.u-line .bad { color: #d9574c; }

.u-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 14px; }
.u-card { min-width: 0; font-size: 12px; }
.u-card .u-name { font-weight: 600; margin-top: 7px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.u-card .muted { font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.u-line { margin-top: 2px; font-variant-numeric: tabular-nums; }
.u-shot {
  aspect-ratio: 4 / 5; border-radius: 10px; overflow: hidden; background-size: cover;
  background-position: center; display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line);
}
.u-shot.bare { background: var(--bg-raised); }
.u-shot img { width: 74%; height: auto; filter: drop-shadow(0 6px 14px rgba(0,0,0,.45)); }
.u-none { font-size: 11px; color: var(--fg-faint); }

/* ---------- PSA offers ----------
 * The one red in the app is PSA's, reserved for the label - and an offer IS a
 * PSA moment, so the tag borrows it.  Everywhere else the offer sits next to
 * the market price and never replaces it. */
:root { --psa-red: #d8262c; }
.off-tag {
  display: inline-flex; align-items: center; margin-left: 10px;
  padding: 3px 9px; border-radius: 999px;
  background: var(--psa-red); color: #fff;
  font: inherit; font-size: 12px; font-weight: 700; letter-spacing: .01em;
  font-variant-numeric: tabular-nums; white-space: nowrap;
  box-shadow: 0 4px 14px rgba(216,38,44,.35);
}
.off-tag.big { font-size: 15px; padding: 4px 12px; vertical-align: middle; }
.ghost-big.psa-offers { display: inline-flex; align-items: center; gap: 8px; }
.ghost-big.psa-offers .psa-logo { height: 15px; }
.ghost-big.psa-offers:hover { border-color: var(--psa-red); }

/* On the tile: value dimmed, offer in red, the word "offer" where the chip
   would be, and what the offer means against the purchase worn ON the slab. */
.tile.offer-tile .s .txt.muted { color: var(--fg-faint); margin-right: 6px; }
.tile .s .txt.offer { color: var(--psa-red); font-weight: 700; }
.tile .s .offer-word { color: var(--psa-red); opacity: .8; }
.offer-chip {
  position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
  padding: 3px 9px; border-radius: 999px; color: #fff;
  font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums;
  white-space: nowrap; z-index: 3; pointer-events: none;
  box-shadow: 0 3px 10px rgba(0,0,0,.35);
}
.offer-chip small { font-size: 9px; font-weight: 600; opacity: .85; margin-left: 2px; }
.offer-chip.up { background: rgba(31,157,99,.94); }
.offer-chip.down { background: rgba(216,86,79,.94); }
.tile-slab { position: relative; }

/* The PSA button in the player: the mark alone, the offer tag riding its top edge. */
.glass-btn.psa-glass {
  position: relative; display: inline-flex; align-items: center;
  padding: 10px 18px; text-decoration: none; line-height: 1; overflow: visible;
}
.glass-btn.psa-glass .bg-psa { height: 15px; display: block; filter: drop-shadow(0 1px 2px rgba(0,0,0,.4)); }
.glass-btn.psa-glass.has-offer { border-color: rgba(216,38,44,.55); }
.off-tag.on-btn {
  position: absolute; top: -12px; left: 50%; transform: translateX(-50%);
  margin: 0; font-size: 10.5px; padding: 2px 8px;
}
