/* shop app: templates/shop/*.html — product grid, variant picker, cart.
   Cards get their own border/padding since .terminal-post itself is
   borderless (see site.css) — the blog list relies on plain vertical
   stacking, but a shop grid needs a visible card boundary per item. */

/* Matches /manage's own width. Uses the "dashboard-page" body class for
   its full-width <main> override, but with scoped .shop-page rules
   instead of adopting .dashboard-page .terminal-panel's folder-tab
   corner notch (that pairs with the tabbed nav bar dashboard.html has,
   which these plain shop pages don't) — same pattern
   notifications_feed.html uses for its own .notifications-page override.
   Critically, /manage's real side gutter on viewports under 1800px comes
   from .dashboard-main's own "padding: 0 20px 40px" wrapper, NOT from
   .dashboard-wide .terminal-panel's max-width/auto-margin (that only
   centers once the viewport actually exceeds 1800px) — without this
   equivalent padding on <main> itself, the shop panel went fully
   edge-to-edge under 1800px while /manage kept its 20px gutter. */
.shop-page main {
  padding: 0 20px;
}
.shop-page .terminal-panel {
  max-width: 1800px;
  width: 100%;
  margin: 40px auto;
  border-radius: 18px;
}
/* Cart specifically reads better narrower -- rows are a single line of
   fixed-size content (image/price/qty/trash), not a grid that benefits
   from the product listing's full width. 1440px (the first attempt at
   "20% narrower than the shared 1800px shop max-width") turned out to
   not even be reachable on a normal monitor -- .terminal-panel's own
   width: 100% already fills whatever's narrower than the cap, so a cap
   above the actual viewport width does nothing visible. 1152px (20%
   off that 1440px, i.e. 1800 * 0.8 * 0.8) sits well inside a typical
   desktop browser width instead. */
.shop-cart-page .terminal-panel {
  max-width: 1152px;
}
/* An empty cart is just a short centered message + button -- the full
   1152px row-list width makes it look sparse/lost on a wide monitor.
   Narrower cap kicks in via the shop-cart-page-empty body class
   (cart.html) only when there's actually nothing in the cart; adding
   an item and coming back gets the regular wider layout above. */
.shop-cart-page-empty .terminal-panel {
  max-width: 480px;
}
.shop-cart-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 16px;
  padding: 20px 0;
}
.shop-cart-empty-emote {
  width: 168px;
  height: 128px;
}
.shop-cart-empty-text {
  margin: 0;
  color: #c9d1d9;
}

/* Header currency switcher (templates/base.html) — sits immediately left
   of the cart icon below, same shop-page-only guard and same reasoning
   for living here instead of site.css. Same custom toggle+menu pattern as
   .header-cart-dropdown just to its right (JS adds/removes .open on
   #header-currency-dropdown) rather than a native <select> -- a native
   select's own open option-list panel can't be restyled in any browser,
   which is exactly the OS-chrome look this replaces. Selecting an option
   fills the hidden #header-currency-form's input and submits it (real
   page load) -- matches the reference layout (plain bold toggle text, no
   button chrome; a rounded card menu with roomy per-row options) recolored
   into this site's own dark/blue palette instead of its light one. */
.header-currency-dropdown {
  position: relative;
  display: inline-flex;
  align-items: center;
  height: 100%;
  margin-right: 14px;
}
.header-currency-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  background: none;
  border: none;
  color: #c9d1d9;
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.header-currency-toggle:hover,
.header-currency-dropdown.open .header-currency-toggle {
  color: #58a6ff;
}
.header-currency-chevron {
  width: 10px;
  height: 6px;
  flex-shrink: 0;
  transition: transform 120ms ease-out;
}
.header-currency-dropdown.open .header-currency-chevron {
  transform: rotate(180deg);
}
.header-currency-menu {
  display: none;
  position: absolute;
  /* .header-currency-dropdown is height: 100% of the 50px header and
     itself starts at the header's own top, so top: 100% (not a fixed
     top: 50px like .header-cart-menu/.header-dropdown-menu use, which
     both anchor to the viewport via position: fixed instead) lands
     exactly on the header's bottom edge -- flush against the navbar,
     no gap, matching those two rather than floating below it. */
  top: 100%;
  /* Lines up each option's text under the toggle's own -- the toggle has
     no left padding, but an option's text sits 8px (this menu's own
     padding) + 14px (.header-currency-option's own left padding) = 22px
     inset from the menu box's edge, so the menu box itself needs to
     start 22px left of the toggle to compensate. */
  left: -22px;
  flex-direction: column;
  /* Shrink-to-fit the widest option's text (e.g. "USD") rather than a
     fixed min-width — width: max-content instead of leaving it implicit
     since the .header-currency-option children below are width: 100%,
     which would otherwise stretch to fill an ancestor-imposed width
     rather than driving it. */
  width: max-content;
  /* Same dropdown-menu palette as .header-dropdown-menu (account menu)
     and .header-cart-menu, so all three header dropdowns read as the
     same component instead of each having their own one-off colors --
     including the flush-top treatment (no border-top, square top
     corners, only the bottom rounded) both of those already use. */
  background-color: rgba(39, 45, 55, 0.98);
  border: 1px solid #333943;
  border-top: none;
  border-radius: 0 0 10px 10px;
  padding: 8px;
  z-index: 20;
}
.header-currency-dropdown.open .header-currency-menu {
  display: flex;
}
.header-currency-option {
  display: block;
  width: 100%;
  padding: 11px 14px;
  border: none;
  border-radius: 12px;
  background: none;
  color: #c9d1d9;
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  text-align: left;
  cursor: pointer;
  transition: background-color 80ms ease-out, color 80ms ease-out;
}
.header-currency-option:hover {
  background-color: rgba(88, 166, 255, 0.08);
  color: #58a6ff;
}
.header-currency-option.active {
  color: #58a6ff;
}

