/* ============================================================
   CAPELY: shop grid (sections/03-shop.html, id="capes")

   Every colour below is a var(--...) token from css/theme.css.
   Cards are assembled by js/03-shop.js from CAPELY.products() and
   GET /api/stock. No cape name, price, rarity or texture path is
   written in this file or in the section markup.

   No cape's `accent` is used on this card at all any more. The light
   coming off a cape is made of that cape's own pixels (see
   .cape-card-bloom), which is both more honest to the artwork and
   physically what a glow is. A flat accent halo was tried and the
   owner rejected it: it spilled into the rarity badge and the stock
   chip instead of reading as the cape glowing.

   Rarity tags use css/theme.css's --rarity-* tokens (grey/green/blue/
   purple, the Fortnite-style ladder). js/03-shop.js maps the exact
   data/products.json "rarity" string to one of four modifier classes
   below; nothing here guesses a colour for a tier that is not in the
   data.
   ============================================================ */

/* ---------- grid ----------
   9 capes divide evenly into 3 columns, which is also what keeps each
   card wide enough to read as roomy rather than shoved together. 2
   columns from 900px, 1 column (full-width card) from 520px down to
   320px, since a cramped 2-up card is exactly the "shoved together"
   look this redesign is fixing. */
.shop-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gutter);
}
@media (max-width: 900px) {
  .shop-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
  .shop-grid { grid-template-columns: 1fr; gap: 14px; }
}

/* ---------- card ----------
   Background/border/radius/hover-lift come from the shared .card and
   .card-hover classes (site.css); this file only adds the internal
   layout. overflow:hidden keeps the thumbnail glow and the rarity
   badge inside the card's rounded corners. */
