/* Graded slab: the card sits inside a clear plastic case with the grader's
   label inside the case, as the grading companies ship them. Tokens only. */
.ps-slab {
  --_s: var(--slab-scale, 1);
  /* px-based, scaled: a percentage padding resolves against the containing
     block, so a 52px slab inside a full-width list row grew 60px of padding
     per side (measured 2026-09-22). */
  --_inset: calc(12px * var(--_s));
  position: relative;
  display: grid;
  /* auto/auto, not a fixed aspect-ratio on the case: the case's height
     follows its content (label strip + art) so the art box below can take
     the source image's own aspect ratio instead of being forced into a
     fixed 100:164 box -- design spec 2026-09-23 item 1. A fixed case ratio
     combined with object-fit:contain on the art is what produced the
     letterbox band the owner reported ("a black band between the label
     strip and a letterboxed image"). */
  grid-template-rows: auto auto;
  gap: calc(var(--space-2) * var(--_s));
  width: 100%;
  padding: var(--_inset);
  box-sizing: border-box;
  border-radius: calc(14px * var(--_s));
  background:
    linear-gradient(118deg, var(--slab-case-hi) 0%, transparent 46%),
    linear-gradient(180deg, transparent 70%, var(--slab-case-lo) 100%),
    var(--slab-case);
  box-shadow:
    inset 0 0 0 2px var(--slab-edge-hi),
    inset 0 0 0 3px var(--slab-edge),
    0 0 0 1px var(--slab-edge),
    var(--elev-1);
  overflow: hidden;
  isolation: isolate;
}
.ps-slab::before {
  /* inner seam of the two-part case */
  content: "";
  position: absolute;
  inset: 2.2%;
  border-radius: calc(9px * var(--_s));
  box-shadow: inset 0 0 0 1px var(--slab-seam);
  pointer-events: none;
  z-index: 2;
}
.ps-slab--hero { --slab-scale: 1.15; width: min(100%, 272px); }