/* Header cart icon + dropdown (templates/base.html) — only ever rendered
   while on a shop page (see the request.resolver_match.app_name check
   there), so it lives here rather than site.css even though the markup
   sits in the shared header; it replaces .header-wallet-balance in the
   same navbar slot for the duration of the shop visit. Click-toggled
   (JS adds/removes .open), unlike the hover-driven .header-dropdown-menu
   the account avatar uses — the cart's content is fetched on open, so a
   hover trigger would refetch on every accidental mouse-over. */
.header-cart-dropdown {
  position: relative;
  display: inline-flex;
  align-items: center;
  margin-right: 5px;
}
.header-cart-link {
  display: inline-flex;
  align-items: center;
  padding: 0 8px;
  height: 100%;
  background: none;
  border: none;
  color: #ffffff;
  cursor: pointer;
  font: inherit;
}
.header-cart-icon-wrap {
  position: relative;
  display: inline-flex;
  /* Anchors .header-cart-badge below — deliberately its own small inline
     wrapper rather than positioning the badge off .header-cart-link
     itself, since that button's own padding would otherwise push the
     badge away from the icon's actual corner. */
}
/* One-shot bump, triggered from shop.html right after a successful Add
   to Cart (see window.bumpShopCartIcon in base.html) -- icon+badge
   move together since they're both inside this same wrapper. */