.cape-card {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ---------- hover: the card lights up, the cape leans in ----------
   The shared .card-hover (site.css) only lifts the box and firms its border,
   which the owner found too quiet to tell which cape the cursor is on. This
   adds the two things that read at a glance:

   1. AN ACCENT EDGE AND A MINT BLOOM under the card, both from --accent-line
      and --glow-mint in theme.css. It is a box-shadow on the card itself, so
      the card's own overflow: hidden cannot clip it, and no halo element is
      reintroduced inside the stage (see the note above .cape-card-canvas -
      a halo in there is what used to bleed into the rarity badge).

   2. THE CAPE ITSELF TILTS AND RISES, as if picked up off the shelf. It is
      the cape that is the product, so the cape is what answers the cursor.
      The bloom copy has to repeat the -50%/-50% centring translate before its
      own rotation or it flies out of the stage, and it leans a fraction
      further and scales a fraction wider than the crisp canvas so the light
      stays behind the artwork at every point of the movement.

   The transition here restates .card-hover's transform and border-color as
   well as box-shadow: this rule is later in the cascade and at the same
   specificity, so a box-shadow-only transition would silently drop the lift.

   --t-slow on the artwork against --t on the card is deliberate. The card
   answers immediately; the cape keeps moving for a moment after it, which is
   what makes the movement read as weight rather than a snap. */
.cape-card {
  transition:
    transform var(--t) var(--e-out),
    border-color var(--t) var(--e-out),
    box-shadow var(--t) var(--e-out);
}
.cape-card:hover {
  border-color: var(--accent-line);
  box-shadow: 0 22px 56px -34px var(--glow-mint);
}

.cape-card-canvas,
.cape-card-bloom {
  transition:
    transform var(--t-slow) var(--e-out),
    opacity var(--t) var(--e-out);
}
.cape-card:hover .cape-card-canvas {
  transform: translateY(-6px) rotate(-4deg) scale(1.06);
}
.cape-card:hover .cape-card-bloom {
  transform: translate(-50%, -50%) translateY(-6px) rotate(-5deg) scale(1.12);
  opacity: 1;
}

/* Keyboard gets the same state, but ONLY for a keyboard.

   This used to key off focus-within, and it left a card lit after the visitor
   closed the zoom: the dialog correctly returns focus to the button inside the
   card it came from, so the card stayed highlighted until something else was
   clicked (owner, 2026-08-31). :has(:focus-visible) is the fix - focus restored
   after a mouse click is focus, but it is not focus-VISIBLE, so a pointer user
   sees nothing and a keyboard user still gets the affordance.

   Kept as its own rule rather than appended to the :hover selector list above:
   one unsupported selector invalidates a whole list, so a browser without
   :has() would lose the hover state entirely instead of just this. */
.cape-card:has(:focus-visible) {
  border-color: var(--accent-line);
  box-shadow: 0 22px 56px -34px var(--glow-mint);
}
.cape-card:has(:focus-visible) .cape-card-canvas {
  transform: translateY(-6px) rotate(-4deg) scale(1.06);
}
.cape-card:has(:focus-visible) .cape-card-bloom {
  transform: translate(-50%, -50%) translateY(-6px) rotate(-5deg) scale(1.12);
  opacity: 1;
}

/* Touch has no hover. Leaving the tilt on a :hover that a phone latches after
   a tap would strand a cape mid-lean until the visitor tapped elsewhere. */
@media (hover: none) {
  .cape-card:hover .cape-card-canvas,
  .cape-card:hover .cape-card-bloom { transform: none; }
  .cape-card:hover .cape-card-bloom { transform: translate(-50%, -50%); }
}
@media (prefers-reduced-motion: reduce) {
  .cape-card-canvas,
  .cape-card-bloom { transition: none; }
  .cape-card:hover .cape-card-canvas { transform: none; }
  .cape-card:hover .cape-card-bloom { transform: translate(-50%, -50%); }
  .cape-card:has(:focus-visible) .cape-card-canvas { transform: none; }
  .cape-card:has(:focus-visible) .cape-card-bloom { transform: translate(-50%, -50%); }
}

/* ---------- thumbnail stage ----------
   A big, roomy panel across the full card width, the way the
   reference's product photo sits at the top of its card. Capely's own
   cape presentation (2D pixel-art crop, soft accent glow) is kept
   exactly as it was; only its stage got bigger and gained a corner
   for the rarity badge. No flat colour or photo backdrop is added
   behind it. */
/* Two rows in normal flow: a chip row, then the artwork. NOT a single box with
   absolutely positioned chips over a hand-tuned top padding, which is what it
   was, and which put the rarity badge on top of the cape twice. A padding is a
   guess about how tall the chips are and how big the cape is at that card
   width; a flex row is a fact. */
.cape-card-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  /* Contains .cape-card-bloom's blend to this stage. Without it the blend
     reaches through the card to the page behind it. */
  isolation: isolate;
}

.cape-card-chips {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  padding: 16px 16px 0;
  /* Reserved even when a cape has no stock chip, so every cape in the grid
     sits on the same line. */
  min-height: 41px;
}

/* A real button: the zoom is keyboard reachable and announced. The 20px of
   padding above the cape is breathing room, not separation, because the chip
   row above already guarantees the separation. */
.cape-card-art {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: 20px 0 22px;
  cursor: zoom-in;
  background: none;
  border: 0;
}
/* There is deliberately no wide accent halo behind the cape any more. A
   62%-of-the-card radial blurred by 30px reached the rarity badge and the
   stock chip, which is exactly the "glow spilling into the ultra rare and
   quantity sections" the owner reported. What a cape is meant to do here is
   glow, not sit in a puddle of its own colour, so the only light on this card
   now comes from .cape-card-bloom below, which is made of the cape's own
   pixels and stays on the cape. Do not reintroduce a halo element. */
/* 2D canvas crop of the cape sheet, drawn at a whole multiple of the
   10x16 source region (js/03-shop.js) and kept blocky here rather than
   smoothed, which is what actually makes it read as crisp pixel art. */