/* Label inside the case */
.ps-slab__strip {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: calc(var(--space-2) * var(--_s));
  row-gap: calc(2px * var(--_s));
  min-height: calc(30px * var(--_s));
  padding: calc(4px * var(--_s)) calc(7px * var(--_s)) calc(3px * var(--_s));
  border: 2px solid var(--slab-label-edge);
  border-radius: calc(4px * var(--_s));
  background: var(--slab-label-bg);
  color: var(--slab-label-ink);
  font-family: var(--font-sans);
  line-height: 1;
}
.ps-slab__label {
  grid-column: 1;
  grid-row: 1;
  font-size: calc(var(--font-size-200) * var(--_s));
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ps-slab__grade {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  font-size: calc(var(--font-size-700) * var(--_s));
  font-weight: 800;
  letter-spacing: -0.02em;
}
.ps-slab__code {
  grid-column: 1;
  grid-row: 2;
  display: flex;
  align-items: center;
  gap: calc(var(--space-2) * var(--_s));
  min-width: 0;
}
.ps-slab__code::before {
  /* barcode strip: two layered bar patterns, no data */
  content: "";
  flex: 1 1 auto;
  height: calc(9px * var(--_s));
  max-width: 62%;
  background:
    repeating-linear-gradient(90deg, var(--slab-label-ink) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg, var(--slab-label-ink) 0 2px, transparent 2px 7px);
  opacity: 0.85;
}
.ps-slab__grader {
  flex: 0 0 auto;
  font-family: var(--font-sans);
  font-size: calc(var(--font-size-100) * var(--_s));
  font-weight: 800;
  letter-spacing: 0.08em;
  color: var(--slab-grader-ink);
}

/* Card behind the plastic: fills the height left under the label and shows the
   whole card (contain), never a cropped one. */
.ps-slab__art {
  position: relative;
  z-index: 1;
  min-height: 0;
  /* The image's own aspect ratio, not the case's -- object-fit:contain
     inside a fixed-ratio box is what let --well (near-black in dark theme)
     show through as a letterbox band. --card2 never renders near-black. */
  aspect-ratio: auto;
  border-radius: calc(4px * var(--_s));
  overflow: hidden;
  background: var(--card2);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.ps-slab__art > img {
  width: 100%;
  height: auto;
  display: block;
}
.ps-slab__art > span {
  aspect-ratio: 5 / 7;
  width: 100%;
  display: grid;
  place-items: center;
  color: var(--muted);
  background: var(--card2);
}
.ps-slab__art > span svg { width: 42%; height: auto; }

/* Grader label treatments */
.ps-slab[data-grader="psa"] {
  --slab-label-edge: var(--slab-psa);
  --slab-grader-ink: var(--slab-psa);
}
.ps-slab[data-grader="bgs"] {
  --slab-label-bg: var(--slab-bgs);
  --slab-label-edge: var(--slab-bgs);
  --slab-label-ink: var(--slab-bgs-ink);
  --slab-grader-ink: var(--slab-bgs-ink);
}
.ps-slab[data-grader="bgs"][data-grade-high="1"] {
  --slab-label-edge: var(--slab-bgs-gold);
  --slab-label-ink: var(--slab-bgs-gold);
  --slab-grader-ink: var(--slab-bgs-gold);
}
.ps-slab[data-grader="cgc"] {
  --slab-label-bg: var(--slab-cgc);
  --slab-label-edge: var(--slab-cgc);
  --slab-label-ink: var(--slab-cgc-ink);
  --slab-grader-ink: var(--slab-cgc-ink);
}

/* Thumbnail slabs (feed rows, list rows, search hits): the label strip cannot
   carry text at 52-64 px, so it shows the grade alone. Live-opportunities
   tiles (96-120px) and Alerts inbox rows (44px) join this group for the same
   reason -- the CSS-drawn strip below still can't set real text at either
   width even though it's wider than the other three. The photoreal
   .ps-slab--tpl variant needs no entry here: its own @container query
   (below) already applies inside any wrapper under 120px generically. */
.hd-feed-art .ps-slab,
.collection-row .ps-slab,
.watch-card-hit .ps-slab,
.opportunity-tile-art .ps-slab,
.alert-receipt-art .ps-slab,
.ps-signal-row__art .ps-slab { --slab-scale: 0.5; --_inset: 3px; }
.hd-feed-art .ps-slab .ps-slab__label,
.hd-feed-art .ps-slab .ps-slab__code,
.collection-row .ps-slab .ps-slab__label,
.collection-row .ps-slab .ps-slab__code,
.watch-card-hit .ps-slab .ps-slab__label,
.watch-card-hit .ps-slab .ps-slab__code,
.opportunity-tile-art .ps-slab .ps-slab__label,
.opportunity-tile-art .ps-slab .ps-slab__code,
.alert-receipt-art .ps-slab .ps-slab__label,
.alert-receipt-art .ps-slab .ps-slab__code,
.ps-signal-row__art .ps-slab .ps-slab__label,
.ps-signal-row__art .ps-slab .ps-slab__code { display: none; }
.hd-feed-art .ps-slab .ps-slab__strip,
.collection-row .ps-slab .ps-slab__strip,
.watch-card-hit .ps-slab .ps-slab__strip,
.opportunity-tile-art .ps-slab .ps-slab__strip,
.alert-receipt-art .ps-slab .ps-slab__strip,
.ps-signal-row__art .ps-slab .ps-slab__strip { grid-template-columns: 1fr; grid-template-rows: auto; min-height: 0; padding: 2px 4px; justify-items: center; }
.hd-feed-art .ps-slab .ps-slab__grade,
.collection-row .ps-slab .ps-slab__grade,
.watch-card-hit .ps-slab .ps-slab__grade,
.opportunity-tile-art .ps-slab .ps-slab__grade,
.alert-receipt-art .ps-slab .ps-slab__grade,
.ps-signal-row__art .ps-slab .ps-slab__grade { grid-column: 1; grid-row: 1; font-size: 11px; line-height: 1; }

/* Photoreal slab (scripts/build-slab-templates.py output, site/assets/slabs/).
   site/member.js's slabMarkup() renders this markup instead of the
   CSS-drawn case above whenever a grader maps to a processed template
   (site/assets/slabs/templates.json); a grader with no entry there always
   renders the CSS-drawn .ps-slab unchanged, so these rules only ever apply
   alongside a data-tpl-key attribute. Geometry (window rect, label text
   rects) is per-grader data, not CSS -- member.js reads templates.json and
   writes each element's position as an inline style, so these rules stay
   generic across every grader; a new processed template needs no CSS
   change here. */
.ps-slab.ps-slab--tpl {
  display: block;
  padding: 0;
  background: none;
  box-shadow: var(--elev-1);
  border-radius: calc(10px * var(--_s, 1));
  aspect-ratio: var(--slab-tpl-aspect, 0.64);
  container-type: inline-size;
}
.ps-slab.ps-slab--tpl::before { content: none; }
/* Full-bleed art (coordinator fix, 2026-09-23 owner report: "the card is
   supposed to cover 100% of the white space" -- a PSA 10 Charizard tile
   showed a visible white band all round because .ps-slab__art's box, AND
   object-fit:contain inside it, both sized to the smaller flat card-art
   PLACEHOLDER instead of the case's whole white insert). Two independent
   fixes, both required:
     1. The box itself: member.js now writes left/top/width/height from
        templates.json's `window`, which scripts/build-slab-templates.py's
        find_insert_window() measures as the WHOLE white insert (see that
        function's docstring), not the nested placeholder.
     2. The fill: object-fit:cover (not contain) so the image scales to
        FILL that box on its shorter axis and crops the overhang on the
        longer one, the way a real card fills its holder, instead of
        letterboxing to fit both axes inside it.
   border-radius reads templates.json's own per-template radiusX/radiusY
   (member.js writes them as --art-radius-x/-y inline, percent of THIS
   box's own w/h) via the CSS `%/%'` slash form -- a single shared value
   would not reproduce the source's circular-in-px corner on a non-square
   window (see measure_corner_radius_pct's docstring); the 1.6cqw fallback
   only applies if a template is somehow missing those (pre-migration
   templates.json cached in a browser, for instance). */
.ps-slab--tpl .ps-slab__art {
  position: absolute;
  overflow: hidden;
  border-radius: var(--art-radius-x, 1.6cqw) / var(--art-radius-y, 1.6cqw);
  z-index: 1;
}
.ps-slab--tpl .ps-slab__art > img,
.ps-slab--tpl .ps-slab__art > span {
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  object-fit: cover;
  display: block;
}
/* Many catalog photos (PriceCharting-style listing scans) carry a thin
   white/near-white margin baked into the source image itself, outside the
   card's own printed edge -- object-fit:cover alone does not remove it
   (cover crops to the WINDOW's aspect, not to the card's printed edge
   inside the photo), which would reintroduce a visible band at whatever
   edge cover does not already crop for aspect. The art route
   (src/index.js's /art/<id> handler) has no existing image-processing
   step to extend -- it is a byte-for-byte cache/proxy of the upstream
   image (fetch -> edge cache -> response body, unmodified) -- so this is a
   CSS-only trim: a small uniform overscale on top of the cover fit,
   applied AFTER cover has already sized to fill the box, crops a further
   ~1.7% off each edge. Small enough to be a no-op in practice on the
   common case (no border) and on a normal close-cropped card photo, while
   visibly reducing a typical thin scan margin. A photo with a materially
   thicker margin still needs real per-image trimming -- out of reach of a
   fixed CSS constant -- which would belong in this same /art/<id> handler
   the day it gains real image processing. */
.ps-slab--tpl .ps-slab__art > img {
  transform: scale(1.035);
}
.ps-slab--tpl .ps-slab__case {
  position: relative;
  z-index: 2;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}
/* The label texts (line1/line2/grade_word/grade_number): font-size in cqw
   so one rule scales from a 52px thumbnail to a 700px hero (design spec),
   sized off the container established by .ps-slab--tpl above, not the
   viewport. Bold/uppercase/tight tracking to read as a printed slab label,
   not body copy; single-line ellipsis since a real label never wraps. */
.ps-slab--tpl .ps-slab__text {
  position: absolute;
  z-index: 3;
  margin: 0;
  font-family: "Helvetica Neue", "Arial Narrow", Arial, sans-serif;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: -0.01em;
  line-height: 1.05;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ps-slab--tpl .ps-slab__text--line1,
.ps-slab--tpl .ps-slab__text--line2,
.ps-slab--tpl .ps-slab__text--grade_word { font-size: 5.4cqw; }
.ps-slab--tpl .ps-slab__text--grade_number { font-size: 9.5cqw; font-weight: 800; }
/* Thumbnail rule, mirrored from the CSS-drawn slab's own (.hd-feed-art
   .ps-slab .ps-slab__label etc. above): below 120px container width only
   the grade number stays legible, so hide the rest and keep the number
   alone -- a container query, not the class list above, so it applies
   inside ANY narrow wrapper, not just the three hardcoded there. */
@container (max-width: 120px) {
  .ps-slab--tpl .ps-slab__text--line1,
  .ps-slab--tpl .ps-slab__text--line2,
  .ps-slab--tpl .ps-slab__text--grade_word { display: none; }
}

/* Tile image rule (approved 2026-09-27). The kind of a tile image is decided
   by the field it came from (site/member.js resolveTileArt), never its
   pixels. Catalog art goes inside the slab template when graded and fills
   the tile edge to edge. A listing photo (an eBay gallery photo, which for a
   graded card already shows its own slab and label) is never framed in a
   template: it is contained -- never cropped, so the seller's label stays
   readable -- on the neutral --art-photo-matte, with a grade tag and a
   "Photo" tag. */
.ps-art-photo {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  overflow: hidden;
  background: var(--art-photo-matte);
  box-shadow: inset 0 0 0 1px var(--border);
}
.hd-card-art > .ps-art-photo,
.collection-grid-art-wrap > .ps-art-photo { position: absolute; inset: 0; width: auto; height: auto; }
.ps-art-photo > .ps-art-photo__img,
.ps-art-photo > .ps-art-photo__img-empty {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  object-position: center;
}
.ps-art-photo > .ps-art-photo__img-empty { display: grid; place-items: center; color: var(--muted); }
.ps-art-photo > .ps-art-photo__img-empty svg { width: 36%; height: auto; }
.ps-art-photo__tags {
  position: absolute;
  left: 6px;
  right: 6px;
  bottom: 6px;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  pointer-events: none;
}
.ps-art-tag {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 5px;
  border: 1px solid var(--border-control);
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  color: var(--text);
  font: 700 var(--font-size-100)/1.2 var(--font-mono);
  letter-spacing: .05em;
  text-transform: uppercase;
  white-space: nowrap;
}
.ps-art-tag svg { width: 10px; height: 10px; flex: none; }
/* Card sheet hero: the same art-box aspect as the tiles. */
.ps-art-photo--hero { height: auto; aspect-ratio: var(--art-box-aspect); max-width: 560px; border-radius: 12px; }
/* Row thumbnails (Collection list, Sold, eBay activity): the photo alone. */
img.ps-art-photo-thumb { object-fit: contain; background: var(--art-photo-matte); }