.header-cart-icon-wrap.bump {
  animation: header-cart-icon-bump 450ms ease-out;
}
@keyframes header-cart-icon-bump {
  0% { transform: scale(1) rotate(0deg); }
  25% { transform: scale(1.3) rotate(-10deg); }
  50% { transform: scale(1.05) rotate(8deg); }
  75% { transform: scale(1.15) rotate(-4deg); }
  100% { transform: scale(1) rotate(0deg); }
}
.header-cart-icon {
  width: 20px;
  height: 20px;
}
.header-cart-badge {
  position: absolute;
  /* Out of normal flow, so this never affects the navbar's own height —
     purely a visual overlay on the icon's top-right corner. */
  top: -6px;
  right: -6px;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  box-sizing: border-box;
  border-radius: 999px;
  background: #58a6ff;
  color: #ffffff;
  font-size: 0.62rem;
  font-weight: 700;
  line-height: 15px;
  text-align: center;
}
.header-cart-badge[hidden] {
  display: none;
}
.header-cart-menu {
  display: none;
  position: fixed;
  top: 50px;
  right: 0;
  /* max-content (bounded by min/max-width) rather than a fixed width —
     lets the dropdown grow to fit the widest row's title/variant text on
     one line (see .header-cart-row-info's white-space: nowrap below)
     instead of wrapping it, while max-width still keeps a very long
     product name from blowing past the viewport. */
  width: max-content;
  min-width: 280px;
  max-width: min(90vw, 560px);
  flex-direction: column;
  background-color: rgba(39, 45, 55, 0.98);
  border: 1px solid #333943;
  border-top: none;
  border-radius: 0 0 10px 10px;
  padding: 12px;
  z-index: 20;
}
.header-cart-dropdown.open .header-cart-menu {
  display: flex;
}
.header-cart-empty {
  color: #8b949e;
  padding: 8px 4px;
}
.header-cart-rows {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: 320px;
  overflow-y: auto;
}
.header-cart-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
}
.header-cart-row img {
  width: 40px;
  height: 40px;
  object-fit: cover;
  border-radius: 6px;
  flex-shrink: 0;
}
.header-cart-row-info {
  flex: 1;
  min-width: 0;
  font-size: 0.85rem;
  color: #c9d1d9;
  white-space: nowrap;
  /* Safety net, not the primary mechanism -- .header-cart-menu's own
     width: max-content (above) is what actually grows the dropdown to
     fit a normal title on one line. This only kicks in past the
     max-width cap, truncating instead of overlapping the price/qty/
     remove columns to its right. */
  overflow: hidden;
  text-overflow: ellipsis;
}
.header-cart-row-side {
  /* Price above, quantity stepper below — its own column so both stay
     right-aligned together, immediately left of the remove button. */
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  flex-shrink: 0;
}
.header-cart-row-price {
  font-size: 0.85rem;
  font-weight: 600;
  white-space: nowrap;
  color: #ffffff;
}
.header-cart-qty-adjust {
  display: flex;
  align-items: center;
  gap: 4px;
}
.header-cart-qty-btn {
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border-radius: 4px;
  border: 1px solid rgba(110, 118, 129, 0.3);
  background: transparent;
  color: #c9d1d9;
  font-size: 0.75rem;
  line-height: 1;
  cursor: pointer;
}
.header-cart-qty-btn:disabled {
  opacity: 0.35;
  cursor: default;
}
.header-cart-qty-value {
  min-width: 14px;
  text-align: center;
  font-size: 0.8rem;
}
.header-cart-remove-btn {
  /* Fixed size — .header-cart-totals below pads itself out by this
     width + the row gap so the total lines up under the itemized
     prices instead of under this column. */
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: none;
  color: #8b949e;
  cursor: pointer;
}
.header-cart-remove-btn:hover {
  color: #f85149;
}
.header-cart-remove-icon {
  width: 16px;
  height: 16px;
}
.header-cart-totals {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  margin-top: 10px;
  padding-top: 10px;
  padding-right: 38px;
  border-top: 1px solid rgba(110, 118, 129, 0.2);
  font-weight: 600;
}
.header-cart-actions {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}
.header-cart-actions .terminal-button {
  flex: 1;
  text-align: center;
}