.cape-card-canvas {
  position: relative;
  z-index: 1;
  width: 38%;
  min-width: 92px;
  max-width: 128px;
  aspect-ratio: 10 / 16;
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
}

/* The cape's own light: the same crop painted again (js/03-shop.js), blurred,
   and screened back OVER the crisp one rather than spread behind it.

   Three things make this read as the cape glowing rather than a glow around
   the cape:

   1. mix-blend-mode: screen. The old version composited normally, so the
      cape's dark pixels painted a dark blurred rectangle around it and its
      bright pixels smeared a coloured cloud outward. Screened, dark pixels
      contribute nothing at all and only the bright ones add light, which is
      what a bloom actually is.
   2. A 6px blur at the same size, not 9px at 105%. The light bleeds a few
      pixels off the artwork instead of tens, so it comes nowhere near the
      rarity badge or the stock chip in the corners above.
   3. It stays BEHIND the crisp canvas. Screening it over the top does make
      the cape itself brighter, but a blurred copy laid over pixel art softens
      every edge, and crisp blocky edges are the whole point of the thumbnail.
      Behind it, the cape is untouched and the light is unmistakably its own.

   Still deliberately not a drop-shadow: that tints only the silhouette and
   therefore always reads as an outline traced around the edge. */
.cape-card-bloom {
  position: absolute;
  z-index: 0;
  top: 50%;
  left: 50%;
  width: 38%;
  min-width: 92px;
  max-width: 128px;
  aspect-ratio: 10 / 16;
  transform: translate(-50%, -50%);
  filter: blur(6px) saturate(1.4);
  opacity: .85;
  mix-blend-mode: screen;
  pointer-events: none;
}

/* ---------- rarity badge ----------
   Sits in the stage's top-left corner, like the merchandising pill on
   the reference cards, but carrying real product metadata (rarity)
   instead of an unverifiable claim. Bigger and bolder than the old
   inline chip on purpose: this is one of the two things the owner
   named directly as "too small." Base rule is a neutral fallback; the
   four modifier classes below are the only source of colour and each
   one is a straight token triad from theme.css. */
.cape-card-rarity {
  display: inline-flex;
  align-items: center;
  padding: 6px 14px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  color: var(--text-dim);
  font-family: 'Outfit', sans-serif;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.cape-card-rarity.rarity-common {
  color: var(--rarity-common-ink);
  background: var(--rarity-common-bg);
  border-color: var(--rarity-common-line);
}
.cape-card-rarity.rarity-uncommon {
  color: var(--rarity-uncommon-ink);
  background: var(--rarity-uncommon-bg);
  border-color: var(--rarity-uncommon-line);
}
.cape-card-rarity.rarity-rare {
  color: var(--rarity-rare-ink);
  background: var(--rarity-rare-bg);
  border-color: var(--rarity-rare-line);
}
.cape-card-rarity.rarity-ultra {
  color: var(--rarity-ultra-ink);
  background: var(--rarity-ultra-bg);
  border-color: var(--rarity-ultra-line);
}

/* ---------- body ---------- */
.cape-card-body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  padding: 0 22px 26px;
}

.cape-card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  row-gap: 6px;
  margin-bottom: 10px;
}
.cape-card-name {
  font-size: 19px;
  font-weight: 700;
  line-height: 1.25;
  overflow-wrap: break-word;
  min-width: 0;
}
/* The name is the way through to the cape's own page. Underlined on hover
   rather than coloured, so the card's only accent stays the buy button and
   the heading does not read as a second call to action. */
.cape-card-name-link {
  text-decoration: none;
  text-underline-offset: 3px;
  transition: color var(--t-fast);
}
.cape-card-name-link:hover {
  color: var(--accent);
  text-decoration: underline;
}
/* The whole card lifts on hover (site.css .card-hover); the artwork answers
   with its zoom affordance so the two read as one surface. */
