* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --primary: #4fc3f7;
  --accent: #66bb6a;
  --bg: #0f0f1a;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: linear-gradient(135deg, var(--bg) 0%, #16213e 50%, #0f3460 100%);
  min-height: 100vh;
  min-height: 100dvh;
  color: #e0e0e0;
}

.container {
  max-width: 95%;
  margin: 0 auto;
  padding: 1.5rem 2rem;
}

header {
  text-align: center;
  margin-bottom: 2rem;
}

header h1 {
  font-size: 2.5rem;
  color: #fff;
  margin-bottom: 0.5rem;
}

.subtitle {
  color: #a0a0a0;
  font-size: 1.1rem;
}

h2 {
  color: #fff;
  margin-bottom: 1rem;
  font-size: 1.5rem;
}

/* Search Section */
.search-section {
  background: rgba(255, 255, 255, 0.05);
  border-radius: 12px;
  padding: 2rem;
  margin-bottom: 2rem;
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.search-bar {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.search-bar input {
  flex: 1;
  padding: 0.8rem 1.2rem;
  border: 2px solid rgba(255, 255, 255, 0.2);
  border-radius: 8px;
  font-size: 1rem;
  background: rgba(255, 255, 255, 0.08);
  color: #fff;
  transition: border-color 0.3s;
}

.search-bar input:focus {
  outline: none;
  border-color: var(--primary);
}

.search-bar input::placeholder {
  color: #888;
}

.search-bar button {
  padding: 0.8rem 2rem;
  background: var(--primary);
  color: #1a1a2e;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.3s, transform 0.1s;
}

.search-bar button:hover {
  background: #81d4fa;
  transform: translateY(-1px);
}

/* Category Tabs (Market page) */
.category-tabs {
  display: flex;
  gap: 0.4rem;
  margin: 1rem 0;
  flex-wrap: wrap;
}
.cat-tab {
  padding: 0.5rem 1.2rem;
  background: rgba(255,255,255,0.05);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 8px;
  color: #ccc;
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 500;
  transition: all 0.15s;
}
.cat-tab:hover {
  background: rgba(79,195,247,0.08);
  border-color: rgba(79,195,247,0.3);
}
.cat-tab.active {
  background: rgba(79,195,247,0.15);
  border-color: #4fc3f7;
  color: #4fc3f7;
  font-weight: 600;
}

.search-bar button:active {
  transform: translateY(0);
}

/* Mode Selector */

/* Select dropdowns */
select {
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.2);
  border-radius: 8px;
  color: #fff;
  padding: 0.6rem;
  font-size: 0.85rem;
}
select option {
  background: #1a1a2e;
  color: #fff;
}

.mode-selector {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.mode-option {
  cursor: pointer;
}

.mode-option input[type="radio"] {
  display: none;
}

.mode-label {
  display: inline-block;
  padding: 0.5rem 1.2rem;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 20px;
  font-size: 0.9rem;
  color: #aaa;
  transition: all 0.3s;
}

.mode-option input[type="radio"]:checked + .mode-label {
  background: rgba(79, 195, 247, 0.2);
  border-color: var(--primary);
  color: var(--primary);
}

.mode-option:hover .mode-label {
  border-color: rgba(255, 255, 255, 0.4);
  color: #fff;
}

.foil-text {
  background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(255, 105, 180, 0.1));
}

.mode-option input[type="radio"]:checked + .foil-text {
  background: linear-gradient(135deg, rgba(255, 215, 0, 0.3), rgba(255, 105, 180, 0.2));
  border-color: #ffd700;
  color: #ffd700;
}

.loading {
  text-align: center;
  padding: 1rem;
  color: var(--primary);
  font-style: italic;
}

.error {
  text-align: center;
  padding: 1rem;
  color: #ef5350;
  background: rgba(239, 83, 80, 0.1);
  border-radius: 8px;
}

.hidden {
  display: none;
}

/* Card Grid */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 1rem;
  margin-top: 1rem;
}

.card-item {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 10px;
  padding: 1rem;
  transition: transform 0.2s, box-shadow 0.2s;
  display: flex;
  flex-direction: column;
  align-items: center;
  overflow: hidden;
  min-width: 0;
}

.card-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}

.card-item.foil-card {
  border-color: rgba(255, 215, 0, 0.4);
  background: linear-gradient(135deg, rgba(255, 215, 0, 0.05), rgba(255, 105, 180, 0.05), rgba(100, 200, 255, 0.05));
}

/*
 * The frame is the shape of a card, so the picture fills it.
 *
 * This was `height: 280px` — a fixed number — against a width that changes with the screen. With
 * `object-fit: contain` the picture keeps its own proportions inside that box, so whenever the box
 * was not a card's shape the difference showed as dead space above and below it. On a phone
 * showing three across the box was about 130 by 280, and a card is 5:7: the picture drew 182px
 * tall and left ninety-eight pixels of empty frame. Henry circled exactly those edges.
 *
 * A trading card is 63 by 88 millimetres. Giving the frame that ratio and letting the height
 * follow the width means the picture fits at any size, at any number across — the same reasoning
 * `.asc-img` on the intake screen already had, written down in one place and not the other.
 */