.shop-grid {
  display: grid;
  /* auto-fit (not auto-fill) collapses unused tracks instead of leaving
     them as empty 1fr columns — with auto-fill + 1fr, a handful of
     products on the now-1800px-wide panel each got stretched into their
     own giant column, pinning them to the left with a huge dead-space
     column to the right. A capped (not 1fr) column size keeps cards at a
     sane width regardless of how many products there are; space-evenly
     (not center) then spends whatever row width is left over as equal
     gaps between/around the cards instead of leaving it as dead margin
     on just the two outer edges. */
  grid-template-columns: repeat(auto-fit, minmax(220px, 260px));
  justify-content: space-evenly;
  gap: 28px;
}
.shop-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.shop-card-image-link {
  display: block;
}
.shop-card-image-frame {
  /* The rounded background lives here, behind the image only — not
     wrapping the title/price/buttons below — and stays a fixed 1:1 box
     across every product regardless of that product's own photo
     proportions, so the grid stays visually uniform. position: relative
     anchors .shop-card-label-badge to this frame's corner. */
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  background: rgba(255, 255, 255, 0.04);
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
/* Mirrors Fourthwall's own product "Label" (e.g. "NEW!", set with an
   optional expiration date in their dashboard's Advanced Settings) --
   scraped from their server-rendered product page, see
   shop/services.py::_fetch_product_label. Shared between the shop
   listing card (.shop-card-image-frame, position: relative above) and
   the product detail page (.shop-product-media-sticky, already
   position: sticky which anchors an absolute child the same way). */
.shop-card-label-badge {
  /* Same color/border/background scheme as .shop-btn-add (the Add to
     Cart button) -- just the resting state, no hover treatment needed
     for a badge nobody clicks. */
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 2;
  padding: 4px 10px;
  border: 1px solid rgba(88, 166, 255, 0.5);
  border-radius: 999px;
  background: rgba(88, 166, 255, 0.1);
  color: #58a6ff;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.shop-card-image {
  width: 100%;
  height: 100%;
  /* contain, not cover — some products (e.g. a taller shirt cut) were
     getting cropped by cover when forced into a square box. contain
     always shows the whole image, letterboxed within the frame's
     background rather than losing part of the photo. */
  object-fit: contain;
  transition: transform 160ms ease-out;
}
.shop-card-image-link:hover .shop-card-image {
  transform: scale(1.05);
}
.shop-card-info {
  /* Own flex context so title/meta/size-row keep the same 10px rhythm
     they had as plain .shop-card flex-column siblings before this
     wrapper existed -- and position: relative so .shop-card-added-overlay
     below can cover exactly this block, not the whole card. */
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.shop-card-name {
  margin: 0;
  font-size: 1.2rem;
  font-weight: bold;
  text-align: center;
}
.shop-card-added-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 10px;
  background: rgba(13, 17, 23, 0.96);
  border-radius: 10px;
  text-align: center;
  z-index: 2;
}
.shop-card-added-overlay[hidden] {
  display: none;
}
.shop-card-added-overlay-text {
  font-weight: 700;
  font-size: 1.05rem;
  color: #7ee787;
}
.shop-card-added-overlay-actions {
  display: flex;
  gap: 8px;
  width: 100%;
}
.shop-card-added-overlay-actions .terminal-button {
  flex: 1;
  min-width: 0;
}
.shop-card-meta-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* Pulls the color swatches/price in from the card's edges just enough
     that .shop-card-added-overlay's own inset: 0 fully covers them --
     the first swatch was peeking out past the overlay's left edge. */
  margin: 0 5px 0 5px;
}
.shop-card-price {
  color: #ffffff;
}
.shop-card-colors {
  display: flex;
  align-items: center;
  gap: 6px;
}
.shop-card-swatch {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid rgba(110, 118, 129, 0.3);
  cursor: pointer;
  padding: 0;
  position: relative;
}
.shop-card-swatch.selected {
  border-color: #7ee787;
  box-shadow: 0 0 0 2px rgba(126, 231, 135, 0.3);
}
.shop-card-colors-more {
  color: #ffffff;
  font-size: 0.8rem;
  font-weight: 600;
  text-decoration: none;
}
.shop-card-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.shop-card-actions-row {
  display: flex;
  gap: 8px;
}
.shop-card-add-form,
.shop-card-buy-form {
  flex: 1;
  min-width: 0;
  display: flex;
}
.shop-card-add-btn,
.shop-card-buy-btn {
  /* min-width: 0 overrides flex items' default content-based floor
     (min-width: auto) -- without it, "Add to cart" being longer than
     "Buy now" would keep pulling more than its fair half of the row
     despite flex: 1's equal-grow factor, breaking the two buttons'
     symmetry. */
  flex: 1;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
}
/* Its own full-width row below Add/Buy, not a third flex sibling of
   theirs -- a distinct secondary action rather than competing for the
   same row's space. A plain <a> (not a <form>+<button>) since it just
   links out to Fourthwall's own native product page -- white-space:
   nowrap keeps the emoji+label on one line even on the shop listing
   card's narrower width, where "Add to cart" is allowed to wrap but
   this shouldn't. */
.shop-card-gift-btn {
  width: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  white-space: nowrap;
}
/* Same reasoning as .header-cart-badge[hidden] / .header-live-indicator
   [hidden] -- this class's own "display: inline-flex" is an author rule
   that would otherwise beat the UA stylesheet's [hidden] { display: none
   }. Toggled by the live-status poll in base.html. */
.shop-card-gift-btn[hidden] {
  display: none;
}
.shop-btn-cart-icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}
.shop-btn-twitch-icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}
/* Confirmed state swaps the cart icon out for the heart badge instead
   of showing both -- same fixed slot, so the button's own width/height
   never changes for it (unlike stacking the heart under the "Added!"
   text used to, which needed a second line's worth of height the
   product detail page's wide button never reserved, since "Add to
   cart" never wraps to 2 lines there the way it does on the narrower
   shop listing card -- this sidesteps that entirely). */
.shop-card-added-heart-icon {
  display: none;
  width: 16px;
  height: 14px;
  flex-shrink: 0;
  /* Bundled as a small pixel-art sprite (badges/seed_assets/donor_1.png,
     the same "Donor" badge shown on a supporter's profile/chat name) --
     pixelated keeps its edges crisp at this size instead of the
     browser's default smoothing blurring it. */
  image-rendering: pixelated;
}
.shop-card-add-btn-confirmed .shop-btn-cart-icon {
  display: none;
}
.shop-card-add-btn-confirmed .shop-card-added-heart-icon {
  display: block;
}
/* Add to Cart (blue) and Buy Now (gold) get their own tinted-at-rest
   look -- not just a hover state -- so the two clearly read as
   different actions rather than one primary + one washed-out
   "secondary" button (that generic .terminal-button.secondary is used
   for de-emphasized actions like Cancel elsewhere, which also happens
   to share its hover-state specificity with .terminal-button:hover and
   lose to source order, silently killing Buy Now's hover entirely --
   these compound .terminal-button.shop-btn-* hover selectors are one
   specificity level higher so that can't happen here). Gold reuses the
   header wallet balance's own currency color for a "money/purchase"
   association distinct from the green already used for
   selected/confirmed states elsewhere on this page. */