.cape-card-zoom {
  position: absolute;
  z-index: 2;
  right: 14px;
  bottom: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  color: var(--text-mute);
  opacity: 0;
  transform: scale(.85);
  transition: opacity var(--t) var(--e-out), transform var(--t) var(--e-out), color var(--t-fast);
  pointer-events: none;
}
.cape-card-zoom svg { width: 15px; height: 15px; }
.cape-card:hover .cape-card-zoom,
.cape-card-art:focus-visible .cape-card-zoom {
  opacity: 1;
  transform: none;
}
.cape-card-art:hover .cape-card-zoom { color: var(--accent); }
/* Touch has no hover, so the affordance would never appear. */
@media (hover: none) {
  .cape-card-zoom { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .cape-card-zoom { transition: opacity var(--t-fast); transform: none; }
}

.cape-card-blurb {
  color: var(--text-dim);
  font-size: 13.5px;
  line-height: 1.6;
  margin-bottom: 8px;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* ---------- details: inline expand ----------
   Keyboard operable by being a real <button>; no modal, no focus trap
   to get wrong. Only opacity/transform animate on open, never height.
   Kept deliberately quiet (small, muted) so the cleaned-up card reads
   name -> description -> price first, with the extra lore a click away
   rather than competing with it. */
/* The add-to-basket button swaps its trolley for a tick for a moment after a
   press. Two icons in the button and a class flip, not innerHTML surgery: the
   button keeps its focus and its accessible name is updated separately. */
.cape-card-cart .cape-cart-tick { display: none; }
.cape-card-cart.is-added .cape-cart-ic { display: none; }
.cape-card-cart.is-added .cape-cart-tick { display: block; }
.cape-card-cart .cape-cart-tick { width: 17px; height: 17px; }

.cape-detail-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  align-self: flex-start;
  color: var(--text-mute);
  font-size: 12.5px;
  font-weight: 600;
  padding: 4px 0;
  margin-bottom: 6px;
  transition: color var(--t-fast);
}
.cape-detail-toggle:hover { color: var(--accent); }
.cape-detail-ic {
  width: 12px;
  height: 12px;
  transition: transform var(--t) var(--e-out);
}
.cape-detail-toggle[aria-expanded="true"] .cape-detail-ic { transform: rotate(180deg); }

/* Opens by interpolating its own height: grid-template-rows 0fr -> 1fr, the
   same technique the FAQ accordion in site.css now uses.

   THE CLIPPED-TEXT BUG LIVED HERE. Before this, the panel eased a max-height
   pinned from detail.scrollHeight, read in the same click handler that had
   just added .is-open - so the measurement was taken while padding-top was
   still animating in from 0, and it came back short. The last line of any
   detail long enough to matter was cut off, permanently, because the pinned
   value stayed on the element. A stale measurement also survived a resize or
   a font swap. An fr row has nothing to measure and cannot be short.

   It was display:none plus a fade before that, which made the card below it
   snap. visibility keeps the collapsed text out of the tab order and the
   accessibility tree without display:none killing the transition, and
   .cape-detail-inner needs min-height: 0 or the grid item refuses to shrink
   below its own content. */
.cape-detail {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  opacity: 0;
  margin: 0;
  color: var(--text-dim);
  font-size: 12.5px;
  line-height: 1.65;
  transition:
    grid-template-rows var(--t) var(--e-out),
    opacity var(--t) var(--e-out),
    margin-bottom var(--t) var(--e-out),
    visibility 0s linear var(--t);
}
.cape-detail-inner {
  min-height: 0;
  overflow: hidden;
  padding-top: 0;
  border-top: 1px solid transparent;
  transition:
    padding-top var(--t) var(--e-out),
    border-top-color var(--t) var(--e-out);
}
.cape-detail.is-open {
  visibility: visible;
  opacity: 1;
  margin-bottom: 12px;
  grid-template-rows: 1fr;
  transition-delay: 0s;
}
.cape-detail.is-open .cape-detail-inner {
  padding-top: 10px;
  border-top-color: var(--hairline);
}
@media (prefers-reduced-motion: reduce) {
  .cape-detail,
  .cape-detail-inner { transition: none; }
}
@media (prefers-reduced-motion: reduce) {
  .cape-detail.is-in { animation: none; }
}

/* ---------- foot: price + circular add-to-cart ----------
   Mirrors the reference's bottom row exactly: price large and bold on
   the left, a round icon-only cart button on the right. Stock is not
   repeated here; it already showed, honestly, up in the name row. */
.cape-card-foot {
  margin-top: auto;
  padding-top: 12px;
}
.cape-card-price-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}
.cape-card-price {
  font-family: 'Outfit', sans-serif;
  font-weight: 700;
  font-size: 23px;
  letter-spacing: -.01em;
  color: var(--text);
}

