/* Retro Andy Mart catalogue — black page, grey panels, three primaries with meaning. The
   wordmark font is self-hosted (no CDN/external requests); everything else stays on the
   system stack for JP glyph coverage. */

/* RetroPix by Brandon Kleeman, CC BY 3.0 — credit line in the footer and site/README.md. */
@font-face {
  font-family: "RetroPix";
  src: url("fonts/retropix.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: optional; /* never swap mid-view; preload makes it ready at first paint */
}

:root {
  color-scheme: dark; /* native selects/scrollbars/spinners render dark, not light chrome */

  /* mart palette — black page, grey panels. The logo's primaries carry meaning only:
     blue acts, green is good, red is danger. Never decorative. */
  --bg: #000000;
  --surface: #161616;     /* tiles, drawer, modal */
  --surface-2: #222222;   /* inputs, pills */
  --border: #333333;      /* rule */
  --text: #F2F2F2;
  --muted: #9A9A9A;
  --accent: #4DA3E8;      /* blue — buttons, focus rings, links */
  --focus: #4DA3E8;
  --danger: #C93A30;      /* red — danger borders/fills */
  --danger-text: #FF7A6E; /* red text on dark surfaces */
  --accent-ink: #000000;  /* dark ink on blue: white on this blue fails contrast */
  --good: #5CCB5C;        /* green — added, near mint */

  /* chip tints — text-on-dark hues for finish/condition pills; all ≥5:1 on --surface-2 */
  --holo: #7CBDF2;     /* holo finishes — lighter step of the brand blue */
  --rev-holo: #C4B2E8; /* violet: same "shiny" family as holo, clearly separable */
  --cond-lp: #B8C97E;  /* condition ramp runs --good → lp → mp → hp → --danger-text */
  --cond-mp: #DDAF6B;
  --cond-hp: #D98E62;

  /* scales */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2.5rem;
  --fs-xs: 0.73rem;
  --fs-sm: 0.85rem;
  --fs-base: 1rem;
  --fs-md: 1.15rem;
  --fs-lg: 1.45rem;
  --fs-hero: clamp(1.9rem, 4.5vw, 2.6rem);
  --radius-sm: 6px;
  --radius: 10px;
  --radius-lg: 14px;
  --radius-pill: 999px;
  --z-toolbar: 10;
  --z-backdrop: 40;
  --z-drawer: 50;

  --page-max: 1400px;
  --page-pad: clamp(1rem, 4vw, 2.5rem);
  --shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
  --font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
    "Hiragino Sans", "Noto Sans JP", sans-serif;
  --font-brand: "RetroPix", "Courier New", monospace;
}

* { box-sizing: border-box; }

/* [hidden] must beat any display: flex/grid a component class sets */
[hidden] { display: none !important; }

body.no-scroll { overflow: hidden; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
    "Hiragino Sans", "Noto Sans JP", sans-serif;
}

/* Full-bleed sections whose content aligns to a shared 1400px column. */
.site-header, .toolbar, .grid, .site-footer {
  padding-inline: max(var(--page-pad), env(safe-area-inset-left), calc((100% - var(--page-max)) / 2));
}

:where(button, a, select, input, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Perforated rule — coupon-edge flourish, used exactly twice (header + footer). */
.site-header::after, .site-footer::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: repeating-linear-gradient(90deg, var(--border) 0 7px, transparent 7px 14px);
}

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

.site-header {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-top: var(--space-5);
  padding-bottom: var(--space-4);
}
.site-header::after { bottom: 0; }

.brand { display: flex; align-items: center; gap: var(--space-3); }

.wordmark {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 0.35em;
  row-gap: 0.2em;
  margin: 0;
  font-family: var(--font-brand);
  font-weight: 400;
  font-size: var(--fs-hero);
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--text);
}
.brand-icon { display: block; flex-shrink: 0; height: 1.3em; width: auto; }

.tagline {
  margin: var(--space-2) 0 0;
  color: var(--muted);
  font-size: var(--fs-base);
}

.collection-note {
  margin: 0;
  color: var(--muted);
  font-size: var(--fs-sm);
  white-space: nowrap;
}
.collection-note strong { color: var(--text); font-weight: 600; }