.shop-btn-add {
  border-color: rgba(88, 166, 255, 0.5);
  background: rgba(88, 166, 255, 0.1);
}
.terminal-button.shop-btn-add:hover,
.terminal-button.shop-btn-add:focus {
  background: rgba(88, 166, 255, 0.24);
  border-color: rgba(88, 166, 255, 0.8);
}
.shop-btn-buy {
  border-color: rgba(240, 180, 41, 0.5);
  background: rgba(240, 180, 41, 0.1);
  color: #f0b429;
}
.terminal-button.shop-btn-buy:hover,
.terminal-button.shop-btn-buy:focus {
  background: rgba(240, 180, 41, 0.24);
  border-color: rgba(240, 180, 41, 0.8);
}
/* Pink/magenta, distinct from Add (blue) and Buy (gold) -- deliberately
   reads as a secondary, celebratory action rather than competing with Buy
   Now for primary-CTA visual weight. Only rendered while the stream is
   live (see is_live in core/context_processors.py). */
.shop-btn-gift {
  border-color: rgba(232, 121, 249, 0.5);
  background: rgba(232, 121, 249, 0.1);
  color: #e879f9;
}
.terminal-button.shop-btn-gift:hover,
.terminal-button.shop-btn-gift:focus {
  background: rgba(232, 121, 249, 0.24);
  border-color: rgba(232, 121, 249, 0.8);
}
.shop-card-add-btn-confirmed {
  border-color: #7ee787;
  color: #7ee787;
  background: rgba(126, 231, 135, 0.1);
}
/* Add to Cart and Buy Now are equal-width (see min-width: 0 above), so
   "Add to cart" -- the longest of the three label states this button
   ever shows ("Add to cart" / "Added!" / "Try again") -- may need to
   wrap to 2 lines at that narrower width, while the shorter "Added!"
   fits on 1. Left alone, that swap would change the button's height
   for the ~1.2s "Added!" is shown, jumping the row. A hidden ghost
   holding the always-longest text, stacked in the same grid cell as
   the real (visible) label, keeps the grid row's own height pinned to
   whatever "Add to cart" needs (the tallest of the three states)
   regardless of which one is actually showing -- and
   .shop-card-actions-row/.shop-card-add-form's default flex
   align-items: stretch then keeps Buy Now matching that same height,
   so the two buttons stay equal on both axes throughout. */
.shop-card-add-label-wrap {
  display: grid;
  width: 100%;
}
.shop-card-add-label-wrap > * {
  grid-area: 1 / 1;
}
.shop-card-add-label-ghost {
  visibility: hidden;
}
/* Confirmed/sold-out states just swap the label text, grid-stacked
   under the same "Add to cart" ghost above so the button's own size
   never changes regardless of which of the three is showing. The heart
   badge for the confirmed state lives in the icon slot instead (see
   .shop-card-added-heart-icon below), not in here -- keeping this to
   plain text avoids any height mismatch between this page's wide
   button (where "Add to cart" never needs to wrap) and the narrower
   shop listing card (where it can). */
.shop-card-add-label,
.shop-card-add-label-confirmed,
.shop-card-add-label-soldout {
  display: none;
}
/* flex + centered, not block -- each of these sits in a grid cell
   that's stretched to match the ghost's own height (2 lines' worth on
   the narrower shop listing card, where "Add to cart" wraps), so a
   shorter single line of text left as plain block content sat at the
   top of that stretched box instead of centered in it -- visibly
   higher than the heart icon, which the button's own flex
   align-items: center keeps centered on the button as a whole. */
.shop-card-add-btn:not(.shop-card-add-btn-confirmed):not(.shop-card-add-btn-soldout) .shop-card-add-label {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}
.shop-card-add-btn-confirmed .shop-card-add-label-confirmed {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}
/* Picking a specific color/size combo that's out of stock (checked via
   the same per-variant in_stock data the product detail page already
   uses, see shop.html's updateStockState()) swaps to this instead of
   leaving "Add to cart" showing on a button that's actually disabled --
   still grid-stacked under the "Add to cart" ghost so it can't grow the
   button past its normal size either. */