/* Stock chip: only rendered by the JS when stock is low or zero, so its
   mere presence already means "pay attention," colour is reinforcement
   on top of that, never the only signal. The tint is theme's own --warn
   / --danger token mixed toward transparent, the same pattern site.css
   already uses for bg-grid/bg-glow, never an invented colour. */
/* The other end of .cape-card-chips, mirroring the rarity badge. Both are laid
   out in flow above the cape, so neither can reach it. */
.cape-card-stock {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: var(--r-pill);
  font-family: 'Outfit', sans-serif;
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .02em;
  white-space: nowrap;
  flex: 0 0 auto;
}
.cape-card-stock-warn {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 32%, transparent);
}
.cape-card-stock-danger {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--danger) 32%, transparent);
}
.cape-stock-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

/* .btn/.btn-primary visual identity (fill, hover, disabled state) is
   untouched; only shape (round, icon-only) is added here, the same way
   .cape-card-buy.btn used to just adjust width/padding. Compound
   selector keeps this winning over .btn regardless of stylesheet
   order. */
.cape-card-cart.btn {
  width: 46px;
  height: 46px;
  padding: 0;
  border-radius: var(--r-pill);
  flex: 0 0 auto;
}
.cape-cart-ic {
  width: 19px;
  height: 19px;
}

/* ---------- skeleton (loading state) ---------- */
.cape-card-skeleton { display: flex; flex-direction: column; }
/* Both sit in the same flow slots the real card uses, so the grid does not
   resize the moment the catalogue lands. */