/* ---------- toolbar ---------- */

.toolbar {
  position: sticky;
  top: 0;
  z-index: var(--z-toolbar);
  padding-top: var(--space-3);
  padding-bottom: var(--space-3);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
}

.toolbar-row { display: flex; gap: var(--space-2); }

/* Slim-bar brand mark: occupies its slot from first paint (no reveal reflow), fades in
   when the header has scrolled out so the mark isn't shown twice at rest. */
.toolbar-brand {
  align-self: center;
  opacity: 0;
  transition: opacity 0.25s ease;
}
.toolbar-brand.show { opacity: 1; }

.search {
  flex: 1;
  min-width: 0;
  padding: 0.6rem 0.9rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  font-size: 16px; /* ≥16px so iOS Safari doesn't zoom on focus */
}

.filters-toggle { display: none; white-space: nowrap; }

.cart-toggle {
  position: relative;
  white-space: nowrap;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
  background: var(--surface);
  color: var(--text);
  border-radius: var(--radius-pill);
  padding: 0.55rem 1.1rem;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.cart-toggle:hover { border-color: var(--accent); }

.cart-count {
  display: inline-block;
  min-width: 1.35em;
  margin-left: 0.35rem;
  padding: 0.05rem 0.35rem;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 700;
  text-align: center;
}

.filter-count {
  display: inline-block;
  min-width: 1.3em;
  padding: 0 0.3rem;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--accent-ink);
  font-weight: 700;
  font-size: var(--fs-xs);
  text-align: center;
}

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.filters select, .price-input {
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  max-width: 13rem;
}
.price-input { width: 5.5rem; }

.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.6rem;
  border-radius: var(--radius-pill);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-xs);
  cursor: pointer;
}
.chip:hover { border-color: var(--accent); }
.chip span { color: var(--muted); }

.toolbar-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  flex-wrap: wrap;
}
.result-count { margin: var(--space-2) 0 0; color: var(--muted); font-size: var(--fs-sm); }

/* Sort is a view control, not a filter — it lives on the meta row (outside #filters), so
   the mobile sheet reparenting never swallows it and Clear never resets it. */
.sort-control {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: var(--space-2);
  color: var(--muted);
  font-size: var(--fs-sm);
  white-space: nowrap;
}
.sort-control select {
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
}

.prices-asof { margin: var(--space-2) 0 0; font-size: var(--fs-xs); opacity: 0.8; }

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

/* One honest line above the grid — the footer's "How this works" surfaced where a
   first-time visitor actually looks. Static HTML, so it never shifts layout. */
.how-note {
  margin: var(--space-4) 0 0;
  margin-inline: max(var(--page-pad), env(safe-area-inset-left), calc((100% - var(--page-max)) / 2));
  color: var(--muted);
  font-size: var(--fs-sm);
  max-width: none;
}
.how-note strong { color: var(--text); }

.list-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-5);
  margin-inline: max(var(--page-pad), env(safe-area-inset-left), calc((100% - var(--page-max)) / 2));
  padding: var(--space-3) var(--space-4);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  border-radius: var(--radius);
  background: var(--surface);
}
.list-banner p { margin: 0; }
/* the banner already carries the top spacing when it's visible */
.list-banner:not([hidden]) + .grid { padding-top: var(--space-4); }

.grid {
  list-style: none;
  margin: 0;
  padding-top: var(--space-5);
  padding-bottom: var(--space-6);
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--space-4);
}

/* Chunked-render sentinel: a full-width sliver so it never claims a tile cell. */
.grid-sentinel { grid-column: 1 / -1; height: 1px; margin: 0; padding: 0; }

.tile {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  cursor: pointer;
  transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
  display: flex;
  flex-direction: column;
  content-visibility: auto;
  contain-intrinsic-size: auto 340px;
}
.tile:hover {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--text) 55%, var(--border));
  box-shadow: var(--shadow);
}

