/*
 * MOON EXCHANGE — one listing, opened.
 *
 * ITS OWN FILE, AND LINKED FROM index.html. These rules first went into a
 * stylesheet that page does not load -- so on `/play`, which is exactly where
 * a shared link lands, `.exchange-detail` computed `position: static;
 * z-index: auto` and the dialog rendered as an unstyled block behind the game
 * board. Every DOM assertion passed: the element existed, `checkVisibility()`
 * returned true, its text was right. Only a screenshot showed the mulligan
 * where the dialog should have been.
 * Contract: tests/unit/styles/exchangeStylesheetLinked.test.js
 */
.exchange-detail {
  position: fixed;
  inset: 0;
  z-index: 99999;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  /* flex-start + card margin:auto + scroll, so the actions stay reachable on a
     short landscape phone. Identical on desktop. */
  overflow-y: auto;
  padding: max(env(safe-area-inset-top, 0px), 12px) 12px max(env(safe-area-inset-bottom, 0px), 12px);
  box-sizing: border-box;
  background: rgba(6, 3, 18, 0.78);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

/*
 * THE CLAIM TICKET — stub left, copy right.
 *
 * The stub is a physical object: warm paper, a slight rotation, a serial along
 * the bottom. The card inside it is the REAL card element, so the stub sizes
 * itself from the card rather than the other way round.
 */
.exchange-listing-overlay .exchange-detail-card {
  /* Prefixed with the overlay class ON PURPOSE. `.starter-reward-card` is
     `min(100%, 500px)` and its stylesheet loads AFTER this one, so an unprefixed
     rule of equal specificity loses the cascade — measured at 480px wide, with
     the title wrapping one letter per line. */
  /* A FULL SURFACE, NOT A POPUP. This is the same kind of arrival as the
     invite screen and was asked for as such repeatedly; a 480-760px box
     floating over a running game reads as a dialog you dismiss, not as a place
     you landed. It takes the viewport the way `starter-reward-overlay` intends,
     capped so it stays a composed panel on a wide monitor rather than a
     stretched band. */
  /* Wide enough for stub + copy side by side, tight enough that the copy is
     not stranded against a far edge — measured, the 1180 version left ~230px of
     empty panel to the right of every line. */
  width: min(100%, 940px);
  max-width: min(100%, 940px);
  min-height: min(100%, 520px);
  text-align: left;
  display: grid;
  grid-template-areas:
    "eyebrow eyebrow"
    "stage   body"
    "stage   actions";
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto 1fr auto;
  column-gap: clamp(18px, 3vw, 40px);
  row-gap: 10px;
  align-items: start;
  /* The copy sits with the card rather than drifting to the top of a tall
     panel; the actions stay on the baseline they share with its lower edge. */
  align-content: center;
  position: relative;
  box-sizing: border-box;
  margin: auto;
  padding: clamp(18px, 3vmin, 30px) clamp(18px, 3vmin, 28px);
  border-radius: 18px;
  border: 1px solid rgba(255, 215, 130, 0.32);
  background: linear-gradient(180deg, rgba(28, 14, 52, 0.96), rgba(12, 6, 26, 0.96));
  color: #eedbf1;
}

.exchange-detail-close {
  position: absolute;
  top: 8px;
  right: 8px;
  /* 44px is the touch minimum, and this is the only way out on a phone. */
  width: 44px;
  height: 44px;
  border: 0;
  background: none;
  color: #eedbf1;
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
}

.exchange-detail-name {
  margin: 0 0 10px;
  font-family: 'Spectral', Georgia, serif;
  font-size: clamp(1.15rem, 3.4vmin, 1.6rem);
}

.exchange-detail-desc {
  margin: 0 0 14px;
  color: #c3b5cc;
  font-size: 0.92rem;
  line-height: 1.45;
}

.exchange-detail-price {
  font-size: clamp(1.05rem, 3vmin, 1.35rem);
  font-weight: 700;
}

.exchange-detail-remaining {
  margin-top: 2px;
  color: #c3b5cc;
  font-size: 0.86rem;
  letter-spacing: 0.06em;
}

.exchange-detail-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 18px;
}