.cape-card-skel-badge {
  width: 74px;
  height: 25px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  animation: cape-skel-pulse 1.4s ease-in-out infinite;
}
.cape-card-skel-thumb {
  width: 38%;
  min-width: 92px;
  max-width: 128px;
  aspect-ratio: 10 / 16;
  background: var(--surface-2);
  border-radius: var(--r-xs);
  animation: cape-skel-pulse 1.4s ease-in-out infinite;
}
.cape-card-skel-line {
  height: 11px;
  margin: 0 22px 10px;
  border-radius: var(--r-xs);
  background: var(--surface-2);
  animation: cape-skel-pulse 1.4s ease-in-out infinite;
}
.cape-card-skel-line-lg { height: 19px; width: calc(100% - 88px); margin-top: 2px; }
.cape-card-skel-line-sm { width: calc(100% - 150px); }
.cape-card-skel-line-md { width: calc(100% - 110px); margin-bottom: 20px; }
.cape-card-skel-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  margin: 20px 22px 22px;
}
.cape-card-skel-price {
  height: 23px;
  width: 40%;
  border-radius: var(--r-xs);
  background: var(--surface-2);
  animation: cape-skel-pulse 1.4s ease-in-out infinite;
}
.cape-card-skel-cart {
  width: 46px;
  height: 46px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  animation: cape-skel-pulse 1.4s ease-in-out infinite;
  flex: 0 0 auto;
}
@keyframes cape-skel-pulse {
  0%, 100% { opacity: .5; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .cape-card-skel-thumb, .cape-card-skel-line, .cape-card-skel-badge,
  .cape-card-skel-price, .cape-card-skel-cart { animation: none; opacity: .7; }
}

/* ---------- error state ---------- */
.shop-error {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 48px 24px;
  text-align: center;
}
.shop-error p { color: var(--text-dim); font-size: 14.5px; max-width: 46ch; }

/* ---------- phones ----------
   One column below 520px is right, but the desktop card's proportions made
   each one about 460px tall, so nine of them were 4100px of scrolling on a
   390px screen. Everything below is the same card, tightened: a smaller cape,
   a shallower reserved band above it, and text one step down. Nothing is
   hidden and nothing is moved, so the card reads identically, just compact. */
@media (max-width: 520px) {
  .cape-card-chips { padding: 13px 13px 0; min-height: 36px; }
  .cape-card-art { padding: 14px 0 16px; }
  .cape-card-canvas,
  .cape-card-bloom {
    width: 30%;
    min-width: 84px;
    max-width: 104px;
  }
  .cape-card-rarity { padding: 5px 11px; font-size: 11px; }
  .cape-card-stock { padding: 4px 10px; font-size: 11px; }
  .cape-card-body { padding: 0 18px 20px; }
  .cape-card-name { font-size: 17px; }
  .cape-card-blurb { font-size: 13px; margin-bottom: 6px; }
  .cape-card-price { font-size: 21px; }
  .cape-card-cart.btn { width: 42px; height: 42px; }

  /* The skeleton has to match, or the grid visibly resizes the moment the
     catalogue arrives. */
  .cape-card-skel-badge { width: 68px; height: 23px; }
  .cape-card-skel-thumb {
    width: 30%;
    min-width: 84px;
    max-width: 104px;
  }
  .cape-card-skel-line { margin: 0 18px 9px; }
  .cape-card-skel-foot { margin: 16px 18px 18px; }
  .cape-card-skel-cart { width: 42px; height: 42px; }
}

/* ============================================================
   CAPE ZOOM (js/modules/cape-zoom.js)

   Clicking a cape grows it out of its own thumbnail into a large view. The
   move is a FLIP: the dialog's artwork is transformed back onto the card's
   rectangle for one frame and released, so nothing here animates a width or a
   height. Only opacity and transform, per DESIGN-CONTRACT.md 4.
   ============================================================ */
.cape-zoom-overlay {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--gutter);
  background: color-mix(in srgb, var(--base-deep) 82%, transparent);
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  opacity: 0;
  transition: opacity var(--t) var(--e-out);
}
.cape-zoom-overlay.is-open { opacity: 1; }

/* THE bug this dialog shipped with, and it broke the entire site.
   `display: flex` above is an author declaration, so it outranks the UA
   stylesheet's `[hidden] { display: none }`. Setting overlay.hidden on close
   therefore did nothing: a full viewport, position:fixed, z-index 1200 layer
   stayed over every page at opacity 0, swallowing every click on the site
   until a reload. Nothing was clickable, and no other cape could be zoomed,
   because the clicks never reached the cards.

   Any element toggled by the `hidden` attribute needs this rule whenever its
   own stylesheet sets a display. The second line is belt and braces: even if
   something ever leaves the overlay displayed, it cannot intercept a pointer
   unless it is genuinely open. */
.cape-zoom-overlay[hidden] { display: none; }
.cape-zoom-overlay:not(.is-open) { pointer-events: none; }

/* The page behind a modal must not scroll under it. */
.has-modal, .has-modal body { overflow: hidden; }