.card-art {
  margin: 0;
  position: relative;
  aspect-ratio: 63 / 88;
  /* faint pixel grid so loading tiles read as an empty sprite sheet */
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M0 .5H16M.5 0V16' fill='none' stroke='%23333333' stroke-opacity='.45'/%3E%3C/svg%3E"),
    var(--surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
}
.card-art img { width: 100%; height: 100%; object-fit: contain; display: block; }
.art-fallback { display: none; color: var(--muted); padding: var(--space-4); text-align: center; }
.card-art.no-img img { display: none; }
.card-art.no-img .art-fallback { display: block; }

/* Column flex + flex:1 so short tiles stretch and .tile-bottom's auto margin pins the
   price + add row to the bottom edge, level across a grid row. */
.tile-info { padding: 0.7rem 0.8rem 0.8rem; flex: 1; display: flex; flex-direction: column; }
.tile-name { margin: 0; font-size: var(--fs-base); line-height: 1.25; }
.tile-local { margin: 0.1rem 0 0; color: var(--muted); font-size: var(--fs-sm); }
.tile-set { margin: var(--space-1) 0 0; color: var(--muted); font-size: var(--fs-xs); }

/* The card name doubles as the tile's real button (stretched-link pattern): its ::after
   covers the tile, while .tile-bottom re-raises the Add button above the overlay. */
.tile-open {
  all: unset;
  cursor: pointer;
}
.tile-open:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.tile-open::after {
  content: "";
  position: absolute;
  inset: 0;
}
/* Show the keyboard ring on the whole tile, not the inline name */
@supports selector(:has(*)) {
  .tile-open:focus-visible { outline: none; }
  .tile:has(.tile-open:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
}

.tile-badges { margin: 0.35rem 0 0; display: flex; flex-wrap: wrap; gap: 0.3rem; }
.tile-badges:empty { display: none; }

.pill {
  font-size: var(--fs-xs);
  padding: 0.1rem 0.45rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  color: var(--muted);
  background: var(--surface-2);
  white-space: nowrap;
}
/* Tinted pills share one recipe: coloured text + a border mixed toward the same hue.
   app.js maps finish/condition values to these classes; unmapped values stay neutral. */
.pill-lang  { --tint: var(--text); }
.pill-holo  { --tint: var(--holo); }
.pill-rev   { --tint: var(--rev-holo); }
.pill-first { --tint: var(--text); }
.pill-nm    { --tint: var(--good); }
.pill-lp    { --tint: var(--cond-lp); }
.pill-mp    { --tint: var(--cond-mp); }
.pill-hp    { --tint: var(--cond-hp); }
.pill-dmg   { --tint: var(--danger-text); }
.pill-lang, .pill-holo, .pill-rev, .pill-first,
.pill-nm, .pill-lp, .pill-mp, .pill-hp, .pill-dmg {
  color: var(--tint);
  border-color: color-mix(in srgb, var(--tint) 40%, var(--border));
}

.tile-bottom {
  position: relative; /* above the .tile-open stretched-link overlay */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: auto;   /* flush to the tile's bottom (see .tile-info) */
  padding-top: 0.6rem;
}
.tile-value {
  font-family: var(--font-display);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  font-size: var(--fs-base);
}

.add-btn {
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: 0.35rem 0.7rem;
  min-height: 2rem;
  font-size: var(--fs-sm);
  cursor: pointer;
  white-space: nowrap;
}
.add-btn:hover { border-color: var(--accent); }
.add-btn.in-cart { background: var(--good); color: var(--accent-ink); border-color: var(--good); font-weight: 600; }

/* ---------- skeleton tiles (shown until the catalogue loads) ---------- */

.tile-skeleton { pointer-events: none; }
.sk-bar { height: 0.8em; border-radius: 4px; background: var(--surface-2); margin-top: 0.5em; }
.sk-w70 { width: 70%; }
.sk-w45 { width: 45%; }

@media (prefers-reduced-motion: no-preference) {
  .tile-skeleton .card-art::before {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(100deg, transparent 30%,
      color-mix(in srgb, var(--text) 6%, transparent) 50%, transparent 70%);
    transform: translateX(-100%);
    animation: sk-shimmer 1.6s ease-in-out infinite;
  }
  @keyframes sk-shimmer {
    to { transform: translateX(100%); }
  }
}

/* ---------- empty / error states ---------- */

.empty-state { text-align: center; color: var(--muted); padding: 3rem var(--space-4); }
.empty-state p { margin: 0 0 var(--space-4); }

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

.primary-btn {
  background: var(--accent);
  color: var(--accent-ink);
  border: none;
  border-radius: var(--radius-sm);
  padding: 0.55rem 1rem;
  font-weight: 700;
  cursor: pointer;
}
.primary-btn:hover { filter: brightness(1.07); }
.primary-btn:disabled, .ghost-btn:disabled { opacity: 0.45; cursor: default; }

.ghost-btn {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.9rem;
  cursor: pointer;
}
.ghost-btn:hover { border-color: var(--accent); }
.ghost-btn.danger:hover { border-color: var(--danger); color: var(--danger-text); }

/* A button that reads as a link — set/series badges in the modal, the share action.
   Inherits font-size so it sits flush in .tile-set (xs) and .modal-links (base) alike. */
.link-btn {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: var(--accent);
  cursor: pointer;
}
.link-btn:hover { text-decoration: underline; }
/* instant tap acknowledgement — matters most on touch, where hover never fires */
.ghost-btn:active, .primary-btn:active, .add-btn:active { filter: brightness(1.15); }

/* ---------- modal ---------- */

.card-modal {
  /* No display/position overrides here: author styles beat the UA sheet, so an
     unconditional display would un-hide the CLOSED dialog, and a position override
     breaks the UA's fixed centering — showModal() then scrolls the page to top when
     it focuses the close button. Layout lives under [open] only; the UA position:fixed
     already makes the open dialog the containing block that pins .modal-close. */
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  padding: 0;
  /* fixed width + full-height nav rails: prev/next steps through cards of varying
     content length, and the click targets must not move under the pointer. Height
     stays content-sized — the rails span whatever height results. */
  --rail: 2.75rem;
  width: min(760px, 92vw);
  max-height: 88dvh;
  overflow: hidden;
  box-shadow: var(--shadow);
}
.card-modal[open] {
  display: grid;
  grid-template-columns: var(--rail) 1fr var(--rail);
}
.card-modal::backdrop { background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(2px); }

/* The scroller is block flow; only .modal-main (art | info) is a grid. The rec row must
   NOT be a modal-body grid child: a second grid row sizes row 1 to the info column (the
   art's overflow:hidden strips its auto minimum) and the aspect-ratio figure then paints
   over the row — the same mechanism as the ≤560px art-overpaint rule below. */
.modal-body {
  grid-row: 1;
  grid-column: 2;
  padding: var(--space-5);
  max-height: 88dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.modal-main {
  display: grid;
  grid-template-columns: minmax(220px, 300px) 1fr;
  gap: var(--space-5);
}
.modal-art { position: relative; border-radius: var(--radius); overflow: hidden; align-self: start; }

/* Recommended-cards strip: block-flow sibling after .modal-main, deliberately below the
   details + add button (the upsell never outranks the primary CTA). One spacing rule
   (margin + rule + padding) on every viewport. */
.rec-row {
  border-top: 1px solid var(--border);
  padding-top: var(--space-4);
  margin-top: var(--space-4);
}
.rec-heading {
  margin: 0 0 var(--space-3);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-base);
}
/* Desktop: a show-all 2×4 grid — mouse users have no natural horizontal-scroll gesture,
   hover arrows test poorly, and macOS overlay scrollbars are invisible until scrolled, so
   a strip hides its tail from most desktop users (NN/g + Baymard testing; sources in the
   vault storefront note). The ≤560px block swaps this for the touch swipe-strip. */
.rec-strip {
  display: grid;
  /* auto-fill keeps tiles readable at any modal width: a neat 4+4 in the full 760px
     modal, fewer columns (more rows) as it narrows toward the 560px strip breakpoint */
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--space-3);
  list-style: none;
  margin: 0;
  padding: 0;
}
.rec-tile {
  all: unset;
  box-sizing: border-box;
  display: block;
  width: 100%;
  cursor: pointer;
}
.rec-tile:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.rec-tile .card-art { border-radius: var(--radius); overflow: hidden; }
.rec-tile:hover .card-art { outline: 1px solid var(--text); }
.rec-name {
  display: block;
  margin-top: 0.3rem;
  font-size: var(--fs-xs);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rec-value {
  display: block;
  color: var(--muted);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}
/* Tile + its sibling add button stack; the button never nests inside the tile button. */
.rec-item { display: flex; flex-direction: column; gap: 0.35rem; }
.rec-add { font-size: var(--fs-xs); padding: 0.25rem 0.5rem; min-height: 1.7rem; }

/* Mobile-only jump-to-recs eye-catch (≤560px block below): desktop shows the whole grid,
   so the affordance would be noise there. */
.rec-jump { display: none; }
@keyframes rec-nudge {
  0%, 100% { transform: translateY(-1px); }
  50% { transform: translateY(2px); }
}
.modal-info h2 {
  margin: 0;
  padding-right: 1.75rem; /* keeps a long first line clear of the close button */
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-lg);
  line-height: 1.2;
}
.modal-value {
  position: relative; /* anchors the ⓘ bubble to the price line */
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  margin: 0.8rem 0 0.4rem;
}

/* The site's most load-bearing sentence lives behind this ⓘ, at the point of the number —
   not only in the footer where nobody reads it (storefront-ui-mockup-lessons §4).
   Hover/focus reveal is desktop convenience; the click toggle (.open, wired in app.js)
   is the path that works on touch. */
.info-btn {
  background: none;
  border: none;
  padding: 0;
  margin-left: 0.1rem;
  width: 1.5rem;
  height: 1.5rem;
  display: inline-grid;
  place-items: center;
  vertical-align: middle;
  color: var(--muted);
  cursor: pointer;
}
.info-btn svg { width: 1.05rem; height: 1.05rem; }
.info-btn:hover, .info-btn:focus-visible { color: var(--text); }
.info-bubble {
  display: none;
  position: absolute;
  left: 0;
  top: calc(100% + 0.25rem);
  z-index: 2;
  width: min(36ch, 76vw);
  padding: 0.5rem 0.7rem;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  /* full reset out of the price line's display face */
  font: 400 var(--fs-xs)/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Hiragino Sans", "Noto Sans JP", sans-serif;
  font-variant-numeric: normal;
  color: var(--text);
}
/* Invisible bridge up to the button: without it, the pointer crossing the gap un-hovers
   the button, the bubble vanishes, and hover-reading the text is impossible. */
.info-bubble::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -0.8rem;
  height: 0.8rem;
}
.info-btn:hover + .info-bubble,
.info-btn:focus-visible + .info-bubble,
.info-bubble:hover,
.info-bubble.open { display: block; }