.shop-card-add-btn-soldout .shop-card-add-label-soldout {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

/* Deliberately compact/single-line — this is a listing card, not the
   product detail page's own roomier size picker. overflow-x lets a
   product with many sizes (S..4XL) scroll horizontally within the one
   line instead of wrapping to a second, rather than growing the card. */
.shop-card-size-row {
  display: flex;
  flex-wrap: nowrap;
  gap: 4px;
  overflow-x: auto;
  padding: 2px;
  border-radius: 6px;
  transition: box-shadow 120ms ease-out;
}
.shop-card-size-btn {
  flex-shrink: 0;
  min-width: 26px;
  padding: 2px 6px;
  font-size: 0.7rem;
  border-radius: 6px;
  border: 1px solid rgba(110, 118, 129, 0.3);
  background: transparent;
  color: #c9d1d9;
  cursor: pointer;
  position: relative;
}
.shop-card-size-btn.selected {
  background: #7ee787;
  color: #0d1117;
  border-color: #7ee787;
}
/* Out-of-stock color/size combos (checked live against the same
   variant_map data the Add/Buy buttons themselves use, see
   shop.html/product_detail.html's updateAvailabilityUI()) are disabled
   outright -- not selectable at all -- and marked with a single 1px
   diagonal red line corner-to-corner (top-right to bottom-left).
   linear-gradient(to bottom left, ...) resolves its own angle from the
   element's actual aspect ratio, so this draws a true corner-to-corner
   line on both the circular swatches and the rectangular size buttons
   without needing a separate angle per shape. Shared by both the card
   grid and the product detail page's own pickers. */
.shop-card-swatch:disabled,
.shop-card-size-btn:disabled,
.shop-swatch:disabled,
.shop-size-button:disabled {
  cursor: not-allowed;
  opacity: 0.45;
}
.shop-card-swatch:disabled::after,
.shop-card-size-btn:disabled::after,
.shop-swatch:disabled::after,
.shop-size-button:disabled::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    to bottom left,
    transparent calc(50% - 1px),
    #f85149 calc(50% - 0.5px),
    #f85149 calc(50% + 0.5px),
    transparent calc(50% + 1px)
  );
}
.shop-card-size-row-missing,
.shop-picker-row-missing {
  animation: shop-card-size-missing-flash 550ms ease-out;
  box-shadow: 0 0 0 2px rgba(248, 81, 73, 0.5);
}
@keyframes shop-card-size-missing-flash {
  0%, 100% { box-shadow: 0 0 0 2px rgba(248, 81, 73, 0.5); }
  50% { box-shadow: 0 0 0 2px rgba(248, 81, 73, 0.9); }
}
.shop-sold-out {
  color: #f85149;
  font-weight: 600;
  text-align: center;
}

/* Image left, title/price/description/variant picker right — matches
   shop.reydempto.com's own product page. Single grid rather than nested
   flex boxes so the two columns naturally stay top-aligned and collapse
   to one column on narrow viewports without extra media-query rules.
   Deliberately no align-items: start override (grid's own default is
   stretch) -- .shop-product-media needs to stretch to the row's full
   height (matching the often-taller info column) so the sticky image
   inside it has room to stay pinned through the whole info column's
   scroll, not just its own shorter image height. */