.cape-zoom-dialog {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(20px, 4vw, 48px);
  width: 100%;
  max-width: var(--maxw-mid);
  padding: clamp(24px, 3vw, 40px);
  background: var(--card-wash), var(--surface);
  border: 1px solid var(--hairline-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lg);
  transform: translateY(10px) scale(.985);
  opacity: 0;
  transition: transform var(--t) var(--e-out), opacity var(--t) var(--e-out);
}
.cape-zoom-overlay.is-open .cape-zoom-dialog { transform: none; opacity: 1; }

.cape-zoom-art {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  isolation: isolate;
  /* The FLIP writes to this element's transform. Its origin must be the centre
     for the maths in playFrom() to line up, which is the default; do not move
     it without changing that function too. */
  transition: transform .34s var(--e-out);
  will-change: transform;
}
.cape-zoom-canvas {
  position: relative;
  z-index: 1;
  width: clamp(160px, 26vw, 260px);
  aspect-ratio: 10 / 16;
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
  border-radius: var(--r-xs);
}
/* Same bloom recipe as the card: the cape's own pixels, screened, so only
   light adds light. Wider here because there is room for it. */
.cape-zoom-bloom {
  position: absolute;
  z-index: 0;
  top: 50%;
  left: 50%;
  width: clamp(160px, 26vw, 260px);
  aspect-ratio: 10 / 16;
  transform: translate(-50%, -50%) scale(1.06);
  filter: blur(16px) saturate(1.45);
  opacity: .9;
  mix-blend-mode: screen;
  pointer-events: none;
}

.cape-zoom-info { min-width: 0; }
.cape-zoom-rarity {
  display: inline-flex;
  align-items: center;
  padding: 6px 14px;
  margin-bottom: 14px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  color: var(--text-dim);
  font-family: 'Outfit', sans-serif;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.cape-zoom-rarity.rarity-common {
  color: var(--rarity-common-ink); background: var(--rarity-common-bg); border-color: var(--rarity-common-line);
}
.cape-zoom-rarity.rarity-uncommon {
  color: var(--rarity-uncommon-ink); background: var(--rarity-uncommon-bg); border-color: var(--rarity-uncommon-line);
}
.cape-zoom-rarity.rarity-rare {
  color: var(--rarity-rare-ink); background: var(--rarity-rare-bg); border-color: var(--rarity-rare-line);
}
.cape-zoom-rarity.rarity-ultra {
  color: var(--rarity-ultra-ink); background: var(--rarity-ultra-bg); border-color: var(--rarity-ultra-line);
}
.cape-zoom-name {
  font-size: clamp(24px, 3.4vw, 34px);
  margin-bottom: 8px;
  overflow-wrap: break-word;
}
.cape-zoom-price {
  font-family: 'Outfit', sans-serif;
  font-size: clamp(22px, 2.6vw, 28px);
  font-weight: 800;
  letter-spacing: -.01em;
  color: var(--accent);
  margin-bottom: 12px;
}
.cape-zoom-blurb {
  color: var(--text-dim);
  font-size: 14.5px;
  line-height: 1.65;
  max-width: 52ch;
  margin-bottom: 22px;
}
.cape-zoom-actions { display: flex; flex-wrap: wrap; gap: 12px; }

.cape-zoom-close {
  position: absolute;
  top: 12px;
  right: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  color: var(--text-dim);
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.cape-zoom-close:hover { color: var(--text); border-color: var(--hairline-strong); background: var(--surface-3); }
.cape-zoom-close svg { width: 17px; height: 17px; }

@media (max-width: 760px) {
  .cape-zoom-dialog {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    text-align: center;
    padding-top: 58px;
  }
  .cape-zoom-blurb { margin-left: auto; margin-right: auto; }
  .cape-zoom-actions { justify-content: center; }
  .cape-zoom-canvas, .cape-zoom-bloom { width: clamp(140px, 46vw, 210px); }
}

@media (prefers-reduced-motion: reduce) {
  .cape-zoom-overlay,
  .cape-zoom-dialog,
  .cape-zoom-art { transition: none; }
}