.price-table { border-collapse: collapse; margin: 0.4rem 0 0.8rem; font-size: var(--fs-sm); }
.price-table td { padding: 0.15rem 1rem 0.15rem 0; color: var(--muted); }
.price-table td:last-child { color: var(--text); text-align: right; font-variant-numeric: tabular-nums; }

/* Type-row glyphs sit on the text baseline at text size; the cell is right-aligned,
   so tokens flex from the end. */
.type-token { display: inline-flex; align-items: center; gap: 0.3rem; }
.type-icon { width: 1em; height: 1em; flex-shrink: 0; }

.price-spread { margin: 0 0 0.8rem; }
.price-spread summary {
  cursor: pointer;
  color: var(--muted);
  font-size: var(--fs-sm);
  padding: 0.15rem 0;
}
.price-spread summary:hover { color: var(--text); }
.price-spread .price-table { margin: 0.2rem 0 0; }

.modal-links { display: flex; flex-wrap: wrap; gap: 0.9rem; margin: 0.4rem 0 1rem; }
.modal-links a { color: var(--accent); text-decoration: none; }
.modal-links a:hover { text-decoration: underline; }

.modal-close {
  position: absolute;
  top: 0.4rem;
  /* sits inside the content column so it never overlaps the full-height next rail */
  right: calc(var(--rail, 0rem) + 0.4rem);
  min-width: 2.25rem;
  min-height: 2.25rem;
  display: grid;
  place-items: center;
  background: transparent;
  border: none;
  color: var(--muted);
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
}
.modal-close:hover { color: var(--text); }