.shop-product-layout {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: 40px;
}
.shop-product-media {
  /* Tall plain wrapper only -- stretches to the grid row's full height
     (the parent no longer overrides align-items) but is NOT itself
     position: sticky. Confirmed empirically that Chromium won't reliably
     stick a grid item that's also being stretched via align-items:
     stretch -- it just sits static, defeating the whole point of
     stretching it in the first place. .shop-product-media-sticky below
     is the actual sticky element: naturally sized (not stretched)
     within this tall parent, which sticky positioning handles fine. */
}
.shop-product-media-sticky {
  position: sticky;
  top: 20px;
}
.shop-product-image {
  /* max-height caps how tall the image can render -- confirmed on the
     real reybo-coffee-mug product that an image taller than the
     viewport (was rendering at ~876px against an ~800px viewport) can
     never stay fully stuck at top: 20px AND have the row's true bottom
     reach the viewport bottom at the same time, no matter how much
     slack .shop-product-media's own height gives it -- that's a hard
     geometric ceiling on position: sticky, not something more wrapper
     tuning can work around. width: auto + max-width/max-height (rather
     than width: 100%) scales the image down proportionally instead of
     distorting it once max-height is the binding constraint. */
  display: block;
  width: auto;
  max-width: 100%;
  max-height: calc(100vh - 80px);
  margin: 0 auto;
  border-radius: 8px;
}
.shop-product-info {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
/* #shop-variant-picker's own field-rows (color/quantity/size) had no
   gap between them at all -- .shop-product-info's own 12px gap only
   applies between ITS direct children, and the picker is a single one
   of those wrapping all three rows, so they stacked with zero space
   between. */
#shop-variant-picker {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
/* Quantity stepper reused verbatim from the header mini-cart/cart page
   (.header-cart-qty-adjust etc.) for visual consistency, but at the
   small size that makes sense in a dropdown -- doubled here, scoped to
   this page only, so the dropdown/cart page stay untouched. */
.shop-product-info .header-cart-qty-adjust {
  gap: 6px;
}
.shop-product-info .header-cart-qty-btn {
  width: 27px;
  height: 27px;
  font-size: 1.125rem;
}
.shop-product-info .header-cart-qty-value {
  min-width: 21px;
  font-size: 1.2rem;
}
.shop-product-title {
  margin: 0;
  font-size: 2.4rem;
  font-weight: 800;
  line-height: 1.2;
  color: #ffffff;
}
.shop-product-base-price {
  font-size: 1.3rem;
  font-weight: 600;
  color: #7ee787;
}
.shop-product-description {
  /* Same body-copy font as blog posts (.post-body/.terminal-article in
     site.css) -- everything else on this page is the terminal
     monospace font, but a paragraph of prose reads better in this. */
  font-family: "Inter", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  line-height: 1.8;
  color: #e8ebed;
}
.shop-product-actions {
  display: flex;
  gap: 8px;
}
.shop-product-add-form,
.shop-product-buy-form {
  flex: 1;
  display: flex;
}
.shop-product-add-form .terminal-button,
.shop-product-buy-form .terminal-button {
  width: 100%;
}
/* .shop-product-gift-link is a plain <a> (not a <form>+<button>), so it
   needs the same flex-basis/full-width treatment applied directly rather
   than via a form wrapper. */
.shop-product-gift-link {
  flex: 1;
  width: 100%;
}
@media (max-width: 820px) {
  .shop-product-layout {
    grid-template-columns: 1fr;
  }
  .shop-product-media-sticky {
    position: static;
  }
}
.shop-swatch-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  /* fit-content, not the full info-column width -- otherwise the
     .shop-picker-row-missing flash (below) drew a hard-edged rectangle
     spanning the whole column instead of snugly wrapping the swatches,
     unlike the shop listing card's own narrower row. Padding + radius
     match that card's .shop-card-size-row for the same "wraps around
     it nicely" look, just sized up a bit for this page's bigger swatches. */
  width: fit-content;
  padding: 4px;
  border-radius: 10px;
  transition: box-shadow 120ms ease-out;
}
.shop-swatch {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 2px solid rgba(110, 118, 129, 0.3);
  cursor: pointer;
  padding: 0;
  position: relative;
}
.shop-swatch.selected {
  border-color: #7ee787;
  box-shadow: 0 0 0 2px rgba(126, 231, 135, 0.3);
}
.shop-size-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  width: fit-content;
  padding: 4px;
  border-radius: 10px;
  transition: box-shadow 120ms ease-out;
}
.shop-size-button {
  position: relative;
}
.shop-size-button.selected {
  background: #7ee787;
  color: #0d1117;
  border-color: #7ee787;
}
/* /shop/cart/ — deliberately built as "the header mini-cart dropdown,
   just wider": rows reuse the exact same .header-cart-qty-adjust/
   .header-cart-qty-btn/.header-cart-qty-value/.header-cart-remove-btn/
   .header-cart-remove-icon classes the dropdown itself uses (defined
   above), rather than a parallel re-implementation that could drift
   from it visually over time. Only the parts that actually differ for
   a full page (bigger thumbnail, no width-driven ellipsis/truncation
   concerns) get their own rules here. */