/*
 * EVERY control here is a touch target, whatever class it wears.
 * The rule used to key on `.btn`; the buttons became `starter-reward-cta` to
 * match the invite screen and silently fell to 30-33px on every phone page box
 * — on the one surface a shared link actually lands on. The pending placeholder
 * is a `div` and is included deliberately: it stands in the primary button's
 * place and must occupy exactly its box, or the panel shifts when the real
 * button lands. */
.exchange-detail-actions > button,
.exchange-detail-actions > .exchange-detail-pending {
  flex: 1 1 auto;
  min-height: 44px;
}

.exchange-detail-close {
  min-width: 44px;
  min-height: 44px;
}

.exchange-detail-art {
  display: flex;
  justify-content: center;
  margin-bottom: 14px;
  min-height: 0;
}

/* Capped against the VIEWPORT, not just the card, so a short landscape phone
   does not push the price and the actions off the bottom of a scrolling
   dialog — the art is the least important thing to see in full. */
.exchange-detail-art-img {
  max-width: 100%;
  max-height: min(34svh, 260px);
  border-radius: 10px;
  object-fit: contain;
}

/*
 * THE CARD IS THE HERO, so it is rendered LARGE — and it is scaled the only way
 * `docs/card-rendering-regression-contract.md` allows: by multiplying
 * `--card-unit` and redeclaring the two derived sizes in the same scope.
 *
 * NOT `transform: scale()`. A transform is a compositor operation: the layer
 * rasterises at the element's LAYOUT box and is then stretched, so the art can
 * never be sharper than the small box no matter how many backing pixels its
 * source holds. That is what made collection, deck-builder and Moon Dream cards
 * blurry, worst on a phone at DPR3 and exactly neutral on Firefox. Multiplying
 * the unit is dimensionally identical, keeps the whole vmin chain, and rasters
 * at the size actually displayed.
 *
 * The ambient unit is captured on the stage because a custom property cannot
 * reference itself.
 */
.exchange-listing-stage {
  /* SIZED BY THE HEIGHT IT HAS, NOT BY vmin.
     On a landscape phone `vmin` IS the height, so a raw `2.6vmin` collapsed the
     card to 50x90 there while blowing it up to 205x369 on desktop and pushing
     the actions 144px past the bottom of the viewport. The card is 22.2 units
     tall by definition, so solving against the space left after the copy gives
     one expression that is correct at both ends. */
  --exchange-card-unit: 1vmin;
  --card-unit: min(
    calc(var(--exchange-card-unit) * 2.6),
    calc((100svh - 130px) / 22.2)
  );
  --card-width: calc(12.29 * var(--card-unit));
  --card-height: calc(22.2 * var(--card-unit));
  display: flex;
  justify-content: center;
  align-items: center;
  /* The tilt engine rotates the card in 3D; without depth here it reads flat. */
  perspective: 900px;
}

/*
 * SHORT LANDSCAPE PHONES — where a shared link most often lands.
 *
 * Measured at the 932x300 page box: the card came to 376px in a 300px viewport
 * and the actions sat 63px below the fold. The container scrolls, so nothing
 * was unreachable, but the call to action on a LANDING surface must be visible
 * without being discovered — a visitor who followed a link and sees only a
 * picture has been given no next step.
 *
 * The art gives up the most because it is the least load-bearing thing here:
 * the reader already saw the card in the preview that brought them.
 */