/* Full-height rails flanking the body — the whole edge is the button. */
.modal-nav {
  grid-row: 1;
  display: grid;
  place-items: center;
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}
.modal-prev { grid-column: 1; border-right: 1px solid var(--border); }
.modal-next { grid-column: 3; border-left: 1px solid var(--border); }
.modal-nav:hover:not(:disabled) { background: var(--surface-2); color: var(--text); }
.modal-nav:disabled { opacity: 0.3; cursor: default; }
.modal-nav:focus-visible { outline-offset: -3px; }
.modal-close { z-index: 2; }

/* ---------- mobile filter sheet ---------- */

/* Same mechanism as the cart drawer — always rendered, slid offscreen by a transform.
   No <dialog>/top-layer involved: the first modal paint on phone GPUs is a documented
   long-frame source, while a compositor transform on a pre-rendered layer is instant. */
.filter-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  max-height: 85dvh;
  background: var(--surface);
  border-top: 1px solid var(--border);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  transform: translateY(103%);
  /* exit: quick, ease-in; .open overrides with a decelerating entry */
  transition: transform 0.16s ease-in;
  z-index: var(--z-drawer);
  display: flex;
  flex-direction: column;
}
.filter-sheet.open { transform: translateY(0); transition: transform 0.2s cubic-bezier(0.2, 0.9, 0.3, 1); }

.sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4) 0;
}
.sheet-header h2 { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: var(--fs-md); }
.sheet-header .modal-close { position: static; }

.sheet-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  overflow-y: auto;
  overscroll-behavior: contain;
}
.sheet-body .filters { margin-top: 0; }

.sheet-footer { padding: var(--space-2) var(--space-4) calc(var(--space-4) + env(safe-area-inset-bottom)); }
.sheet-footer .primary-btn { width: 100%; }

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

.drawer-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  z-index: var(--z-backdrop);
}

.cart-drawer {
  position: fixed;
  top: 0;
  right: 0;
  height: 100dvh;
  width: min(430px, 100vw);
  background: var(--surface);
  border-left: 1px solid var(--border);
  transform: translateX(102%);
  /* exit: quick, ease-in (get out of the way); .open overrides with a decelerating entry */
  transition: transform 0.16s ease-in;
  z-index: var(--z-drawer);
  display: flex;
  flex-direction: column;
}
.cart-drawer.open { transform: translateX(0); transition: transform 0.2s cubic-bezier(0.2, 0.9, 0.3, 1); }

.drawer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) 1.2rem 0.6rem;
}
.drawer-header h2 { margin: 0; font-family: var(--font-display); font-weight: 700; font-size: var(--fs-md); }
.drawer-header .modal-close { position: static; }

.cart-note { color: var(--muted); font-size: var(--fs-sm); padding: 0 1.2rem; }

.cart-items {
  list-style: none;
  margin: 0;
  padding: 0 1.2rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  flex: 1;
}
.cart-items li {
  display: grid;
  grid-template-columns: 44px 1fr auto auto;
  gap: 0.6rem;
  align-items: center;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--border);
}
.cart-items img { width: 44px; height: 61px; object-fit: contain; border-radius: 4px; background: var(--surface-2); }
.cart-name { margin: 0; font-size: var(--fs-sm); }
.cart-meta { margin: 0; color: var(--muted); font-size: var(--fs-xs); }
.cart-value { font-size: var(--fs-sm); font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; white-space: nowrap; }

.remove-btn {
  min-width: 2.25rem;
  min-height: 2.25rem;
  display: grid;
  place-items: center;
  background: transparent;
  border: none;
  color: var(--muted);
  font-size: 1.2rem;
  cursor: pointer;
}
.remove-btn:hover { color: var(--danger-text); }

.drawer-footer {
  padding: 0.8rem 1.2rem calc(1.2rem + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
}
.cart-total { margin: 0 0 0.7rem; font-weight: 600; }
.cart-total:empty { display: none; }
.drawer-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.confirm-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
  font-size: var(--fs-sm);
  color: var(--muted);
}