.shop-cart-rows {
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.shop-cart-row {
  display: flex;
  align-items: center;
  gap: 10px;
  border: 1px solid rgba(110, 118, 129, 0.16);
  border-radius: 10px;
  padding: 12px;
}
.shop-cart-row-image {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: 6px;
  flex-shrink: 0;
}
.shop-cart-row-info {
  flex: 1;
  min-width: 0;
}
.shop-cart-row-side {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-shrink: 0;
}
.shop-cart-row-price {
  font-weight: 600;
  white-space: nowrap;
  color: #ffffff;
}
.shop-cart-qty-form {
  display: inline-flex;
}
.shop-cart-totals {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  margin: 16px 0;
  /* Same 38px = .header-cart-remove-btn's 28px width + .shop-cart-row's
     10px gap offset the dropdown's own totals use, so "Total" lines up
     under the itemized prices above it instead of under the trash
     icons -- same reasoning, same row gap, so the same number works. */
  padding-right: 38px;
  font-size: 1.1rem;
  font-weight: 600;
}
.shop-cart-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  /* Same 38px offset as .shop-cart-totals above -- so Checkout's own
     right edge lines up with the total's right edge, not the row's
     true (trash-icon-inclusive) right edge. */
  padding-right: 38px;
}

/* ---- Fourthwall checkout popup overlay -- shown across all shop pages
   while window.openFourthwallCheckoutPopup()'s popup is open, mirroring
   the PayPal tip flow's .asset-modal overlay (see tipping.css). Not
   reusing .asset-modal itself since core.css (which defines its
   --narrow/--padded modifiers) isn't loaded on shop pages -- kept
   self-contained here instead. ---- */
.shop-checkout-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: none;
  visibility: hidden;
  opacity: 0;
  align-items: center;
  justify-content: center;
  z-index: 11000;
  pointer-events: none;
  transition: opacity 180ms ease, visibility 180ms ease;
}
.shop-checkout-overlay.open {
  display: flex;
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
}
.shop-checkout-overlay-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(10, 14, 20, 0.88);
  backdrop-filter: blur(8px);
}
.shop-checkout-overlay-panel {
  position: relative;
  width: 360px;
  max-width: calc(100% - 32px);
  background: rgba(15, 20, 28, 0.96);
  border: 1px solid rgba(110, 118, 129, 0.22);
  border-radius: 18px;
  box-shadow: 0 40px 90px rgba(0, 0, 0, 0.45);
  padding: 24px;
  text-align: center;
  z-index: 11001;
}
.shop-checkout-overlay-icon {
  width: 56px;
  height: auto;
  margin-bottom: 10px;
  image-rendering: pixelated;
}
.shop-checkout-overlay-title {
  font-weight: 700;
  margin-bottom: 8px;
}
.shop-checkout-overlay-subtitle {
  font-size: 0.85rem;
  color: #8b949e;
  margin-bottom: 16px;
}

/* ---- Gift widget modal: holds the same-origin iframe pointed at
   shop:gift_widget, which isolates Fourthwall's "Gift to Twitch Chat"
   block from the rest of their product page -- see
   shop/services.py::fetch_gift_widget_html. Same fixed-overlay/backdrop
   shape as .shop-checkout-overlay above, just with a close button and an
   iframe instead of a status message. ---- */
.gift-widget-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: none;
  visibility: hidden;
  opacity: 0;
  align-items: center;
  justify-content: center;
  z-index: 11000;
  pointer-events: none;
  transition: opacity 180ms ease, visibility 180ms ease;
}
.gift-widget-modal.open {
  display: flex;
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
}
.gift-widget-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(10, 14, 20, 0.88);
  backdrop-filter: blur(8px);
}
.gift-widget-modal-panel {
  /* Sized to hug the iframe's actual content height (set in JS, see
     base.html's giftWidgetFrame load handler -- it's same-origin, so
     the real content height is readable) rather than a fixed guess:
     the checkout step now always breaks out to a normal top-level page
     (see shop/views.py::gift_widget's docstring for why proxying it
     isn't viable), so this modal only ever needs to fit the small,
     fairly consistent isolated product-info__gift block. */
  position: relative;
  width: 460px;
  max-width: calc(100% - 32px);
  max-height: calc(100% - 32px);
  background: rgba(15, 20, 28, 0.96);
  border: 1px solid rgba(232, 121, 249, 0.35);
  border-radius: 18px;
  box-shadow: 0 40px 90px rgba(0, 0, 0, 0.45);
  z-index: 11001;
  overflow-y: auto;
  overflow-x: hidden;
}
.gift-widget-modal-close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(110, 118, 129, 0.3);
  border-radius: 50%;
  background: rgba(15, 20, 28, 0.85);
  color: #dfe7f2;
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  z-index: 11002;
}
.gift-widget-modal-close:hover {
  background: rgba(232, 121, 249, 0.18);
  border-color: rgba(232, 121, 249, 0.6);
}
.gift-widget-frame {
  display: block;
  width: 100%;
  /* Default before the load handler measures and sets a real px height
     -- see the comment on .gift-widget-modal-panel above. */
  height: 280px;
  border: 0;
  background: #fff;
}