.card-image {
  width: 100%;
  /*
   * 100%, and stated once.
   *
   * This said 200px for a long time and never meant it: a bare '.card-image { max-width: 100%; }'
   * sat 980 lines below, same specificity and later in the file, so it won everywhere. Three
   * media queries set 120px, 200px and 180px at their breakpoints and lost to it too. Four
   * numbers, none of them applied, all of them read as if they did — which is how the foil
   * wrapper came to be capped at 200px to "match" a rule that resolved to 100%, and foils shipped
   * 29.5px narrower than the card beside them at 1900px wide.
   */
  max-width: 100%;
  aspect-ratio: 5 / 7;
  height: auto;
  object-fit: contain;
  border-radius: 8px;
  margin-bottom: 0.8rem;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}

.card-image.foil-img {
  box-shadow: 0 4px 16px rgba(255, 215, 0, 0.3);
  position: relative;
}

/* Foil holographic shimmer effect — animated overlay style */
/*
 * A foil card and a plain one are the same size, because the box is the same box.
 *
 * Henry: "ภาพแบบ nonfoil มันไม่เท่ากับ foil render ไม่เท่ากัน", and then "ทำไมเห็นกรอบ foil มันล้น
 * จากภาพอยู่อีก". One cause behind both.
 *
 * A plain card is an `<img class="card-image">` sitting in the card, so `width: 100%` measures the
 * card's column and the 5:7 frame above gives it its height. A foil card is the same image wrapped
 * in `.foil-shimmer-wrap`, which is `display: inline-block` with no width of its own — it shrinks
 * to fit its contents, while its contents ask for 100% of it. CSS breaks that circle by falling
 * back to the picture's own pixel width, so a foil came out whatever size the file happened to be,
 * and never the size of the card beside it.
 *
 * The shimmer is an `::after` pinned to all four sides of that wrapper, so once the wrapper stopped
 * matching the picture the rainbow frame hung off the edges. That is the second thing he circled;
 * it was never a separate bug.
 *
 * So the wrapper becomes the box — the same width, cap and 5:7 shape a plain card's image has — and
 * the image inside fills it. Scoped to `.card-item` deliberately: these two wrapper classes are
 * used in eight places across the shop, several of which size them by hand (the intake list's
 * 36×50 thumbnail, for one), and this is a statement about a market card, not about foils.
 */
.card-item .foil-shimmer-wrap,
.card-item .surge-foil-wrap {
  display: block;
  width: 100%;
  /* Whatever '.card-image' above resolves to. Not a number copied from it — a number copied is a
   * number that stops matching the day the other one moves, which is exactly what happened here. */
  max-width: 100%;
  aspect-ratio: 5 / 7;
  margin-bottom: 0.8rem;
}

.card-item .foil-shimmer-wrap .card-image,
.card-item .surge-foil-wrap .card-image {
  width: 100%;
  height: 100%;
  /* The wrapper owns the shape now; a second one here would fight it. */
  aspect-ratio: auto;
  max-width: none;
  margin-bottom: 0;
}

.foil-shimmer-wrap {
  position: relative;
  display: inline-block;
  overflow: hidden;
  border-radius: 8px;
}
.foil-shimmer-wrap img {
  display: block;
}
.foil-shimmer-wrap::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><defs><linearGradient id="g" x1="0%25" y1="0%25" x2="100%25" y2="100%25"><stop offset="0%25" style="stop-color:rgb(255,0,128);stop-opacity:0.3"/><stop offset="16%25" style="stop-color:rgb(255,200,0);stop-opacity:0.3"/><stop offset="33%25" style="stop-color:rgb(0,255,100);stop-opacity:0.3"/><stop offset="50%25" style="stop-color:rgb(0,150,255);stop-opacity:0.3"/><stop offset="66%25" style="stop-color:rgb(128,0,255);stop-opacity:0.3"/><stop offset="83%25" style="stop-color:rgb(255,100,200);stop-opacity:0.3"/><stop offset="100%25" style="stop-color:rgb(255,200,0);stop-opacity:0.3"/></linearGradient></defs><rect fill="url(%23g)" width="200" height="200"/></svg>');
  background-size: 300% 300%;
  mix-blend-mode: color-dodge;
  opacity: 0.55;
  z-index: 1;
  pointer-events: none;
  animation: foilGradient 4s ease infinite;
}
@keyframes foilGradient {
  0% { background-position: 0% 0%; }
  25% { background-position: 100% 0%; }
  50% { background-position: 100% 100%; }
  75% { background-position: 0% 100%; }
  100% { background-position: 0% 0%; }
}

/* Surge Foil — Three.js WebGL shader overlay (see surge-foil.js) */
.surge-foil-wrap {
  position: relative;
  display: inline-block;
  overflow: hidden;
  border-radius: 8px;
}
.surge-foil-wrap img {
  display: block;
}

/*
 * The whole name, however long it runs.
 *
 * Henry: "ชื่อห้ามมี ... ต้องโชวเต็ม เพราะ card variant เยอะ มีโอกาสชื่อหน้าเดียวกันแต่ชื่อหลัง
 * มันต่างกัน และเจอบ่อยมาก".
 *
 * These three lines used to say nowrap, hidden, ellipsis, which is the right treatment for a
 * label and the wrong one for an identifier. Two cards can share every character up to the point
 * where the ellipsis falls and be different printings at different prices — which is the one
 * thing a shopper is reading this line to tell apart. A tile that is a few pixels taller costs
 * nothing; a name that stops before the part that distinguishes it costs a wrong order.
 *
 * The set and variant line below gets the same treatment for the same reason: "0215 - Borderless
 * Surge Foil" and "0212 - Borderless Surge Foil" differ at the front, but the edition after
 * them is what separates two cards that are otherwise identical.
 *
 * Grid tiles stretch to the tallest in their row, so uneven names leave the row even.
 */