.copy-fallback-wrap { margin-top: var(--space-3); }
.copy-fallback-wrap p { margin: 0 0 var(--space-1); font-size: var(--fs-sm); color: var(--muted); }
.copy-fallback {
  width: 100%;
  min-height: 6rem;
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  font-size: var(--fs-sm);
  resize: vertical;
}

/* ---------- footer ---------- */

.site-footer {
  position: relative;
  padding-top: var(--space-5);
  padding-bottom: var(--space-6);
  color: var(--muted);
}
.site-footer::before { top: 0; }
.site-footer p { max-width: 65ch; }
.fineprint { font-size: var(--fs-xs); opacity: 0.8; }
.contact { margin-top: var(--space-3); font-size: var(--fs-sm); }
.contact strong { color: var(--text); }
.how-note a, .site-footer a, .drawer-footer a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}
.how-note a:hover, .site-footer a:hover, .drawer-footer a:hover { filter: brightness(1.15); }

@media (min-width: 721px) {
  .site-footer {
    display: grid;
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    column-gap: var(--space-6);
    align-items: start;
  }
  .site-footer > p { margin: 0; max-width: none; }
  .site-footer .contact { grid-column: 2; grid-row: 1; }
  .site-footer .fineprint { grid-column: 1; margin-top: var(--space-4); }
  .site-footer .prices-asof { grid-column: 1 / -1; margin-top: var(--space-3); }
}

/* ---------- signature: tilt (fine pointers, motion allowed) ---------- */

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .tile { --tx: 0deg; --ty: 0deg; }
  .tile:hover {
    transform: perspective(700px) rotateX(var(--ty)) rotateY(var(--tx)) translateY(-3px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .tile, .cart-drawer, .filter-sheet, .toolbar-brand { transition: none; }
}

/* ---------- small screens ---------- */

/* Phones + narrow tablets: filters collapse behind the Filters button; controls become
   big, thumb-sized, and two-per-row. */
@media (max-width: 720px) {
  .site-header {
    /* space-3 (was space-4): fold space is scarcest here — see mockup-lessons "above
       the fold". The wordmark moment survives; the padding didn't need to. */
    padding-top: var(--space-3);
    padding-bottom: 0.6rem;
    flex-wrap: wrap;
    gap: var(--space-1);
  }
  .wordmark { font-size: var(--fs-lg); }
  .tagline { font-size: var(--fs-sm); }
  .brand { gap: var(--space-2); }
  .brand-icon { width: 40px; height: 40px; }
  /* The note wraps to its own header line here, so un-hiding it when the catalogue
     lands pushed everything below down (~0.19 CLS). Reserve the line from first paint;
     visibility (not display) keeps the reservation while [hidden] keeps it out of AT. */
  .collection-note {
    font-size: var(--fs-xs);
    flex-basis: 100%;
    min-height: 1lh;
  }
  .collection-note[hidden] {
    display: block;
    visibility: hidden;
  }

  /* Solid toolbar on phones: backdrop blur recomposites through every keyboard-open
     and scroll frame on mobile GPUs — the polish isn't worth the jank there. */
  .toolbar {
    background: var(--bg);
    backdrop-filter: none;
  }

  /* #search and #filters live inside the bottom sheet here (app.js reparents them);
     the toolbar shows only the sheet trigger + the cart button. Until the reparent runs,
     hide them in the toolbar (ancestor-scoped, so they reappear inside the sheet): on a
     slow phone first paint beats app.js execution, and painting the inline layout first
     meant the reparent yanked ~190px out of the toolbar mid-view (~0.18 CLS). */
  .toolbar .search,
  .toolbar .filters {
    display: none;
  }

  .filters-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    flex: 1;
    min-width: 0;
  }

  .filters select, .price-input {
    flex: 1 1 46%;
    max-width: none;
    width: auto;
    min-width: 0;
    font-size: 16px; /* no iOS focus zoom */
    padding: 0.6rem;
  }
  #clear-filters { flex: 1 1 46%; }
  /* match the filter selects' thumb sizing; 16px so iOS doesn't zoom on focus */
  .sort-control select { font-size: 16px; padding: 0.6rem; }

  .drawer-actions .primary-btn, .drawer-actions .ghost-btn { flex: 1 1 45%; padding: 0.7rem 0.5rem; }
}