@media (max-height: 430px) {
  .exchange-detail-card {
    padding: 14px 16px 12px;
    min-height: 0;
  }

  .exchange-detail-art {
    margin-bottom: 8px;
  }

  .exchange-detail-art-img {
    max-height: 20svh;
  }

  /* The card gives up the most on a short page box because the reader already
     saw it in the preview that brought them here; the offer and its call to
     action have not been seen yet. */
  .exchange-listing-stage {
    margin: 0;
  }

  .exchange-detail-name {
    margin-bottom: 4px;
  }

  /* Two lines, then ellipsis. The full text is on the listing itself; here it
     is orientation, not the offer. */
  .exchange-detail-desc {
    margin-bottom: 8px;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  .exchange-detail-actions {
    margin-top: 10px;
  }
}

.exchange-listing-eyebrow { grid-area: eyebrow; }
.exchange-listing-stage { grid-area: stage; align-self: center; }
.exchange-detail-body { grid-area: body; align-self: center; }
.exchange-detail-actions { grid-area: actions; }

/* Portrait phones have the height but not the width: stack there instead. */
@media (max-aspect-ratio: 3/4) {
  .exchange-detail-card {
    grid-template-areas:
      "eyebrow"
      "stage"
      "body"
      "actions";
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    text-align: center;
  }
}

/* The reward mark is decoration and must not consume a grid cell — it landed
   under the card and pushed the composition around. */
.exchange-listing-card > .starter-reward-mark,
.exchange-listing-card > [class*="reward-mark"] {
  position: absolute;
  bottom: 14px;
  left: 18px;
  width: clamp(28px, 5vmin, 46px);
  height: auto;
  opacity: 0.5;
  pointer-events: none;
}

.exchange-listing-overlay .exchange-listing-eyebrow {
  text-align: left;
  margin: 0 0 10px;
}

.exchange-listing-overlay .exchange-detail-name {
  /* Left-aligned and allowed a sensible measure; centred inside a narrow
     column is what produced "PAR / ALL / AX / WY / RM". */
  text-align: left;
  margin: 0 0 6px;
  font-size: clamp(1.25rem, 3.2vmin, 2rem);
  line-height: 1.12;
}

.exchange-listing-overlay .exchange-detail-body,
.exchange-listing-overlay .exchange-detail-actions {
  text-align: left;
  justify-content: flex-start;
}

/* The CTA is a pill sized for a full-width column; here it shares a row. */
.exchange-listing-overlay .exchange-detail-actions > button,
.exchange-listing-overlay .exchange-detail-actions > .exchange-detail-pending {
  /* The pill is `min-width: min(100%, 250px)` for a full-width column; here two
     of them share a row, and the CTA's own text overflowed the shape. */
  min-width: 0;
  flex: 0 1 auto;
  padding-inline: 18px;
  white-space: nowrap;
}

.exchange-detail-name {
  overflow-wrap: anywhere;
}

/*
 * ABOVE THE GAME, BELOW THE LEGAL GATES.
 *
 * The listing is where the reader landed, so it has to clear everything the app
 * starts behind it — the guest tutorial's mulligan (9700), the game-end overlay
 * (9999), the starter grant (10020) and the invite ticket (10050).
 *
 * It must NOT clear the consent gate (10200) or username onboarding (10190).
 * It shipped at 99999, which outranked both: a signed-in reader opening a
 * shared link after a legal-version bump would have had the Terms rendered
 * UNDERNEATH the listing, with every click on the consent checkbox swallowed —
 * the exact signup blocker `consentGateOutranksOverlays.test.js` was written
 * for, on a surface that test did not yet know about. Both of this file's
 * surfaces are in its list now.
 */
.exchange-listing-overlay {
  z-index: 10060;
}

.exchange-listing-overlay .exchange-detail-desc {
  margin: 0 0 12px;
  color: #c3b5cc;
  font-size: clamp(0.85rem, 1.7vmin, 1rem);
  line-height: 1.5;
  max-width: 60ch;
}


.exchange-detail-actions {
  flex-wrap: wrap;
}

/* ── The stub ───────────────────────────────────────────────── */
.exchange-ticket-stub {
  grid-area: stage;
  /* The claim burst mounts inside the ticket and is absolutely positioned
     against it. */
  position: relative;
  align-self: center;
  justify-self: center;
  padding: clamp(10px, 1.4vmin, 18px) clamp(10px, 1.4vmin, 18px) clamp(6px, 0.9vmin, 12px);
  border-radius: 8px;
  background: linear-gradient(168deg, #fdf7e8, #efe3ca);
  box-shadow:
    0 18px 44px rgba(0, 0, 0, .55),
    0 2px 0 rgba(255, 255, 255, .5) inset;
  /* Sat down at a slight angle, like something placed rather than rendered. */
  transform: rotate(-2.2deg);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(6px, 0.8vmin, 10px);
}

/* The perforation, drawn rather than imaged. */
.exchange-ticket-stub::before {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 0;
}

.exchange-ticket-serial {
  font-family: 'IBM Plex Mono', ui-monospace, monospace;
  font-size: clamp(0.52rem, 1.05vmin, 0.66rem);
  letter-spacing: 0.16em;
  color: #8a7a55;
  white-space: nowrap;
}

/* ── The copy ───────────────────────────────────────────────── */
.exchange-listing-overlay .exchange-detail-facts {
  margin-top: 12px;
  font-family: 'IBM Plex Mono', ui-monospace, monospace;
  font-size: clamp(0.68rem, 1.35vmin, 0.82rem);
  letter-spacing: 0.04em;
  color: rgba(238, 219, 241, .62);
}

/*
 * SHARE IS NOT A THIRD BUTTON. It is what you do after deciding, so it reads as
 * a link beside the claim rather than a rival to it — the reported defect was
 * "extremely tiring to have 3 calls to action, all styled mostly the same".
 */
.exchange-listing-overlay .exchange-detail-share {
  background: none;
  border: 0;
  color: rgba(238, 219, 241, .58);
  font-size: clamp(0.76rem, 1.5vmin, 0.9rem);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  padding: 0 6px;
  cursor: pointer;
  flex: 0 0 auto;
  min-width: 0;
}

.exchange-listing-overlay .exchange-detail-share:hover {
  color: rgba(238, 219, 241, .92);
}

/* The claim keeps the pill; nothing else does. */
.exchange-listing-overlay .exchange-detail-actions {
  align-items: center;
  gap: clamp(10px, 1.6vmin, 20px);
}

.exchange-listing-overlay .starter-reward-cta {
  flex: 0 1 auto;
}

/*
 * THE DIALOG TAKES FOCUS PROGRAMMATICALLY AND MUST NOT DRAW A RING FOR IT.
 *
 * `:focus-visible` was left styled on the assumption that a programmatic focus
 * never matches it. Chromium matches it anyway when the page has not been
 * clicked — which is every arrival from a link — so a 2px gold outline framed
 * the whole panel on open and vanished at the first click. Rendering the same
 * panel two different ways for a reason nobody can see is the "defect even for
 * a split second" class, and the ring is not load-bearing: this is a container,
 * not a control, and every control inside it keeps its own indicator.
 */
.exchange-listing-overlay .exchange-detail-card:focus,
.exchange-listing-overlay .exchange-detail-card:focus-visible { outline: none; }

/*
 * SIGN-IN RAISED, FOR THIS PATH ONLY.
 *
 * A visitor who followed a listing link and pressed "Create an account to buy"
 * got `#auth-view` revealed at z-index 1000 underneath the guest tutorial's
 * 9700 mulligan overlay: displayed, but unclickable. Scoped to the body mark so
 * every other route to sign-in keeps its existing stacking.
 */
body.exchange-signin-open #auth-view {
  /* Same window as the listing above: over the tutorial the visitor is standing
     in, under the legal gates that come next in their own flow. At 100000 it
     outranked the consent gate a brand-new account meets one step later. */
  z-index: 10060;
}

/* ── The action, in every state it has ──────────────────────── */

/*
 * NOT YET KNOWN — and it should barely be seen.
 *
 * A visitor now gets their button on the FIRST PAINT (the persisted session is
 * read from IndexedDB in ~5ms, not waited for through the app's boot), and a
 * signed-in reader waits only for one Convex round trip that starts before the
 * app has painted anything. So this holds the button's box for a few hundred
 * milliseconds at most.
 *
 * It used to announce itself: a full-size pill with a border and a 1.5s
 * shimmer sweeping across it, which read as a broken control rather than a
 * pause. Now it is a quiet, breathing ghost of the button — nothing to look at.
 * It also waits 180ms before appearing at all: a pause you never see needs no
 * indicator, and showing one for 80ms is worse than showing none.
 *
 * It carries the arriving button's label, hidden, so the row rule below sizes
 * it exactly as it will size that button. Do NOT replace that with a fixed
 * `min-width`: without the text this element has only its padding, and the row
 * rule rendered it as a 36px CIRCLE that snapped out to a 189px pill.
 */
.exchange-listing-overlay .exchange-detail-pending {
  min-width: min(100%, 250px);
  border-radius: 999px;
  border: 0;
  background: rgba(238, 219, 241, .06);
  opacity: 0;
  animation:
    exchange-pending-appear 140ms ease-out 180ms forwards,
    exchange-pending-breathe 1.8s ease-in-out 320ms infinite;
}

/* Occupies space, shows nothing. This is what makes the ghost the same width
   as the button that replaces it, in every language, at any price. */
.exchange-detail-pending-measure {
  visibility: hidden;
}

@keyframes exchange-pending-appear {
  to { opacity: 1; }
}

@keyframes exchange-pending-breathe {
  0%, 100% { background-color: rgba(238, 219, 241, .05); }
  50% { background-color: rgba(238, 219, 241, .10); }
}

@media (prefers-reduced-motion: reduce) {
  .exchange-listing-overlay .exchange-detail-pending {
    animation: exchange-pending-appear 1ms linear 180ms forwards;
    background: rgba(238, 219, 241, .07);
  }
}

/*
 * CANCELLING YOUR OWN LISTING is the only action on that screen, so it has to
 * read as a real button — but it takes something away, so it does not get the
 * gold pill the claim wears. An outline in the warning hue: unmistakably
 * pressable, never the loudest thing on the panel.
 */
.exchange-listing-overlay .exchange-detail-cancel {
  background: rgba(226, 138, 128, .10);
  border-color: rgba(226, 138, 128, .55);
  color: #f3c6c0;
  box-shadow: none;
  font-weight: 700;
}

.exchange-listing-overlay .exchange-detail-cancel:hover {
  background: rgba(226, 138, 128, .18);
  filter: none;
  box-shadow: none;
}

/* A disabled call to action must LOOK unavailable, not merely refuse the
   click — the reason is always stated in the status line beneath it. */
.exchange-listing-overlay .exchange-detail-actions > button:disabled {
  cursor: not-allowed;
  opacity: .45;
  filter: grayscale(.35);
  box-shadow: none;
}

/* What the panel says back: the reason a button is disabled, a failure, or
   nothing at all. It renders HERE and not in the exchange grid, which sits
   underneath this overlay where its status line cannot be read. */
.exchange-listing-overlay .exchange-detail-status {
  margin: 10px 0 0;
  min-height: 1.2em;
  font-size: clamp(0.74rem, 1.45vmin, 0.88rem);
  color: rgba(238, 219, 241, .72);
}

.exchange-listing-overlay .exchange-detail-status[data-tone="error"] {
  color: #f3b3ac;
}

/* The moment it worked. Replaces the action rather than leaving a button that
   would now do the wrong thing. */
.exchange-listing-overlay .exchange-detail-done {
  margin: 0;
  flex: 1 1 auto;
  color: #ffe5a0;
  font-size: clamp(0.9rem, 1.8vmin, 1.05rem);
  font-weight: 700;
}

/* ── The claim ──────────────────────────────────────────────── */

/*
 * THE TICKET IS THE BEAT.
 *
 * A spark burst was tried here first and cut: it rendered correctly and was
 * invisible anyway, because gold 6px sparks fired from the centre of a card
 * whose own art is bright gold read as nothing at all. Captured frames showed
 * the pop clearly and the sparks not at all.
 *
 * The ticket pops, straightens out of its resting tilt and takes a warm rim —
 * legible against every card in the game, and it is the object the claim
 * actually happened to.
 */
.exchange-ticket-stub.exchange-ticket-claimed {
  animation: exchange-ticket-claim 720ms cubic-bezier(.2, .9, .2, 1.06);
}

@keyframes exchange-ticket-claim {
  0% {
    transform: rotate(-2.2deg) scale(1);
    box-shadow: 0 18px 44px rgba(0, 0, 0, .55), 0 2px 0 rgba(255, 255, 255, .5) inset;
  }

  26% {
    transform: rotate(-0.4deg) scale(1.055);
    box-shadow:
      0 22px 54px rgba(0, 0, 0, .55),
      0 0 0 3px rgba(255, 229, 160, .85),
      0 0 34px rgba(240, 201, 107, .55),
      0 2px 0 rgba(255, 255, 255, .5) inset;
  }

  100% {
    transform: rotate(-2.2deg) scale(1);
    box-shadow: 0 18px 44px rgba(0, 0, 0, .55), 0 2px 0 rgba(255, 255, 255, .5) inset;
  }
}

@media (prefers-reduced-motion: reduce) {
  .exchange-ticket-stub.exchange-ticket-claimed { animation: none; }
}