.card-item .card-name {
  font-size: 0.95rem;
  font-weight: 600;
  color: #fff;
  margin-bottom: 0.3rem;
  text-align: center;
  width: 100%;
  white-space: normal;
  /* A long unbroken word — some promo names have no spaces — breaks rather than pushing the
     tile wider than its column. */
  overflow-wrap: anywhere;
}

.card-item .card-set {
  font-size: 0.8rem;
  color: #aaa;
  margin-bottom: 0.3rem;
  text-align: center;
  width: 100%;
  /* See the note on .card-name above: this line carries the edition, which is half of what
     tells two printings of the same card apart. */
  white-space: normal;
  overflow-wrap: anywhere;
}

.card-item .card-condition {
  font-size: 0.75rem;
  color: var(--text-muted);
  margin-top: auto;
  padding-top: 0.5rem;
  margin-bottom: 0.5rem;
  text-align: center;
}

.card-item .foil-badge {
  display: inline-block;
  padding: 0.2rem 0.6rem;
  background: linear-gradient(135deg, #ffd700, #ff69b4);
  color: #1a1a2e;
  border-radius: 10px;
  font-size: 0.7rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
}

.card-item .preorder-badge {
  display: inline-block;
  padding: 0.2rem 0.6rem;
  background: rgba(255, 167, 38, 0.2);
  color: #ffa726;
  border-radius: 10px;
  font-size: 0.7rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
}

.card-item .sealed-badge {
  display: inline-block;
  padding: 0.2rem 0.6rem;
  background: rgba(171, 71, 188, 0.2);
  color: #ce93d8;
  border-radius: 10px;
  font-size: 0.7rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
}

.card-item .accessory-badge {
  display: inline-block;
  padding: 0.2rem 0.6rem;
  background: rgba(79, 195, 247, 0.2);
  color: #4fc3f7;
  border-radius: 10px;
  font-size: 0.7rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
}

/* A Set of X. Amber, because the two badges above it are already purple and blue and a shopper
   scanning a mixed grid tells them apart by hue before they read the word. */
.card-item .bundle-badge {
  display: inline-block;
  padding: 0.2rem 0.6rem;
  background: rgba(255, 183, 77, 0.2);
  color: #ffb74d;
  border-radius: 10px;
  font-size: 0.7rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
}

.card-item .card-price {
  font-size: 1.3rem;
  font-weight: 700;
  color: var(--accent);
  margin-bottom: 0.8rem;
  text-align: center;
}

.card-item .add-btn {
  width: 100%;
  padding: 0.6rem;
  background: var(--accent);
  color: #1a1a2e;
  border: none;
  border-radius: 6px;
  font-size: 0.9rem;
  font-weight: 600;
  /* Both stated, and stated here so they reach the <button> and the <a> alike.
     Measured: the pre-order tile came out 46px against the cart tile's 44 in the same row. A
     <button> does not inherit font-family — it was rendering in the user agent's Arial while the
     link took the page font — and neither had a line-height, so each fell back to its own idea of
     `normal`. Two different fonts at two different normals is where the two pixels were. */
  font-family: inherit;
  line-height: 1.25;
  cursor: pointer;
  transition: background 0.3s;
}

.card-item .add-btn:hover {
  background: #81c784;
}

/* A pre-order goes somewhere instead of into the basket, so it is an <a> — which needs the box
   model a <button> gets for free, or it sits at a different height to every other tile in the
   row and stops filling the card's width. Amber rather than the green of Add to Cart: it is a
   different promise, and it should not be pressed by muscle memory. */
.card-item a.add-btn {
  display: block;
  box-sizing: border-box;
  text-align: center;
  text-decoration: none;
  background: #f59e0b;
  color: #1a1a2e;
}
.card-item a.add-btn:hover {
  background: #fbbf24;
}

/* "Save for later" (market.js wishButtonHtml). Henry, 2026-09-20: it had no rule anywhere in
   public/ -- the two hits a grep finds are .shell-wish-btn, a different element in the header --
   so the browser painted its own white button under every tile.

   It sits directly below Add to Cart on every tile, sold-out ones included, where Add to Cart is
   a full-width green fill. So it is an outline: same width, quieter ink, no fill to compete with.
   Tokens rather than the literals the two rules above use, so day mode follows from theme.css's
   own root block with no second rule. No min-height: the tap-target floor reaches a bare <button>
   and lifts this to 44px on a touch screen by itself. */
.wish-btn {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin-top: 0.4rem;
  padding: 0.45rem;
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1.25;
  cursor: pointer;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.wish-btn:hover {
  background: var(--surface-3);
  color: var(--text);
}
/* Saved is a state, not a second action: aria-pressed already carries it for a screen reader, and
   this gives the eye the same fact. The danger tint reads as "pressing this again takes it back".

   The ink is --text and NOT --danger, which is what this rule shipped with an hour ago. Measured
   on the painted pixel at 127.0.0.1:3001: --danger ink on its own 14% wash over the night ground
   is 3.18:1 at 390 and 3.14:1 at 1440, under the 4.5:1 floor, and it passed by day (4.78) only
   because --danger is a far darker red there. There is no --danger-ink token to reach for -- the
   set has --primary-ink for exactly this reason and the semantic colours never got one -- so the
   hue stays where it is readable, in the border and the wash, and the text takes --text. Saved
   therefore reads BRIGHTER than unsaved (--text-muted), which is what an active state should do. */
.wish-btn[aria-pressed="true"] {
  background: var(--danger-soft);
  border-color: var(--danger);
  color: var(--text);
}

/* Market Section */
.market-section {
  background: rgba(255, 255, 255, 0.05);
  border-radius: 12px;
  padding: 2rem;
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.market-summary {
  display: flex;
  align-items: center;
  gap: 2rem;
  padding: 1rem 1.5rem;
  background: rgba(102, 187, 106, 0.1);
  border: 1px solid rgba(102, 187, 106, 0.3);
  border-radius: 10px;
  margin-bottom: 1.5rem;
  flex-wrap: wrap;
}

.total-value {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.total-value span:first-child {
  font-size: 1rem;
  color: #aaa;
}

#totalValue {
  font-size: 1.8rem;
  font-weight: 700;
  color: var(--accent);
}

.card-count {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.card-count span:first-child {
  color: #aaa;
}

#cardCount {
  font-weight: 600;
  color: #fff;
}

.clear-btn {
  margin-left: auto;
  padding: 0.5rem 1.2rem;
  background: rgba(239, 83, 80, 0.2);
  color: #ef5350;
  border: 1px solid rgba(239, 83, 80, 0.4);
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.85rem;
  transition: background 0.3s;
}

.clear-btn:hover {
  background: rgba(239, 83, 80, 0.4);
}

/* Market List */
.market-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.market-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.8rem 1.2rem;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 8px;
  transition: background 0.2s;
}

.market-item:hover {
  background: rgba(255, 255, 255, 0.08);
}

.market-item-info {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex: 1;
}

.market-item-thumb {
  width: 40px;
  height: 56px;
  object-fit: cover;
  border-radius: 4px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}

.market-item-name {
  font-weight: 500;
  color: #fff;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.market-foil-badge {
  font-size: 0.65rem;
  padding: 0.1rem 0.4rem;
  background: linear-gradient(135deg, #ffd700, #ff69b4);
  color: #1a1a2e;
  border-radius: 8px;
  font-weight: 700;
}

.market-item-set {
  font-size: 0.8rem;
  color: var(--text-muted);
}

.market-item-qty {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.market-item-qty button {
  width: 26px;
  height: 26px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.08);
  color: #fff;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.9rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.market-item-qty button:hover {
  background: rgba(255, 255, 255, 0.15);
}

.market-item-qty span {
  min-width: 24px;
  text-align: center;
  font-weight: 500;
}

.market-item-price {
  font-weight: 600;
  color: var(--accent);
  min-width: 80px;
  text-align: right;
}

.market-item .remove-btn {
  margin-left: 1rem;
  padding: 0.3rem 0.6rem;
  background: transparent;
  color: #ef5350;
  border: 1px solid rgba(239, 83, 80, 0.4);
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.75rem;
  transition: background 0.3s;
}

.market-item .remove-btn:hover {
  background: rgba(239, 83, 80, 0.2);
}

.empty-market {
  text-align: center;
  padding: 2rem;
  color: #666;
  font-style: italic;
}

/* Responsive */
@media (max-width: 768px) {
  .container {
    padding: 0.8rem;
  }

  header h1 {
    font-size: 1.5rem;
  }

  .subtitle {
    font-size: 0.9rem;
  }

  h2 {
    font-size: 1.2rem;
  }

  .search-section {
    padding: 1rem;
    border-radius: 10px;
  }

  .search-bar {
    flex-direction: column;
    gap: 0.5rem;
  }

  .search-bar input {
    width: 100%;
  }

  .search-bar button {
    width: 100%;
    padding: 0.8rem;
  }

  .search-bar select {
    width: 100%;
    padding: 0.7rem;
  }

  .card-grid {
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: 0.6rem;
  }

  .card-item {
    padding: 0.6rem;
  }

  .card-item .card-name {
    font-size: 0.8rem;
  }

  .card-item .card-set {
    font-size: 0.7rem;
  }

  .card-name {
    font-size: 0.85rem;
  }

  .market-summary {
    flex-direction: column;
    align-items: stretch;
    gap: 0.8rem;
  }

  .market-summary button {
    width: 100%;
    padding: 0.8rem;
  }

  .clear-btn {
    margin-left: 0;
  }

  .market-item {
    flex-wrap: wrap;
    gap: 0.5rem;
  }

  .market-item-info {
    width: 100%;
  }

  .buyer-row {
    flex-direction: column;
    gap: 0.3rem;
  }

  .buyer-row input {
    width: 100%;
  }
}

/* Tablet */
@media (min-width: 769px) and (max-width: 1024px) {
  .container {
    padding: 1rem;
  }

  /* Columns for this range are set in the tablet block further down (200px). The rule that stood
     here said 180px and never applied: two definitions for one breakpoint, the later one winning
     silently. Anyone tuning this number would have seen nothing happen. */
}

/* Touch-friendly: minimum tap target 44px */
@media (hover: none) and (pointer: coarse) {
  button, .tab-btn, .nav-link, .add-btn, .primary-btn, .secondary-btn, .sell-btn {
    min-height: 44px;
    min-width: 44px;
  }

  input, select, textarea {
    font-size: 16px; /* Prevents iOS zoom on focus */
    min-height: 44px;
  }

  .card-item .add-btn {
    padding: 0.7rem;
    font-size: 0.9rem;
  }
}

/* Lightbox - click to enlarge card images */
.lightbox-overlay {
  display: none;
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.85);
  z-index: 3000;
  align-items: center;
  justify-content: center;
  cursor: zoom-out;
}
.lightbox-overlay.active { display: flex; }
.lightbox-overlay img {
  max-width: 90vw;
  max-height: 90vh;max-height: 90dvh;
  border-radius: 12px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.lightbox-overlay .foil-shimmer-wrap {
  border-radius: 12px;
}
.lightbox-overlay .foil-shimmer-wrap img {
  border-radius: 12px;
}
.lightbox-overlay .surge-foil-wrap {
  border-radius: 12px;
}
.lightbox-overlay .surge-foil-wrap img {
  border-radius: 12px;
}
.card-img-clickable { cursor: zoom-in; }

/* Payment method badges */
.badge-pickup { background: rgba(0, 188, 212, 0.2); color: #00bcd4; padding: 0.2rem 0.6rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; white-space: nowrap; }
.badge-pos { background: rgba(76, 175, 80, 0.2); color: #66bb6a; padding: 0.2rem 0.6rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; white-space: nowrap; }
.badge-delivery { background: rgba(255, 152, 0, 0.2); color: #ffb74d; padding: 0.2rem 0.6rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; white-space: nowrap; }

/* Stage tracker connector line */
.stage-tracker { position: relative; }
.stage-tracker::before {
  content: '';
  position: absolute;
  top: 6px;
  left: 5%;
  right: 5%;
  height: 2px;
  background: rgba(255,255,255,0.15);
  z-index: 0;
  pointer-events: none;
}
.stage-step { z-index: 1; }
.stage-dot { position: relative; z-index: 1; }

/* ===================== CHECKOUT MODAL ===================== */
/* The bottom padding carries the cookie notice — see public/lib/cookie-notice.js.
 *
 * 9999 loses to the notice's 10000, but raising it would be the wrong fix and would not work
 * anyway: the notice is a legal control and has to stay on top. Measured before this line
 * existed, with the panel at its full height and scrolled all the way down: at 1280x800
 * "Confirm Payment" and "← Back" both sat at y=688 under a notice starting at 692; at 375x812
 * "Confirm Payment" was at y=734 and the Transfer / Pay-at-Shop pair at y=647; at 320x640 the
 * slip-upload field went under it too. elementFromPoint at each button's own centre returned the
 * notice, and scrolling could not help because these are the last controls in the scroll.
 *
 * This is the step that takes the money, and it only bites a visitor who has not pressed
 * "Got it" — which is every visitor arriving from a promotion. */
.checkout-modal {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  padding-bottom: calc(var(--cookie-notice-height, 0px) + env(safe-area-inset-bottom, 0px));
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
}
.checkout-modal.hidden { display: none; }
.checkout-backdrop {
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.7);
}
.checkout-content {
  position: relative;
  background: #1a1a2e;
  border: 1px solid rgba(255,255,255,0.15);
  border-radius: 12px;
  width: 95%;
  max-width: 560px;
  /* `90%` and not `90vh`. Identical while no cookie notice is on screen; while one is, the
     modal's content box is the space actually left, and a percentage shrinks with it. A `vh`
     would leave the panel taller than the box it is centred in, which overflows at both ends
     and puts the ✕ above the top of the screen. */
  max-height: 90%;
  overflow-y: auto;
  padding: 2rem;
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.checkout-close {
  position: absolute;
  top: 1rem; right: 1rem;
  background: none;
  border: none;
  color: #888;
  font-size: 1.8rem;
  cursor: pointer;
  line-height: 1;
}
.checkout-close:hover { color: #fff; }

/* Step indicators */
.checkout-steps {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}
.checkout-step {
  font-size: 0.8rem;
  color: #666;
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
.checkout-step span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: rgba(255,255,255,0.1);
  font-size: 0.7rem;
  font-weight: 700;
}
.checkout-step.active { color: #4fc3f7; }
.checkout-step.active span { background: #4fc3f7; color: #1a1a2e; }
.checkout-step.done { color: #66bb6a; }
.checkout-step.done span { background: #66bb6a; color: #1a1a2e; }
.checkout-step-line {
  width: 30px; height: 2px;
  background: rgba(255,255,255,0.15);
}

/* Timer */
.checkout-timer {
  background: rgba(239,83,80,0.1);
  border: 1px solid rgba(239,83,80,0.3);
  border-radius: 8px;
  padding: 0.6rem 1rem;
  text-align: center;
  margin-bottom: 1rem;
  color: #ef5350;
  font-weight: 600;
  font-size: 0.9rem;
}

/* No time to show yet (step 1, before the order exists): paint nothing, not an empty bar. */
.checkout-timer:empty { display: none; }

/* Items list */
.checkout-items {
  max-height: 200px;
  overflow-y: auto;
  margin-bottom: 1rem;
}
.checkout-item {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
/*
 * One box, two things that can sit in it.
 *
 * `.checkout-item-noimg` is the placeholder market.js draws when a line has no picture, or when
 * the picture 404s. It is named on this rule rather than given its own so that the 36x50 lives
 * in exactly one place: restating those two numbers in the markup is the drift this file's own
 * history is a record of — four hard-coded card widths that never applied, and a foil wrapper
 * capped at 200px to "match" a rule that resolved to 100%. A placeholder whose box is copied
 * from here is a placeholder that stops matching the day this line changes.
 *
 * `object-fit` is inert on a div and is left on the shared rule rather than split out; it costs
 * nothing and splitting the rule would put the width back in two places, which is the whole
 * point of not doing that.
 */
.checkout-item img,
.checkout-item-noimg {
  width: 36px; height: 50px;
  object-fit: cover;
  border-radius: 4px;
}
.checkout-item-info { flex: 1; }
.checkout-item-name { font-size: 0.85rem; font-weight: 600; color: #fff; }
.checkout-item-detail { font-size: 0.7rem; color: var(--text-muted); }
.checkout-item-price { font-weight: 600; color: var(--accent, #66bb6a); white-space: nowrap; }

/* Summary */
.checkout-summary {
  background: rgba(255,255,255,0.04);
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 1.5rem;
}
.checkout-row {
  display: flex;
  justify-content: space-between;
  padding: 0.3rem 0;
  font-size: 0.9rem;
  color: #ccc;
}
.checkout-row.checkout-total {
  border-top: 1px solid rgba(255,255,255,0.15);
  padding-top: 0.6rem;
  margin-top: 0.4rem;
  font-weight: 700;
  font-size: 1rem;
  color: var(--accent, #66bb6a);
}

/* Address */
.checkout-address-options { margin-bottom: 1.5rem; }
.checkout-radio {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0;
  cursor: pointer;
  color: #ccc;
  font-size: 0.9rem;
}
.checkout-radio input[type="radio"] { accent-color: #4fc3f7; }
.checkout-saved-address {
  background: rgba(79,195,247,0.08);
  border: 1px solid rgba(79,195,247,0.2);
  border-radius: 8px;
  padding: 0.8rem;
  margin: 0.5rem 0 1rem 1.5rem;
  font-size: 0.85rem;
  color: #ccc;
}
.checkout-new-address {
  margin: 0.5rem 0 1rem 1.5rem;
}
.checkout-new-address textarea {
  width: 100%;
  padding: 0.7rem;
  border: 2px solid rgba(255,255,255,0.2);
  border-radius: 8px;
  background: rgba(255,255,255,0.08);
  color: #fff;
  font-size: 0.9rem;
  resize: vertical;
}
.checkout-save-label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
  font-size: 0.8rem;
  color: #888;
  cursor: pointer;
}

/* Payment */
.checkout-payment-methods {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}
.checkout-method-btn {
  flex: 1;
  padding: 0.7rem;
  border: 2px solid rgba(255,255,255,0.15);
  border-radius: 8px;
  background: transparent;
  color: #ccc;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}
.checkout-method-btn.active {
  border-color: #4fc3f7;
  background: rgba(79,195,247,0.1);
  color: #4fc3f7;
}
.checkout-qr {
  display: flex;
  justify-content: center;
  padding: 1rem;
}
.checkout-qr img { max-width: 200px; border-radius: 8px; }
.checkout-upload {
  margin-top: 1rem;
}
.checkout-upload label {
  display: block;
  font-size: 0.85rem;
  color: #ccc;
  margin-bottom: 0.4rem;
}
.checkout-upload input[type="file"] {
  width: 100%;
  padding: 0.5rem;
  background: rgba(255,255,255,0.08);
  border: 1px dashed rgba(255,255,255,0.3);
  border-radius: 8px;
  color: #ccc;
  font-size: 0.85rem;
}
.checkout-slip-preview {
  margin-top: 0.5rem;
  text-align: center;
}
.checkout-slip-preview img {
  max-width: 150px;
  border-radius: 6px;
  border: 1px solid rgba(255,255,255,0.2);
}

/* Navigation buttons */
.checkout-nav {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 1.5rem;
}
.checkout-next { width: 100%; margin-top: 1rem; }


/* Checkout Step 2 extras */
.checkout-saved-info {
  background: rgba(79,195,247,0.06);
  border: 1px solid rgba(79,195,247,0.15);
  border-radius: 8px;
  padding: 0.8rem 1rem;
  margin-bottom: 1rem;
}
.checkout-profile-row {
  display: flex;
  justify-content: space-between;
  padding: 0.25rem 0;
  font-size: 0.85rem;
  color: #ccc;
}
.checkout-profile-row span:first-child {
  color: var(--text-muted);
  min-width: 70px;
}
.checkout-form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.8rem;
}
.checkout-new-address .form-group {
  margin-bottom: 0.8rem;
}
.checkout-new-address .form-group label {
  display: block;
  font-size: 0.8rem;
  color: var(--text-muted);
  margin-bottom: 0.3rem;
}
.checkout-new-address .form-group input,
.checkout-new-address .form-group select {
  width: 100%;
  padding: 0.6rem;
  border: 1px solid rgba(255,255,255,0.2);
  border-radius: 6px;
  background: rgba(255,255,255,0.08);
  color: #fff;
  font-size: 0.9rem;
}
.checkout-new-address .form-group select:disabled {
  opacity: 0.5;
}


/* ===================== RESPONSIVE ENHANCEMENTS ===================== */

/* Mobile: up to 480px (small phones) */
@media (max-width: 480px) {
  .card-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 0.4rem;
  }
  .card-item {
    padding: 0.5rem;
  }
  .card-item .card-name { font-size: 0.75rem; }
  .card-item .card-set { font-size: 0.65rem; }
  .card-item .add-btn { font-size: 0.75rem; padding: 0.5rem; }
  .cond-tab { padding: 0.2rem 0.4rem !important; font-size: 0.6rem !important; }

  /* Condition tabs wrap */
  .cond-tabs { flex-wrap: wrap !important; gap: 0.2rem !important; }

  /* Checkout modal */
  /* `95%` for the same reason `.checkout-content` above is `90%`: a percentage of the space the
     cookie notice has left, which is 95vh whenever there is no notice. */
  .checkout-content { padding: 1rem; max-height: 95%; }
  .checkout-steps { gap: 0.3rem; }
  .checkout-step { font-size: 0.7rem; }
  .checkout-step-line { width: 15px; }
  .checkout-form-grid { grid-template-columns: 1fr; }
  .checkout-nav { flex-direction: column; }
  .checkout-nav button { width: 100%; }

  /* Pagination */
  #searchPagination { flex-wrap: wrap; gap: 0.3rem; }
  #searchPagination .tab { font-size: 0.75rem; padding: 0.3rem 0.6rem; }

  /* Cart */
  .market-item { flex-direction: column; align-items: flex-start; }
  .market-item-price { font-size: 0.85rem; }
}

/* Mobile landscape / larger phones: 481-640px */
@media (min-width: 481px) and (max-width: 640px) {
  /*
   * Two cards to a row on a small screen, counted rather than fitted.
   *
   * Henry: "ถ้าจอมันเล็กมากๆ อยากให้ลดการแสดงผลเหลือ 2 อันทุก games" — the second time he has said
   * it; the first was "ถ้า 3 อันมันแน่นเกิน เหลือ 2 อันก็ได้นะ".
   *
   * `auto-fill` with a 150px minimum answers "how many pictures fit", and three do fit at the
   * 541px he screenshotted. But a card in this shop is not a picture: under it sit a name, a set
   * line, up to six condition buttons, two prices and an Add button. Those need about 165px
   * before they start wrapping into each other — the same figure `minCardPx()` uses to decide a
   * carousel tile on a phone. Three columns of 150 satisfied the grid and squeezed everything
   * below the picture.
   *
   * So the count is stated instead of derived. Below 769px it is two, at every width, for every
   * game — the game pages draw the same `.card-grid`, so this is one rule and not seven.
   */
  .card-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .checkout-content { padding: 1.5rem; }
  .checkout-form-grid { grid-template-columns: 1fr 1fr; }
}

/*
 * Small tablets: 641-768px, three across.
 *
 * "ถ้าจอมันเล็กมากๆ อยากให้ลดการแสดงผลเหลือ 2 อัน" — two is for a screen that is genuinely small,
 * and this is where that stops being true. Two across at 768 was measured at a 302px picture in a
 * 675px tile: one and a half cards visible at a time, down a list of 495. Three gives a 205px
 * picture and a little over two tiles a screen, which is what the band did before.
 *
 * 640 is the line because minCardPx() in lib/carousel.js already draws it there to decide
 * whether a carousel tile is being read on a phone. One number, two places that need it.
 */
@media (min-width: 641px) and (max-width: 768px) {
  .card-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* Tablet: 769-1024px */
@media (min-width: 769px) and (max-width: 1024px) {
  .card-grid {
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  }
}

/* Desktop: 1025px+ */
@media (min-width: 1025px) {
  .card-grid {
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  }
  .container {
    max-width: 95%;
    margin: 0 auto;
  }
}

/* Profile form responsive */
@media (max-width: 480px) {
  .profile-form .form-grid { grid-template-columns: 1fr; }
}

/* Ensure images don't overflow */
img { max-width: 100%; height: auto; }
/* '.card-image { max-width: 100%; }' used to sit here. It was the only cap on a card picture that
 * ever applied, and it applied by being last rather than by saying so — four other declarations
 * up the file looked authoritative and were not. The cap now lives with the rest of that rule. */

/* Condition tabs always wrap on small cards */
.cond-tabs { flex-wrap: wrap; justify-content: center; }

/* Tracking Timeline */
.tracking-timeline { position: relative; padding-left: 24px; margin-top: 1rem; }
.tracking-timeline::before { content: ''; position: absolute; left: 8px; top: 8px; bottom: 8px; width: 2px; background: rgba(255,255,255,0.1); }
.timeline-item { position: relative; padding-bottom: 1.2rem; padding-left: 20px; }
.timeline-item:last-child { padding-bottom: 0; }
.timeline-dot { position: absolute; left: -20px; top: 4px; width: 12px; height: 12px; border-radius: 50%; background: rgba(255,255,255,0.2); border: 2px solid rgba(255,255,255,0.3); }
.timeline-dot.dot-active { background: #4fc3f7; border-color: #4fc3f7; box-shadow: 0 0 8px rgba(79,195,247,0.5); }
.timeline-current .timeline-status { color: #4fc3f7; font-weight: 600; }
.timeline-status { color: #e0e0e0; font-size: 0.85rem; font-weight: 500; margin-bottom: 0.15rem; }
.timeline-location { color: var(--text-muted); font-size: 0.8rem; }
.timeline-date { color: #666; font-size: 0.75rem; margin-top: 0.1rem; }

/* ═══ SETTINGS GROUPS ═══
 *
 * The settings page is twelve sections and ~1,400 lines in one column, with twelve separate
 * Save buttons down it: finding the commission rate meant scrolling past branding, Google
 * sign-in and an SMTP form. lib/settings-nav.js groups them into five and shows one at a time.
 *
 * The strip is built at runtime — the page is design-locked, and more to the point every one
 * of those sections carries a form that writes money settings whose handlers find fields by
 * id. Moving that markup to improve navigation would risk the shop's pricing to rearrange some
 * headings.
 */
.settings-groups {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}

.settings-group-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 1rem;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 10px;
  color: #cfd6dd;
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.settings-group-btn:hover { background: rgba(255, 255, 255, 0.11); color: #fff; }
.settings-group-btn.active {
  background: rgba(79, 195, 247, 0.16);
  border-color: rgba(79, 195, 247, 0.55);
  color: #fff;
}

/* How many sections sit behind this tab — so a group that looks empty can be told apart from
   one that is simply closed. */
.settings-group-count {
  min-width: 1.35rem;
  padding: 0 0.35rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.28);
  font-size: 0.72rem;
  font-weight: 700;
  text-align: center;
  line-height: 1.35rem;
}

/* One line saying what this group is for. A settings page full of unexplained switches is the
   reason people are frightened of settings pages. */
.settings-group-hint {
  margin: 0 0 1.25rem;
  color: #8f9aa5;
  font-size: 0.86rem;
  line-height: 1.5;
  max-width: 68ch;
}

@media (hover: none) and (pointer: coarse) {
  .settings-group-btn { min-height: 44px; }
}

/* ─── NOTICE CAROUSEL MUST NEVER COVER SAVE ─────────────────────────────────
   Henry ruled 2026-09-21 (relayed by Planning, docs/seats/qa.md 80ac79e7): the
   #noti-carousel overlay ext-nav.js's showPopupCarousel() injects must not sit
   in front of a staff screen's primary Save action. ext-nav.js is Security's
   held file right now (SEATBOARD §1, A-SAVE-THAT-NEVER-REACHED-THE-SERVER), so
   this is position-only, via shell.css, with !important to beat the overlay's
   own inline `style.cssText` (the only thing that can beat an inline style).
   Nothing removed or restyled beyond position -- the notice stays full content,
   still dismissible via .nc-close. Full-viewport centered backdrop becomes a
   bottom-right anchored corner panel, out of the way of an in-flow Save button
   on every staff screen. */
#noti-carousel {
  top: auto !important;
  left: auto !important;
  right: 1rem !important;
  bottom: 1rem !important;
  width: min(420px, calc(100vw - 2rem)) !important;
  height: auto !important;
  max-height: 70vh !important;max-height: 70dvh !important;
  align-items: flex-end !important;
  justify-content: flex-end !important;
  padding-bottom: 0 !important;
}
#noti-carousel > * {
  max-height: 70vh;max-height: 70dvh;
  overflow-y: auto;
}

@media (hover: none) and (pointer: coarse) {
  #noti-carousel .nc-close,
  #noti-carousel .nc-prev,
  #noti-carousel .nc-next,
  #noti-carousel .nc-done {
    min-height: 44px;
    min-width: 44px;
  }
}

/* ─── NOTICE PANEL AT ≤480px: OFF THE BOTTOM, OFF THE COOKIE BANNER'S GROUND ──
   QA round 4 (qa.md 9f9f95ed) measured f91af4a2 live: bottom-right holds at
   1280 (40/40) but at 400px it still failed twice -- ext-preorders.html's
   #cSave sits inside the panel's own rect (both bottom-anchored), and
   #cookieNotice (z-index 10000, public/lib/cookie-notice.js) sits over
   #noti-carousel (9990) at the same bottom edge, so a real elementFromPoint
   at .nc-close's centre resolves to the cookie banner, not the button --
   a synthetic .click() had been reading that as dismissible when it is not.
   Moving the panel's anchor to the TOP at this width puts it in a different
   vertical band than either bottom-anchored element, without touching
   ext-nav.js or the cookie banner's own z-index/position. Desktop (>480px)
   is unchanged -- the bottom-right anchor above already holds there.

   ROUND 5 (qa.md b9d1de43): the top anchor cleared both bottom elements but
   introduced its own -- at 400px the panel fully covered #mainNav (rect
   22.8,12.8,354.4,68), so a worker could not reach the hamburger or the
   theme toggle until the notice was dismissed. No header-height convention
   existed (round 4's disclosed deviation) -- it exists now: ext-nav.js
   measures #mainNav's real getBoundingClientRect().bottom, the same
   pattern public/lib/cookie-notice.js already uses for --cookie-notice-
   height, and publishes it as --nav-h on the root element, right before it
   shows the carousel (so nav.js's group sync and ext-nav.js's own link
   injection have both already run). The fallback (5.5rem) is only for a
   page where that measurement never ran. */
@media (max-width: 480px) {
  #noti-carousel {
    top: calc(var(--nav-h, 5.5rem) + 0.5rem) !important;
    bottom: auto !important;
    align-items: flex-start !important;
    justify-content: flex-start !important;
    padding-bottom: 0 !important;
  }
}