/* Narrowest phones: drop the visible "Sort" text (the select keeps its aria-label) so a
   filtered count ("23 of 822 cards") and the sort control still share one meta line. */
@media (max-width: 360px) {
  .sort-label { display: none; }
}

@media (max-width: 560px) {
  .cart-label { display: none; }
  .collection-note { display: none; }
  .cart-drawer { width: 100%; border-left: none; }
  /* Single column as normal flow, NOT grid: the dialog stretches #modal-body to 88dvh,
     and when stacked content exceeds that, grid shrinks the art's row to the leftover
     space (overflow:hidden strips its auto minimum) — the art then paints over the info
     below. Block flow cannot shrink children, so the overlap is impossible. */
  .modal-main { display: block; }
  .modal-art { max-width: 240px; margin: 0 auto var(--space-4); }
  .card-modal { --rail: 2rem; width: 94vw; }
  .modal-body { padding: var(--space-4); }
  /* the × floats over the card art here — scrim keeps it legible on any artwork */
  .card-modal .modal-close {
    background: color-mix(in srgb, var(--bg) 65%, transparent);
    border-radius: var(--radius-pill);
    color: var(--text);
  }
  /* Add-to-list stays reachable without scrolling the modal body: a small sticky pill
     at the scroller's bottom-right; opaque fill, so content sliding beneath reads fine. */
  .modal-info .add-btn {
    position: sticky;
    bottom: 0;
    display: block;
    width: auto;
    margin-left: auto;
    box-shadow: 0 -6px 14px rgba(0, 0, 0, 0.4);
  }
  /* The rec row sits below the fold on phones; this floating down-arrow signposts it and
     taps straight to it. Absolute anchors to the UA-fixed dialog (the .modal-close
     precedent), so it holds still while #modal-body scrolls beneath — and being outside
     the scroller's positioning context it is not clipped by it. The IntersectionObserver
     in app.js flips `hidden` whenever the row is actually on screen; [hidden] must win
     over this display:grid. Bottom-LEFT: the sticky add pill owns bottom-right. */
  .rec-jump {
    display: grid;
    place-items: center;
    position: absolute;
    left: calc(var(--rail) + 0.4rem);
    bottom: 0.9rem;
    width: 44px;
    height: 44px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--surface-2) 88%, transparent);
    border: 1px solid var(--accent);
    color: var(--accent);
    cursor: pointer;
    z-index: 2;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.45);
  }
  .rec-jump[hidden] { display: none; }
  @media (prefers-reduced-motion: no-preference) {
    .rec-jump svg { animation: rec-nudge 1.6s ease-in-out infinite; }
  }
  /* Touch: swipe strip, ~2.5 tiles with the cut-off one signalling it scrolls (the
     peek is the tested-effective mobile cue). min-width kills the flex auto-minimum,
     which would let a card with a large natural image inflate its own tile. */
  .rec-strip {
    display: flex;
    overflow-x: auto;
    padding-bottom: var(--space-2);
    scroll-snap-type: x proximity;
    scrollbar-width: thin;
  }
  .rec-strip li { flex: 0 0 34vw; min-width: 0; scroll-snap-align: start; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.7rem; }
  .tile-info { padding: 0.55rem 0.6rem 0.65rem; }
  /* Thumb-sized add: value on its own line, button a full-width 44px bar beneath */
  .tile-bottom { flex-direction: column; align-items: stretch; gap: 0.45rem; }
  .tile-bottom .add-btn { min-height: 44px; }
}

/* Thumb-sized targets on touch devices; visual glyphs unchanged. */
@media (pointer: coarse) {
  .add-btn { min-height: 44px; }
  .remove-btn, .modal-close { min-width: 44px; min-height: 44px; }
  .chip { min-height: 36px; padding-inline: 0.8rem; }
}
