/* ═══════════════════════════════════════════════════════
   MAEVE — Exclusive Hub  ·  Premium Mini App Theme
   Brand-matched to maevelove.online

   Design principles for this sheet:
   · One accent, used sparingly. Purple marks what is interactive or
     selected — never decoration. Everything else earns attention
     through spacing and type, not colour.
   · Elevation instead of outline. Surfaces step up with a soft top
     highlight + downward shadow, so cards read as lit from above
     rather than as boxes drawn on a flat plane.
   · A 4px spacing rhythm and one radius scale, applied consistently.
   ═══════════════════════════════════════════════════════ */

@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=DM+Sans:wght@400;500;600;700&display=swap');

/* ─── Design Tokens ─── */
:root {
  --bg: #0e0719;
  --bg-glow: #341a5c;

  /* Three elevation levels. Cards no longer all share one flat fill —
     the step between them is what creates hierarchy on a dark screen. */
  --surface-1: rgba(255, 255, 255, 0.035);
  --surface-2: rgba(255, 255, 255, 0.055);
  --surface-3: rgba(255, 255, 255, 0.08);
  --card: var(--surface-1);
  --card-hover: var(--surface-2);
  --card-solid: #1a0f2b;

  --accent: #c078f5;
  --accent-bright: #e0b4ff;
  --accent-soft: rgba(192, 120, 245, 0.14);
  --accent-line: rgba(192, 120, 245, 0.42);
  --accent-glow: rgba(192, 120, 245, 0.38);

  --gold: #e8c477;
  --btc: #f7931a;
  --success: #7de0b0;

  /* Mint, pulled toward teal on purpose. A yellow-leaning green fought the
     violet page; sharing a blue base with the accent makes "earn / free"
     read as part of the same palette instead of a foreign sticker. */
  --green: #2ad4a4;
  --green-bright: #55e6bd;
  --green-soft: rgba(42, 212, 164, 0.1);
  --green-line: rgba(42, 212, 164, 0.34);
  --green-glow: rgba(42, 212, 164, 0.28);
  --green-ink: #032a20;

  --text: #f7f2ee;
  --text-muted: rgba(247, 242, 238, 0.6);
  --text-faint: rgba(247, 242, 238, 0.36);

  --border: rgba(255, 255, 255, 0.07);
  --border-2: rgba(255, 255, 255, 0.13);

  /* Top highlight — the single detail that makes a dark surface read as
     a physical, lit object instead of a painted rectangle. */
  --lift: inset 0 1px 0 rgba(255, 255, 255, 0.07);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 4px 16px -6px rgba(0, 0, 0, 0.5);
  --shadow-lg: 0 16px 40px -16px rgba(0, 0, 0, 0.7);

  --radius-sm: 12px;
  --radius-md: 16px;
  --radius-lg: 22px;
  --radius-full: 100px;

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --serif: 'Cormorant Garamond', Georgia, serif;
  --sans: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
}

/* ─── Reset ─── */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Telegram's real visible height, published by app.js from
   viewportStableHeight. Falls back to dvh outside Telegram (and before the
   first viewportChanged fires), so the browser preview is unaffected. */
:root { --tg-vh: 100dvh; }

html, body {
  font-family: var(--sans);
  color: var(--text);
  /* dvh, with vh as the fallback for anything that doesn't know it. On iOS the
     browser chrome collapses as you scroll and 100vh keeps measuring the TALL
     viewport, so the last ~90px of a full-height screen sat under the toolbar
     and the bottom button couldn't be reached at all.
     On Android, Telegram's own viewport is the one that matters — see --tg-vh. */
  min-height: 100vh;
  min-height: 100dvh;
  min-height: var(--tg-vh);
  overflow-x: hidden;
  /* Stops the whole page rubber-banding behind the mini app on iOS, which is
     what made the sheet look like it was tearing away from Telegram's header. */
  overscroll-behavior-y: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
  /* Android WebView "font boosting" silently enlarges text inside wide blocks,
     which is why the same screen looked cramped on Android and fine on iOS.
     Pinning this makes both platforms render the sizes we actually wrote. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  background:
    radial-gradient(120% 70% at 50% -10%, var(--bg-glow) 0%, transparent 62%),
    var(--bg);
  background-attachment: fixed;
  padding-bottom: env(safe-area-inset-bottom, 20px);
}

/* Capped and centred so the layout stays phone-shaped when Telegram opens the
   mini app in a wide sheet (iPad, Android tablets, Desktop). Every phone in
   use is narrower than this, so handsets are unaffected — without it a CTA
   stretched to ~800px and the whole screen read as a broken web page. */
.app {
  max-width: 520px;
  margin: 0 auto;
  min-height: 100vh;
  min-height: 100dvh;
  position: relative;
}

/* ─── Views ─── */
.view {
  display: none;
  opacity: 0;
  padding: 0 18px 28px;
}

/* The pay bar is fixed to the bottom and covers whatever is under it. 28px of
   padding left the real purchase button sitting UNDER the bar with no way to
   scroll past it — the page had already hit the end of its scroll range. This
   reserves the bar's own height plus the phone's home indicator, so the button
   can always be scrolled clear of it. */
#paymentView {
  padding-bottom: calc(104px + env(safe-area-inset-bottom, 0px));
}

.view.active {
  display: block;
  opacity: 1;
  animation: viewIn 0.4s var(--ease) both;
}

@keyframes viewIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes bounceIn {
  0% { opacity: 0; transform: scale(0.8); }
  60% { transform: scale(1.06); }
  100% { opacity: 1; transform: scale(1); }
}

@keyframes glowPulse {
  0%, 100% { box-shadow: 0 0 0 1px var(--accent-line), 0 0 22px -6px var(--accent-glow); }
  50% { box-shadow: 0 0 0 1px var(--accent-line), 0 0 30px -4px var(--accent-glow); }
}

@keyframes dotPulse {
  0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 0 0 var(--accent-glow); }
  50% { opacity: 0.7; transform: scale(0.9); box-shadow: 0 0 0 5px rgba(188, 132, 238, 0); }
}

/* ─── Header ─── */
.store-header {
  /* Negative margin pulls the sticky backdrop out to the true screen
     edges (.view adds 18px of side padding); the extra bottom padding
     is what the fade below lives in, so the black never ends in a hard
     line above the tabs.

     Deliberately NO backdrop-filter: a blur has a hard box boundary, so
     while the gradient faded smoothly the blur stopped dead — and that
     mismatch was the visible horizontal seam under the wordmark. A pure
     multi-stop gradient fades all the way out with nothing to cut. */
  margin: 0 -18px;
  padding: 22px 18px 34px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  position: sticky;
  top: 0;
  z-index: 100;
  background: linear-gradient(
    to bottom,
    var(--bg) 0%,
    var(--bg) 46%,
    rgba(14, 7, 25, 0.92) 62%,
    rgba(14, 7, 25, 0.7) 76%,
    rgba(14, 7, 25, 0.34) 88%,
    rgba(14, 7, 25, 0) 100%
  );
}

.store-brand {
  display: flex;
  align-items: center;
  gap: 11px;
  min-width: 0;
}

.brand-avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  /* Ring is drawn with shadows, not a border, so the image itself never
     gets scaled down inside its own frame. */
  box-shadow:
    0 0 0 1.5px var(--accent-line),
    0 0 0 4px rgba(192, 120, 245, 0.08),
    var(--shadow-md);
}

.brand-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.store-title {
  font-family: var(--serif);
  font-size: 27px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.005em;
  background: linear-gradient(115deg, #ffffff 35%, var(--accent-bright));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

.store-subtitle {
  font-size: 8.5px;
  font-weight: 700;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--accent);
  opacity: 0.85;
}

.store-trust-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin: -6px 0 16px;
  font-size: 11px;
  font-weight: 600;
  color: var(--accent-bright);
  background: var(--accent-soft);
  padding: 6px 14px;
  border-radius: var(--radius-full);
  border: 1px solid var(--accent-line);
  width: fit-content;
}

/* ─── Language Selector ─── */
.lang-selector {
  flex-shrink: 0;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  box-shadow: var(--lift);
  color: var(--text);
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  min-height: 40px;
  padding: 9px 30px 9px 13px;
  border-radius: var(--radius-full);
  outline: none;
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'><path d='M1 1L5 5L9 1' stroke='rgba(245,240,235,0.55)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 12px center;
  transition: border-color 0.2s var(--ease);
}

.lang-selector:focus {
  border-color: var(--accent-line);
}

.lang-selector option {
  background: var(--card-solid);
  color: var(--text);
}

/* ─── Promo / Claim Banner ───
   Copy and timer on the left, one compact pill on the right. A full-width
   button was tried and was simply too loud for the top of a shopping screen:
   it read as the app's main action rather than as one offer inside it. The
   pill only has to be findable and comfortably tappable, not enormous — the
   whole banner is clickable anyway. */
.promo-banner {
  margin: 2px 0 18px;
  padding: 14px 15px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 13px;
  animation: fadeUp 0.5s var(--ease) both;
}

/* min-width is what stops a long translated CTA from crushing the timer: the
   banner drops the button to a second row instead of squeezing the countdown. */
.promo-left { flex: 1 1 150px; min-width: 150px; }

/* Claimable state — refined emerald, tappable */
.promo-banner.claimable {
  background: linear-gradient(135deg, rgba(55, 217, 154, 0.1), rgba(255, 255, 255, 0.03));
  border: 1px solid var(--green-line);
  box-shadow: var(--lift), var(--shadow-md);
  cursor: pointer;
  /* No looping glow. The banner used to breathe a green halo every 3 seconds,
     which never stopped and pulled the eye away from the products. */
  animation: fadeUp 0.5s var(--ease) both;
}

.promo-banner.claimable:active { transform: scale(0.985); }

/* Single line, always. Both promo states are written to fit; the clipping is
   only a guarantee that a long translation can never push the banner taller. */
.promo-title {
  font-family: var(--sans);
  font-size: 14.5px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 3px;
  letter-spacing: -0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.promo-sub {
  font-size: 11.5px;
  font-weight: 500;
  color: var(--text-muted);
  margin-bottom: 6px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Countdown timer. Shared by the claimable banner and the claimed line — the
   two states look like the same promo precisely because the timer is drawn
   identically in both. */
.promo-countdown {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 3px;
  flex-shrink: 0;
}

.cd-block {
  display: flex;
  align-items: baseline;
  gap: 1px;
}

.cd-block span {
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--green-bright);
  min-width: 19px;
  text-align: center;
  /* Fixed-width digits: without this the timer re-flows every single second
     (1 is narrower than 8), which is what read as a jittery, "off" counter. */
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

.cd-block small {
  font-size: 9px;
  font-weight: 600;
  color: var(--text-faint);
  text-transform: uppercase;
}

.cd-sep {
  font-size: 12px;
  font-weight: 700;
  color: var(--text-faint);
  margin: 0 1px;
}

/* The one thing on this banner anyone is meant to press.
   A flat two-stop gradient in a pill is what made the first version read as a
   placeholder: real buttons have a light source. This one has a top-lit face
   (light band above the midline, dark below), a bright inner rim along the top
   edge, a dark rim along the bottom, and a coloured shadow beneath — so it
   reads as a raised physical key rather than a green rectangle. */
.promo-cta {
  position: relative;
  overflow: hidden;
  flex-shrink: 0;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 116px;
  min-height: 44px;
  font-size: 13.5px;
  font-weight: 800;
  letter-spacing: 0.04em;
  color: #04301f;
  /* Three stops, not two: the hard-ish turn just past the middle is the
     highlight edge, and it's what gives the face its curve. */
  background:
    linear-gradient(180deg, #8ff7d0 0%, #45dfa6 46%, #16b87e 47%, #0e9e6b 100%);
  padding: 12px 17px;
  border-radius: var(--radius-full);
  box-shadow:
    /* the light catching the top edge */
    inset 0 1px 0 rgba(255, 255, 255, 0.62),
    /* the wall of the key, slightly darker than the face */
    inset 0 -1px 0 rgba(0, 0, 0, 0.28),
    /* contact shadow, then the coloured spill it throws on the banner */
    0 2px 3px -1px rgba(0, 0, 0, 0.5),
    0 8px 20px -8px rgba(31, 193, 132, 0.75);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.22);
  white-space: nowrap;
  animation: promoCtaBreath 2.6s ease-in-out infinite;
}

/* A sheen that crosses the face every few seconds. This is the "alive" part —
   a scale pulse alone just made the whole pill twitch, which draws the eye
   without ever suggesting the thing is pressable. */
.promo-cta::after {
  content: '';
  position: absolute;
  top: -60%;
  bottom: -60%;
  width: 38%;
  left: -50%;
  background: linear-gradient(100deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.5) 50%,
    rgba(255, 255, 255, 0) 100%);
  transform: skewX(-18deg);
  animation: promoCtaSheen 3.6s ease-in-out infinite;
  pointer-events: none;
}

@keyframes promoCtaBreath {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.035); }
}

/* Long pause, quick pass. A sheen on a short loop reads as a loading bar. */
@keyframes promoCtaSheen {
  0%, 62%  { left: -50%; }
  86%, 100% { left: 118%; }
}

/* Pressed: the key goes down into the banner — highlight off, shadow pulled in. */
.promo-banner.claimable:active .promo-cta {
  animation: none;
  transform: translateY(1px) scale(0.985);
  box-shadow:
    inset 0 2px 4px rgba(0, 0, 0, 0.32),
    0 1px 2px -1px rgba(0, 0, 0, 0.5);
}

/* ─── Claimed: one line, above the tabs ───
   No border, no fill, no panel. The discount is already applied to every
   price on the screen below; this line only names it and says how long it
   lasts, so it takes a line's worth of space and no more. The countdown is
   the same component the claimable banner uses, at the same size and colour,
   which is what makes the two states read as one promo rather than as two
   unrelated widgets. */
.promo-line {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 4px 9px;
  margin: 2px 0 14px;
  animation: fadeUp 0.4s var(--ease) both;
}

.pl-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  flex-shrink: 0;
  background: linear-gradient(135deg, var(--green-bright), var(--green));
  color: var(--green-ink);
  font-size: 9px;
  font-weight: 900;
}

.pl-main {
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.005em;
  color: var(--green-bright);
}

/* The separator before the timer. A dot rather than a rule: the two halves are
   one sentence, not two fields. */
.promo-line .promo-countdown::before {
  content: '·';
  color: var(--text-faint);
  margin-right: 6px;
  font-weight: 700;
}

/* Spun and won nothing. Nothing is applied to anything, so the green goes. */
.promo-line.empty .pl-main,
.pl-quiet { color: var(--text-muted); font-weight: 600; }

/* Price-drop reveal after claim. The number itself is counted down in JS
   (animatePriceDrop) — all that's left here is fading the original in beside
   it. There is deliberately no scale-pop: it fought the counting digits. */
.price-reveal .product-old-price {
  animation: fadeUp 0.5s ease;
}

/* ─── Category Tabs ─── */
.category-tabs {
  display: flex;
  /* "safe center" keeps the row centred when it fits, but falls back to
     start-alignment when it overflows — plain `center` makes the first tab
     unreachable behind the scroll origin on Android. */
  justify-content: center;
  justify-content: safe center;
  overflow-x: auto;
  gap: 8px;
  padding: 0 16px 10px;
  margin: 0 -16px 18px;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
}

.category-tabs::-webkit-scrollbar { display: none; }

.category-tab {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Tall enough to be a comfortable target on a phone (40px is the floor both
     Apple and Google publish); horizontal padding stays tight because all four
     categories still have to clear a 320px screen without the strip turning
     into a scroller. */
  min-height: 40px;
  padding: 8px 14px;
  border-radius: var(--radius-full);
  background: var(--surface-1);
  border: 1px solid var(--border);
  color: var(--text-muted);
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: color 0.2s var(--ease), background 0.2s var(--ease),
              border-color 0.2s var(--ease), transform 0.15s var(--ease);
  white-space: nowrap;
}

.category-tab svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
}

.category-tab:active { transform: scale(0.95); }

.category-tab.active {
  background: linear-gradient(135deg, var(--accent), #9a55d8);
  border-color: transparent;
  color: #fff;
  box-shadow: 0 6px 18px -8px var(--accent-glow), var(--lift);
}

/* ─── Section Header ─── */
.section-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 22px 0 12px;
}

.section-header:first-child { margin-top: 2px; }

.section-header h2 {
  font-family: var(--serif);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text);
  white-space: nowrap;
}

.section-header .section-line {
  flex: 1;
  height: 1px;
  background: linear-gradient(to right, var(--border-2), transparent);
}

/* ─── Product Grid ─── */
.product-grid {
  display: flex;
  flex-direction: column;
  gap: 9px;
}

/* ─── Product Card ─── */
.product-card {
  background: var(--surface-1);
  border: 1px solid var(--border);
  box-shadow: var(--lift);
  border-radius: var(--radius-md);
  padding: 14px;
  display: flex;
  align-items: center;
  gap: 13px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: border-color 0.25s var(--ease), background 0.25s var(--ease),
              transform 0.15s var(--ease);
  animation: fadeUp 0.45s var(--ease) both;
}

.product-card:active {
  transform: scale(0.985);
  background: var(--surface-2);
}

.product-card.featured {
  border-color: var(--accent-line);
  background: linear-gradient(135deg, var(--accent-soft), var(--surface-1));
  box-shadow: var(--lift), 0 0 26px -12px var(--accent-glow);
}

/* Product Icon */
.product-icon {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  background: linear-gradient(150deg, var(--accent-soft), rgba(192, 120, 245, 0.04));
  border: 1px solid var(--accent-line);
  box-shadow: var(--lift);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--accent-bright);
}

/* Slightly larger than the old line icons: these carry facets and a highlight,
   and below ~23px those details collapse into a blob. */
.product-icon svg {
  width: 24px;
  height: 24px;
}

.product-info {
  flex: 1;
  min-width: 0;
}

/* Name and badge share this line, and the badge must NOT be allowed to wrap
   underneath — a badge that sits inline on one card and drops to its own line
   on the next is what made the list look ragged. nowrap pins it beside the
   name; the width it needs is bought back elsewhere (a narrower price column,
   a smaller badge, and shorter badge wording in the languages that need it),
   so in practice the name never has to give up a character. */
.product-name-row {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 7px;
  min-width: 0;
  margin-bottom: 3px;
}

/* One badge, one treatment: filled gold, everywhere, in every category.
   The violet "Popular" next to a gold "Best value" read as two unrelated
   stickers from two different shops — and violet is the app's interactive
   colour everywhere else, so a violet badge quietly suggested a button. */
/* Sits beside the product name. Deliberately small: every pixel it takes is a
   pixel the name loses, and on this card the name is the thing being sold. */
.product-badge {
  font-size: 7px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 3px 5.5px;
  border-radius: var(--radius-full);
  background: linear-gradient(135deg, #f0c869, #c9962f);
  color: #241705;
  box-shadow: 0 2px 8px -3px rgba(232, 196, 119, 0.8);
  white-space: nowrap;
  flex-shrink: 0;
}

.product-name {
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

.product-desc {
  font-size: 11.5px;
  color: var(--text-muted);
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* No text-wrap hint at all — plain greedy wrapping, on purpose.
     `balance` evens the two lines by length and `pretty` pulls words down to
     avoid a short last line; both are typographic goals, and both override
     the seam the sentence actually has ("…channel / for 30 days"). The break
     is controlled in the copy instead: the phrases that must travel together
     are joined with non-breaking spaces in the translations, so greedy filling
     lands the break exactly where the meaning does — and identically on every
     browser, old WebKit included, since nothing here depends on a property
     the engine may not implement. */
}

.product-price-block {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  /* No gap: the only other child is the zero-height badge spacer, and a gap
     would be the one thing still able to nudge the price off centre. */
  gap: 0;
  flex-shrink: 0;
  text-align: right;
}

/* Original above, current below — stacked, not side by side. Side by side the
   pair is as wide as both numbers put together, and in RUB that is ~116px of
   the card taken by the price alone, which is what pushed the longest product
   names into an ellipsis. Stacked it is as wide as the wider number. */
.product-price-row {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
}

.product-old-price {
  font-size: 12px;
  color: var(--text-faint);
  text-decoration: line-through;
  white-space: nowrap;
}

/* Price keeps the same weight/colour claimed or not — only the struck-through
   original appears beside it. Changing the colour made it recede instead of pop. */
.product-price {
  font-family: var(--sans);
  font-size: 18.5px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--text);
  white-space: nowrap;
  /* Fixed-width digits: the claim animation counts this number down, and
     proportional digits made the whole price block twitch on every frame. */
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

.product-price.full {
  color: var(--text);
}

/* ═══════════════════════════════════════════
   PAYMENT VIEW
   ═══════════════════════════════════════════ */

.back-btn {
  display: flex;
  align-items: center;
  gap: 5px;
  background: none;
  border: none;
  color: var(--text-muted);
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  /* The trailing padding is the tap target, not decoration — a back control
     36px tall is one of the easiest things on a phone to miss. */
  padding: 20px 10px 10px 0;
  margin-left: -2px;
  transition: color 0.2s;
}

.back-btn:active { color: var(--text); }
.back-btn svg { width: 18px; height: 18px; }

.payment-view-header {
  text-align: center;
  padding: 14px 0 20px;
}

.payment-view-header h2 {
  font-family: var(--serif);
  font-size: 31px;
  font-weight: 600;
  letter-spacing: 0.005em;
  margin-bottom: 4px;
}

.payment-view-header p {
  color: var(--text-muted);
  font-size: 13px;
}

/* ─── What you're buying ───
   Reworked into a single quiet line: an icon, the name, and the price on
   the right. It used to be a full card with its own large price block —
   which meant the same price appeared twice on one screen (again in the
   Total below) and the two competed. This states the order and gets out
   of the way; Total downstairs is the one number that carries weight. */
.payment-product-summary {
  background: linear-gradient(135deg, var(--accent-soft), rgba(255, 255, 255, 0.025));
  border: 1px solid var(--accent-line);
  box-shadow: var(--lift);
  border-radius: var(--radius-md);
  padding: 11px 14px;
  margin-bottom: 22px;
  display: flex;
  align-items: center;
  gap: 12px;
}

.payment-product-summary .product-icon {
  width: 36px;
  height: 36px;
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.1);
}

.payment-product-summary .product-icon svg {
  width: 22px;
  height: 22px;
}

/* Name on the left, price on the right, one line. The description is
   dropped here on purpose: the person just read it on the card they tapped,
   so repeating it only pushed the payment methods further down the screen. */
.summary-info {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Wrapping matters on 320px screens: with a promo running the price side
     carries three chips, and without this the name got crushed to ~78px and
     broke across four lines. Now the price drops to its own line instead. */
  flex-wrap: wrap;
  gap: 3px 12px;
}

.summary-name {
  flex: 1 1 130px;
  min-width: 0;
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text);
  /* Two lines is the ceiling — a long name should never push the payment
     methods off the first screen. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.summary-desc { display: none; }

.summary-price-row {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  justify-content: flex-end;
  gap: 3px 7px;
  flex: 0 0 auto;
  flex-wrap: nowrap;
}

.summary-price {
  font-family: var(--sans);
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.summary-old-price {
  font-size: 12px;
  color: var(--text-faint);
  text-decoration: line-through;
  white-space: nowrap;
}

.discount-tag {
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.05em;
  color: var(--green-ink);
  background: linear-gradient(135deg, var(--green-bright), var(--green));
  border: none;
  padding: 3px 8px;
  border-radius: var(--radius-full);
  white-space: nowrap;
}

/* Payment Methods */
.payment-methods-title {
  font-size: 10.5px;
  font-weight: 700;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  margin-bottom: 11px;
}

.payment-method-card {
  background: var(--surface-1);
  border: 1px solid var(--border);
  box-shadow: var(--lift);
  border-radius: var(--radius-md);
  padding: 14px 15px;
  margin-bottom: 9px;
  display: flex;
  align-items: center;
  gap: 13px;
  cursor: pointer;
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease),
              transform 0.15s var(--ease), box-shadow 0.2s var(--ease);
}

.payment-method-card:active { transform: scale(0.99); }

.payment-method-card.selected {
  border-color: var(--accent-line);
  background: linear-gradient(135deg, var(--accent-soft), rgba(255, 255, 255, 0.03));
  box-shadow: var(--lift), 0 0 24px -14px var(--accent-glow);
}

.method-icon {
  width: 42px;
  height: 42px;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 21px;
  font-weight: 700;
  flex-shrink: 0;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: var(--lift);
}

.method-icon svg { width: 24px; height: 24px; }

.method-icon.stars {
  color: #ffc93c;
}

.method-icon.stars svg {
  filter: drop-shadow(0 0 5px rgba(255, 201, 60, 0.5));
}

.method-icon.crypto {
  color: var(--btc);
  font-family: var(--sans);
}

.method-info { flex: 1; min-width: 0; }

.method-name {
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
}

/* Provider chips under each method name. Deliberately quiet — they exist to be
   found by someone looking for reassurance ("can I pay with PayPal?"), not to
   be read first. */
.method-providers {
  margin-top: 4px;
  font-size: 11.5px;
  font-weight: 500;
  color: var(--text-muted);
  transition: color 0.2s;
}

.method-providers span {
  white-space: nowrap;
}

.method-providers span:not(:last-child)::after {
  content: ", ";
}

.payment-method-card.selected .method-providers {
  color: rgba(247, 242, 238, 0.85);
}

.method-check {
  width: 21px;
  height: 21px;
  border-radius: 50%;
  border: 2px solid var(--border-2);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s var(--ease);
  flex-shrink: 0;
}

.payment-method-card.selected .method-check {
  border-color: var(--accent);
  background: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

.method-check-inner {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  transform: scale(0);
  transition: all 0.2s var(--ease);
}

.payment-method-card.selected .method-check-inner {
  opacity: 1;
  transform: scale(1);
}

/* ─── "What happens next" ───
   Deliberately unboxed: it's a whisper before the one loud element on the
   screen, not a card competing with the methods above it. */
.pay-next {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 16px 2px 0;
}

.pay-next svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  margin-top: 2px;
  color: var(--accent);
  opacity: 0.8;
}

.pay-next p {
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--text-muted);
}

/* ─── Total ───
   The one number that should carry weight on this screen. Deliberately
   unframed: a panel around it plus the button's own surface read as a box
   inside a box, which made the whole block look heavier than it is. The
   number carries itself on spacing alone. */
.payment-price-display {
  text-align: center;
  padding: 0 16px;
  margin: 14px 0 0;
}

.price-label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--text-muted);
  margin-bottom: 7px;
}

.price-value {
  font-family: var(--sans);
  font-size: 32px;
  font-weight: 700;
  letter-spacing: -0.025em;
  line-height: 1.05;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.price-subtext {
  font-size: 12px;
  color: rgba(247, 242, 238, 0.78);
  margin-top: 6px;
  text-align: center;
}

/* Purchase Button — etched glass.
   A frosted surface instead of a filled slab: it sits on the page rather
   than on top of it, and a highlight travelling across the glass every few
   seconds is what draws the eye — motion, not saturation. */
.purchase-btn {
  position: relative;
  width: 100%;
  padding: 19px;
  border: none;
  border-radius: 18px;
  margin-top: 16px;
  /* Deliberately far brighter than the method cards above (which sit at
     0.035): on this screen the cards are a choice already made, and only
     one element should look pressable. The halo lifts it off the page so
     it reads as the last step rather than a fourth option. */
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.05));
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: #fff;
  font-family: var(--sans);
  font-size: 16.5px;
  font-weight: 700;
  letter-spacing: 0.01em;
  cursor: pointer;
  overflow: hidden;
  /* The fill stays genuinely translucent; prominence comes from the bright
     hairline and the halo instead, so the button reads as glass rather than
     as a painted slab. */
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.34),
    inset 0 1px 0 rgba(255, 255, 255, 0.2),
    0 12px 34px -12px rgba(255, 255, 255, 0.2),
    0 8px 24px -12px rgba(0, 0, 0, 0.6);
  transition: transform 0.15s var(--ease), background 0.25s var(--ease), opacity 0.2s;
}

/* The travelling highlight. Sits behind the label (which app.js wraps in a
   <span>), so it sweeps under the text rather than washing it out. */
.purchase-btn::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -60%;
  width: 45%;
  z-index: 0;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.5), transparent);
  animation: purchaseGlint 3.4s var(--ease) infinite;
}

/* The sweep is only visible for the first ~55% of each cycle, and the
   animation clock starts at page load — so opening the payment screen during
   the quiet stretch showed nothing at all, which read as "the effect only
   works on some products". restartGlint() in app.js drops this class in for
   one frame to reset the clock, so the sweep always plays on arrival. */
.purchase-btn.glint-reset::before { animation: none; }

.purchase-btn > * { position: relative; z-index: 1; }

/* Method icon sits inline with the label; the button itself is colourless,
   so this is the only spot of colour on it. */
.purchase-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.btn-icon { color: var(--gold); font-size: 16px; line-height: 1; }

@keyframes purchaseGlint {
  0% { left: -60%; }
  55%, 100% { left: 130%; }
}

.purchase-btn:active {
  transform: scale(0.985);
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.09));
}

.purchase-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.purchase-btn.loading { pointer-events: none; }
.purchase-btn.loading::before { animation: none; }

/* The gift-card fallback is a separate action, not part of the Total
   panel — it gets its own detached, quieter treatment. */
.purchase-btn.secondary {
  border-radius: var(--radius-md);
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  color: var(--text);
  box-shadow: var(--lift);
}

/* The fallback must stay the quieter of the two — no travelling highlight. */
.purchase-btn.secondary::before { display: none; }

/* ═══════════════════════════════════════════
   SUCCESS VIEW
   ═══════════════════════════════════════════ */

.success-view {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 80vh;
  text-align: center;
  padding: 40px 24px;
}

.success-icon {
  width: 76px;
  height: 76px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), #9a55d8);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 24px;
  animation: bounceIn 0.55s var(--ease);
  box-shadow: 0 0 48px -8px var(--accent-glow), var(--lift);
}

.success-icon svg {
  width: 36px;
  height: 36px;
  color: #fff;
  stroke-width: 3;
}

/* Pending state (e.g. crypto invoice opened, not yet confirmed) —
   amber, clearly different from a confirmed purple/green success */
.success-icon.pending {
  background: linear-gradient(135deg, #ffc93c, #f59e0b);
  box-shadow: 0 0 48px -8px rgba(245, 158, 11, 0.4), var(--lift);
}

.success-title {
  font-family: var(--serif);
  font-size: 31px;
  font-weight: 600;
  margin-bottom: 8px;
  animation: fadeUp 0.5s ease 0.15s both;
}

.success-subtitle {
  font-size: 14px;
  color: var(--text-muted);
  margin-bottom: 30px;
  animation: fadeUp 0.5s ease 0.22s both;
}

.success-order-card {
  background: var(--surface-1);
  border: 1px solid var(--border);
  box-shadow: var(--lift);
  border-radius: var(--radius-md);
  padding: 8px 18px;
  width: 100%;
  max-width: 320px;
  margin-bottom: 24px;
  animation: fadeUp 0.5s ease 0.3s both;
}

.success-order-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 14px;
  padding: 11px 0;
}

.success-order-row:not(:last-child) {
  border-bottom: 1px solid var(--border);
}

.success-order-label {
  font-size: 12.5px;
  color: var(--text-muted);
  flex-shrink: 0;
}

.success-order-value {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
  text-align: right;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.success-btn {
  width: 100%;
  max-width: 320px;
  padding: 15px;
  border: none;
  border-radius: var(--radius-md);
  background: linear-gradient(135deg, var(--accent), #9a55d8);
  color: #fff;
  font-family: var(--sans);
  font-size: 14.5px;
  font-weight: 700;
  cursor: pointer;
  margin-top: 8px;
  box-shadow: 0 10px 26px -12px var(--accent-glow), var(--lift);
  animation: fadeUp 0.5s ease 0.36s both;
}

.success-btn.secondary {
  background: transparent;
  border: 1px solid var(--border-2);
  color: var(--text-muted);
  box-shadow: none;
}

/* ═══════════════════════════════════════════
   MISC
   ═══════════════════════════════════════════ */

.spinner {
  width: 18px;
  height: 18px;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
  display: inline-block;
  vertical-align: middle;
}

@keyframes spin { to { transform: rotate(360deg); } }

.safe-bottom {
  padding-bottom: calc(env(safe-area-inset-bottom, 16px) + 16px);
}

.store-footer {
  text-align: center;
  padding: 28px 0 16px;
  color: var(--text-faint);
  font-size: 11px;
}

::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 4px; }

/* ─── Confetti (fired when a credit reward is redeemed) ───
   launchConfetti() in app.js builds these nodes and sets colour, size,
   position and duration inline; this is the motion they were missing. */
.confetti-container {
  position: fixed;
  inset: 0;
  z-index: 9998;
  pointer-events: none;
  overflow: hidden;
}

/* Each particle is a wrapper that falls and drifts sideways, holding an inner
   piece that spins on its own axis. One element doing both meant `transform`
   had to carry the fall and the spin together, which is why every piece
   dropped in the same dead-straight line at the same rate — the thing that
   made it read as a cheap effect rather than as paper in the air. */
.confetti-particle {
  position: absolute;
  will-change: transform;
  animation-name: confettiFall;
  animation-timing-function: cubic-bezier(0.15, 0.45, 0.55, 1);
  animation-fill-mode: forwards;
}

.confetti-particle i {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 1px;
  animation-name: confettiFlip;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* Round pieces mixed into the strips — all-rectangles is what reads as
   "divs falling" instead of confetti. */
.confetti-particle.round i { border-radius: 50%; }

@keyframes confettiFall {
  0%   { transform: translate3d(0, -6vh, 0); opacity: 1; }
  92%  { opacity: 1; }
  100% { transform: translate3d(var(--drift, 0px), 106vh, 0); opacity: 0; }
}

/* Rotating on two axes is what gives the flat pieces their flicker — edge-on
   they nearly vanish, then catch the light again. */
@keyframes confettiFlip {
  0%   { transform: rotateX(0deg) rotateZ(0deg); }
  100% { transform: rotateX(720deg) rotateZ(360deg); }
}

/* A short outward burst from the centre, fired alongside the fall. The old
   effect started every piece off-screen at the top, so the celebration
   arrived about a second after the number it was celebrating. */
.confetti-burst {
  position: absolute;
  left: 50%;
  top: 42%;
  animation-name: confettiBurst;
  animation-timing-function: cubic-bezier(0.12, 0.7, 0.3, 1);
  animation-fill-mode: forwards;
}

@keyframes confettiBurst {
  0%   { transform: translate3d(0, 0, 0) scale(0.4); opacity: 1; }
  70%  { opacity: 1; }
  100% { transform: translate3d(var(--bx, 0px), var(--by, 0px), 0) scale(1); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .confetti-container { display: none; }
}

/* ═══════════════════════════════════════════
   SECTION DIVIDER (products ↔ bottom sections)
   ═══════════════════════════════════════════ */
.section-divider {
  height: 1px;
  margin: 26px 6px;
  background: linear-gradient(to right, transparent, var(--accent-line) 50%, transparent);
  position: relative;
}

/* ═══════════════════════════════════════════
   INITIAL LOADING OVERLAY
   ═══════════════════════════════════════════ */
.loading-screen {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  background:
    radial-gradient(120% 70% at 50% -10%, var(--bg-glow) 0%, transparent 62%),
    var(--bg);
  opacity: 1;
  transition: opacity 0.45s var(--ease);
}

.loading-screen.hidden {
  opacity: 0;
  pointer-events: none;
}

.loader-ring {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 2.5px solid rgba(192, 120, 245, 0.16);
  border-top-color: var(--accent-bright);
  animation: spin 0.8s linear infinite;
  box-shadow: 0 0 26px -6px var(--accent-glow);
}

.loader-wordmark {
  font-family: var(--serif);
  font-size: 26px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text);
  animation: loaderPulse 1.6s ease-in-out infinite;
}

@keyframes loaderPulse {
  0%, 100% { opacity: 0.45; }
  50% { opacity: 0.9; }
}

/* ═══════════════════════════════════════════════════════
   INVITE & EARN
   ═══════════════════════════════════════════════════════ */

/* ─── Teaser card on the store view ─── */
/* Same neutral card as everything else on the page — any tinted outline
   here read as a sticker pasted on top of the layout. The mint now shows
   up only where it means something: the soft glow behind the gift icon
   and the arrow. */
.earn-teaser {
  position: relative;
  margin: 4px 0 22px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border);
  background: var(--surface-1);
  box-shadow: var(--lift), var(--shadow-md);
  overflow: hidden;
  cursor: pointer;
  transition: transform 0.24s var(--ease), border-color 0.24s, background 0.24s;
}
.earn-teaser:active { transform: scale(0.985); }
.earn-teaser:hover { border-color: var(--border-2); background: var(--surface-2); }

.earn-teaser-glow {
  position: absolute;
  inset: -40% -20% auto auto;
  width: 200px; height: 200px;
  background: radial-gradient(circle, var(--green-glow), transparent 68%);
  pointer-events: none;
}

.earn-teaser-body {
  position: relative;
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 16px;
}
.earn-teaser-icon { font-size: 26px; flex-shrink: 0; }
.earn-teaser-text { flex: 1; min-width: 0; }
.earn-teaser-text h3 {
  font-size: 15px; font-weight: 700; letter-spacing: -0.01em;
  color: var(--text); margin-bottom: 3px;
}
.earn-teaser-text p {
  font-size: 12.5px; line-height: 1.45; color: var(--text-muted);
}
.earn-teaser-arrow { width: 18px; height: 18px; color: var(--green); flex-shrink: 0; }

/* ─── Earn view ─── */
/* Every block on this screen enters on the same stagger the store uses for
   product cards, so arriving here reads as the same app rather than a plainer
   sub-page that simply appears. */
/* Delays follow the reading order of the page, so they have to be re-checked
   whenever the sections are re-ordered. */
#earnView > *:not(.view-topbar) { animation: fadeUp 0.5s var(--ease) both; }
#earnView > .earn-hero          { animation-delay: 0.02s; }
#earnView > .earn-balance-card  { animation-delay: 0.06s; }
#earnView > .earn-how           { animation-delay: 0.10s; }
#earnView > .earn-link-label,
#earnView > .earn-link-box,
#earnView > .earn-link-actions  { animation-delay: 0.14s; }

.earn-hero { position: relative; text-align: center; padding: 10px 0 22px; }
/* Two glows instead of one: a wide violet wash for depth and a tighter mint
   core underneath it. The mint is what tells you, before reading a word, that
   this screen belongs to the "free / earn" half of the palette. */
.earn-hero::before {
  content: '';
  position: absolute; top: -46px; left: 50%; transform: translateX(-50%);
  width: 300px; height: 200px;
  background:
    radial-gradient(circle at 50% 45%, var(--green-soft) 0%, transparent 58%),
    radial-gradient(circle, var(--accent-soft) 0%, transparent 72%);
  filter: blur(10px);
  z-index: -1; pointer-events: none;
}
.earn-hero h2 {
  font-family: var(--serif);
  font-size: 36px; font-weight: 700; letter-spacing: -0.015em;
  line-height: 1.12;
  background: linear-gradient(115deg, var(--text) 34%, var(--green-bright) 92%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  margin-bottom: 8px;
}
/* The gradient above is clipped to the text — which also repaints emoji as a
   flat violet silhouette. Emoji opt back out of the clip so they render in
   their own colours. The explicit light fill (rather than `initial`, which
   resolves to black) keeps a monochrome fallback glyph legible on the dark
   background. */
.earn-hero h2 .emo {
  -webkit-background-clip: border-box; background-clip: border-box;
  -webkit-text-fill-color: var(--text); color: var(--text);
}
.earn-hero p { font-size: 13.5px; color: var(--text-muted); }

.earn-ratio-caption {
  margin-top: 3px;
  font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
  color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Balance — the centerpiece of the screen, treated like the store's featured
   card: a lit gradient face, an outer halo, and a sheen along the top edge. */
.earn-balance-card {
  position: relative;
  border-radius: var(--radius-lg);
  border: 1px solid var(--green-line);
  background:
    radial-gradient(120% 90% at 50% -20%, rgba(85, 230, 189, 0.22), transparent 62%),
    linear-gradient(150deg, rgba(55, 217, 154, 0.13), rgba(255, 255, 255, 0.03));
  box-shadow:
    var(--lift),
    var(--shadow-lg),
    0 0 34px -16px var(--green-glow);
  overflow: hidden;
  margin-bottom: 22px;
}
/* Hairline of light across the top edge, brightest in the middle. The single
   detail that separates a lit object from a coloured rectangle. */
.earn-balance-card::before {
  content: '';
  position: absolute; top: 0; left: 12%; right: 12%; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(85, 230, 189, 0.75), transparent);
  pointer-events: none;
}
/* The balance reads left-to-right — number, then what it is — so the card is
   only as tall as one line of type. It was a 52px number centred over two
   centred captions, which took a third of the screen to say "you have 3". */
.earn-balance-main {
  position: relative;
  display: flex; align-items: center; justify-content: center; gap: 13px;
  padding: 17px 18px 16px;
}
/* The number and the 💎 are different scripts with different baselines, and
   Android sits the emoji noticeably lower than iOS does. Centring them as flex
   items instead of leaning on the text baseline lines the pair up everywhere. */
.earn-balance-value {
  font-family: var(--sans);
  font-size: 34px; font-weight: 700; line-height: 1;
  letter-spacing: -0.035em;
  display: flex;
  align-items: center;
  gap: 7px;
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}
/* Sized to its own text, NOT flex:1 — given the rest of the row it would pin
   the pair to the left edge instead of letting the two of them sit centred in
   the card together. min-width:0 so a long ratio caption ellipses inside it
   rather than pushing the number off-centre in the wordier languages. */
.earn-balance-meta { min-width: 0; }
/* Only the NUMBER takes the gradient. Applying it to the flex row would drag
   the 💎 into the clip and flatten it into a silhouette, the same trap the
   hero heading documents above. */
.earn-balance-value > span:first-child {
  background: linear-gradient(140deg, #ffffff 18%, var(--green-bright) 78%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
.earn-gem {
  font-size: 20px; line-height: 1; display: block;
  filter: drop-shadow(0 3px 10px var(--green-glow));
}
.earn-balance-label {
  font-size: 9.5px; font-weight: 700; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ─── Next reward ───
   The bridge between "you have 15" and "rewards cost 20": without it the
   balance is a number with nothing to measure itself against. Sits inside the
   hero card, between the total and the record, because it belongs to both. */
.earn-next {
  padding: 13px 16px 15px;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
  background: rgba(0, 0, 0, 0.14);
}
.earn-next-line {
  display: flex; align-items: center; gap: 9px;
  margin-bottom: 9px;
}
.earn-next-icon { font-size: 15px; flex-shrink: 0; line-height: 1; }
.earn-next-name {
  flex: 1; min-width: 0;
  font-size: 12.5px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.earn-next-left {
  flex-shrink: 0;
  font-size: 10px; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--green-bright);
}
.earn-next-bar {
  height: 4px; border-radius: 100px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
.earn-next-bar span {
  display: block; height: 100%; border-radius: 100px;
  background: linear-gradient(90deg, var(--green), var(--green-bright));
  box-shadow: 0 0 12px -2px var(--green-glow);
  transition: width 0.7s var(--ease);
}

.earn-balance-split {
  display: flex;
  border-top: 1px solid var(--green-line);
  background: rgba(0, 0, 0, 0.28);
}
/* A record, not a scoreboard. Number and label sit on ONE line each, so the
   three of them cost a single 34px strip instead of three stacked blocks that
   out-shouted the balance they were supporting. */
.earn-stat {
  flex: 1; position: relative;
  display: flex; align-items: baseline; justify-content: center; gap: 5px;
  padding: 10px 4px;
}
.earn-stat + .earn-stat::before {
  content: ''; position: absolute; left: 0; top: 28%; bottom: 28%;
  width: 1px; background: var(--border);
}
.earn-stat b {
  font-size: 13px; font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums;
}
.earn-stat i {
  font-style: normal; font-size: 8.5px; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* How it works — one continuous rail with medallions sitting on it, instead of
   three separate cards with a stub of line between them. Reading it top to
   bottom should feel like a single path, which is the actual promise. */
.earn-how { position: relative; margin-bottom: 26px; }
/* left = step border (1) + step padding-left (13) + half the 32px medallion,
   minus half the rail's own width. Any change to that padding has to come
   back here, or the rail slides off the medallion centres. */
.earn-how::before {
  content: '';
  position: absolute; left: 29px; top: 26px; bottom: 26px; width: 2px;
  background: linear-gradient(180deg, var(--green), var(--green-line) 55%, transparent);
  border-radius: 2px;
}
.earn-how-step {
  position: relative;
  display: flex; align-items: center; gap: 14px;
  padding: 14px 15px 14px 13px; margin-bottom: 9px;
  border-radius: var(--radius-md);
  background: var(--surface-1); border: 1px solid var(--border);
  box-shadow: var(--lift);
  transition: border-color 0.25s var(--ease), background 0.25s var(--ease);
}
.earn-how-step:last-child { margin-bottom: 0; }
.earn-how-num {
  position: relative; z-index: 1;
  flex-shrink: 0;
  width: 32px; height: 32px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; font-weight: 800;
  background: linear-gradient(135deg, var(--green), var(--green-bright));
  color: var(--green-ink);
  /* The dark ring is what lets the medallion sit ON the rail rather than the
     rail appearing to pass through it. */
  box-shadow:
    0 0 0 4px var(--bg),
    0 4px 16px -4px var(--green-glow);
}
.earn-how-step p { font-size: 13.5px; font-weight: 500; line-height: 1.45; color: var(--text); }

/* Link */
.earn-link-label {
  font-size: 10px; font-weight: 700; letter-spacing: 0.2em;
  text-transform: uppercase; color: rgba(247, 242, 238, 0.46); margin-bottom: 9px;
}
/* The link is the one thing on this screen the user is meant to take away, so
   it is styled as a solid inset token rather than a dashed placeholder box —
   dashed borders read as "empty slot", which is the opposite of the message. */
.earn-link-box {
  position: relative;
  padding: 16px 44px 16px 16px;
  border-radius: var(--radius-md);
  background: rgba(0, 0, 0, 0.34);
  border: 1px solid var(--accent-line);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.45);
  font-size: 12.5px; font-weight: 500; color: var(--accent-bright);
  letter-spacing: 0.01em;
  word-break: break-all; cursor: pointer;
  transition: border-color 0.25s var(--ease), background 0.25s var(--ease),
              color 0.25s var(--ease);
}
/* Copy affordance drawn in CSS — no extra markup, nothing to translate. */
.earn-link-box::after {
  content: '';
  position: absolute; top: 50%; right: 15px;
  width: 13px; height: 13px; margin-top: -8px;
  border: 1.5px solid currentColor; border-radius: 3px;
  box-shadow: -3px 3px 0 -1.5px rgba(0, 0, 0, 0.34), -3px 3px 0 0 currentColor;
  opacity: 0.55;
  transition: opacity 0.25s var(--ease);
}
.earn-link-box:active { transform: scale(0.99); }
.earn-link-box.copied {
  border-color: var(--green); background: var(--green-soft); color: var(--green-bright);
}
.earn-link-box.copied::after { opacity: 1; }

.earn-link-actions { display: flex; gap: 9px; margin: 10px 0 26px; }
.earn-btn {
  flex: 1;
  display: flex; align-items: center; justify-content: center; gap: 7px;
  padding: 14px 12px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-2);
  background: var(--surface-2);
  box-shadow: var(--lift);
  color: var(--text);
  font-family: var(--sans); font-size: 13.5px; font-weight: 600;
  cursor: pointer;
  transition: transform 0.2s var(--ease), background 0.25s;
}
.earn-btn svg { width: 15px; height: 15px; }
.earn-btn:active { transform: scale(0.97); }
.earn-btn.primary {
  background: linear-gradient(120deg, var(--green), var(--green-bright));
  border-color: transparent; color: var(--green-ink); font-weight: 700;
  box-shadow: 0 8px 22px -12px var(--green-glow), var(--lift);
}

/* Section titles — the label plus a hairline that runs to the right edge.
   Gives the lower half of the screen real structure instead of small grey
   text floating above a stack of cards. */
.earn-section-title {
  display: flex; align-items: center; gap: 12px;
  font-size: 10px; font-weight: 700; letter-spacing: 0.2em;
  text-transform: uppercase; color: rgba(247, 242, 238, 0.5);
  margin: 0 0 13px;
}
.earn-section-title::after {
  content: '';
  flex: 1; height: 1px;
  background: linear-gradient(90deg, var(--border-2), transparent);
}
.earn-activity-hint {
  font-size: 11.5px; line-height: 1.5; color: var(--text-faint);
  margin: -5px 0 12px;
}

/* Rewards */
.earn-rewards { margin-bottom: 26px; }
.earn-reward {
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--surface-1);
  box-shadow: var(--lift);
  padding: 13px 14px 11px;
  margin-bottom: 9px;
  transition: border-color 0.25s, background 0.25s;
}
/* An affordable reward is the moment this screen is selling. It gets the same
   treatment the store gives .product-card.featured — lit fill plus an outer
   halo — so it visibly separates from the ones still locked. */
.earn-reward.ready {
  border-color: var(--green-line);
  background: linear-gradient(135deg, rgba(42, 212, 164, 0.16), var(--surface-1));
  box-shadow: var(--lift), 0 0 26px -12px var(--green-glow);
}
.earn-reward-top { display: flex; align-items: center; gap: 12px; }
/* Given the same tile the store gives a product icon, so a reward reads as a
   thing you acquire rather than a bullet point. Mint instead of violet — it is
   bought with credits, not money. */
.earn-reward-icon {
  width: 42px; height: 42px; flex-shrink: 0;
  border-radius: var(--radius-sm);
  display: flex; align-items: center; justify-content: center;
  font-size: 20px; line-height: 1;
  background: linear-gradient(150deg, rgba(42, 212, 164, 0.14), rgba(42, 212, 164, 0.03));
  border: 1px solid rgba(42, 212, 164, 0.22);
  box-shadow: var(--lift);
}
.earn-reward.ready .earn-reward-icon {
  border-color: var(--green-line);
  background: linear-gradient(150deg, rgba(42, 212, 164, 0.26), rgba(42, 212, 164, 0.06));
}
.earn-reward-info { flex: 1; min-width: 0; }
.earn-reward-info b {
  display: block; font-size: 14px; font-weight: 700; color: var(--text);
}
.earn-reward-info i {
  display: block; font-style: normal; font-size: 12px;
  color: var(--text-muted); margin-top: 2px;
}
.earn-reward-btn {
  flex-shrink: 0;
  min-height: 40px;
  padding: 9px 15px;
  border-radius: var(--radius-full);
  border: 1px solid transparent;
  background: linear-gradient(135deg, var(--green-bright), var(--green));
  color: var(--green-ink);
  font-family: var(--sans); font-size: 12.5px; font-weight: 700;
  cursor: pointer; white-space: nowrap;
  box-shadow: 0 4px 14px -6px var(--green-glow);
  transition: transform 0.2s var(--ease);
}
.earn-reward-btn:active { transform: scale(0.95); }
.earn-reward-btn:disabled {
  background: transparent; border-color: var(--border-2);
  color: var(--text-faint); cursor: default; box-shadow: none;
}
.earn-reward-bar {
  height: 3px; border-radius: 100px;
  background: rgba(255, 255, 255, 0.07);
  margin-top: 11px; overflow: hidden;
}
.earn-reward-bar span {
  display: block; height: 100%; border-radius: 100px;
  background: linear-gradient(90deg, var(--green), var(--green-bright));
  transition: width 0.6s var(--ease);
}

/* Activity */
.earn-activity { margin-bottom: 10px; }
.earn-activity-row {
  display: flex; align-items: center; gap: 10px;
  padding: 11px 13px; margin-bottom: 7px;
  border-radius: var(--radius-sm);
  background: var(--surface-1); border: 1px solid var(--border);
  box-shadow: var(--lift);
}
.earn-activity-icon { font-size: 13px; flex-shrink: 0; }
.earn-activity-name {
  flex: 1; min-width: 0;
  font-size: 13px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.earn-activity-status {
  flex-shrink: 0; font-size: 11px; color: var(--text-faint); font-weight: 500;
}
.earn-empty {
  padding: 26px 18px; text-align: center;
  font-size: 13px; color: var(--text-faint); line-height: 1.5;
  border-radius: var(--radius-md);
  border: 1px dashed var(--border-2);
}

/* ═══════════════════════════════════════════════════════
   RESPONSIVE — COMPACT PHONES
   Everything above is sized for a ~390px iPhone. Android
   handsets are commonly 360px (and 320px on the smallest
   iPhones), where fixed paddings and the 4-block countdown
   run out of room. These tiers claw that room back.
   ═══════════════════════════════════════════════════════ */

/* ─── Tier 1: ≤ 400px — most Android phones ─── */
@media (max-width: 400px) {
  .view { padding-left: 14px; padding-right: 14px; }

  /* Side margins must match .view's padding, or the sticky backdrop
     stops short of the screen edge and leaves visible strips. */
  .store-header { margin: 0 -14px; padding: 18px 14px 26px; }
  .store-title { font-size: 24px; }
  .store-subtitle { letter-spacing: 0.26em; font-size: 8px; }
  .brand-avatar { width: 40px; height: 40px; }
  .store-brand { gap: 9px; }
  .lang-selector { font-size: 11.5px; padding: 8px 26px 8px 11px; }

  /* Promo banner: only the box padding is tightened. The title, timer and the
     Claim button keep their original sizes on purpose — this banner is the
     conversion element and it should look identical on every phone. */
  .promo-banner { padding: 13px 14px; gap: 9px 10px; }

  /* Category tabs: 4 tabs must not need a scroll on a 360px screen */
  .category-tabs { gap: 6px; padding-left: 14px; padding-right: 14px; margin: 0 -14px 16px; }
  .category-tab { padding: 8px 14px; font-size: 12.5px; }

  /* Product rows */
  .product-card { padding: 13px 12px; gap: 11px; }
  .product-icon { width: 40px; height: 40px; }
  .product-icon svg { width: 22px; height: 22px; }
  .product-name { font-size: 13.5px; }
  .product-desc { font-size: 11px; }
  .product-price { font-size: 17px; }
  .product-old-price { font-size: 11px; }
  .product-price-block { gap: 0; }

  /* Payment */
  .payment-product-summary { padding: 12px 13px; gap: 11px; }
  .payment-method-card { padding: 13px; gap: 11px; }
  .method-name { font-size: 14px; }
  .price-value { font-size: 29px; }

  /* Earn — scaled down, but the balance keeps its presence: it is the one
     number the whole screen exists to show, so it gives up the least. */
  .earn-hero h2 { font-size: 31px; }
  .earn-hero p { font-size: 13px; }
  .earn-balance-main { padding: 15px 15px 14px; gap: 11px; }
  .earn-balance-value { font-size: 31px; }
  .earn-gem { font-size: 19px; }
  .earn-ratio-caption { font-size: 10.5px; }
  .earn-stat { padding: 9px 3px; gap: 4px; }
  .earn-stat b { font-size: 12.5px; }
  .earn-stat i { font-size: 8px; letter-spacing: 0.08em; }
  .earn-how::before { left: 27px; }   /* padding-left drops to 11 here */
  .earn-how-step { padding: 12px 12px 12px 11px; gap: 12px; }
  .earn-how-step p { font-size: 13px; }
  .earn-link-box { padding: 14px 40px 14px 14px; }
  .earn-teaser-body { padding: 14px; gap: 11px; }
  .earn-teaser-text h3 { font-size: 14.5px; }
  .earn-teaser-text p { font-size: 12px; }
  .earn-reward-btn { padding: 8px 13px; font-size: 12px; }
}

/* ─── Tier 2: ≤ 365px ─── */
@media (max-width: 365px) {
  /* Label-only tabs, so this is just type trimming — four words still have to
     fit a 320px strip without it silently becoming a scroller. */
  .category-tab { padding: 7px 11px; font-size: 12px; min-height: 38px; }
  /* At this width the longest German/Russian names still don't fit one line,
     and there is no padding left to take. So they wrap instead of ellipsing —
     a card that is one line taller beats a product whose name is cut off. */
  .product-name {
    font-size: 13px;
    white-space: normal;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    line-height: 1.28;
  }
  .product-price { font-size: 16px; }
  .summary-price { font-size: 17px; }
  .earn-hero h2 { font-size: 27px; }
  .earn-balance-value { font-size: 28px; }
  .earn-gem { font-size: 18px; }
  .earn-stat i { font-size: 7.5px; }
}

/* ─── Tier 3: 320px (iPhone SE 1st gen) ───
   The last width where the Claim button can't sit beside the timer, which would
   put the unclaimed banner on two rows while the claimed one stays on one.
   A smaller CTA frees exactly enough width to keep them level. */
@media (max-width: 340px) {
  .promo-banner { padding: 12px; gap: 9px 10px; }
  /* Below this the pill can't sit beside the copy at a size worth tapping, so
     it takes its own row instead of shrinking under the target size. */
  .promo-left { flex: 1 1 100%; min-width: 0; }
  .promo-cta { flex: 1 1 100%; margin-left: 0; min-width: 0; font-size: 13px; }
  .pl-main { font-size: 12px; }
  /* Around 95px per stat, the longest labels ("HANNO COMPRATO") stop fitting
     beside their number, so the pair stacks rather than ellipsing. */
  .earn-stat { flex-direction: column; align-items: center; gap: 2px; padding: 8px 3px; }
}

/* ═══════════════════════════════════════════
   PURCHASES
   ═══════════════════════════════════════════ */

/* Store-screen entry point. Sits below the earn teaser and stays quiet — it's
   for people coming back, not part of the buying path. */
.orders-link {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  margin-top: 12px;
  padding: 13px 15px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--surface-1);
  box-shadow: var(--lift);
  color: var(--text-muted);
  font-family: var(--sans);
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.25s var(--ease), color 0.2s;
}

.orders-link:hover { background: var(--surface-2); color: var(--text); }
.orders-link:active { transform: scale(0.99); }
.orders-link svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--accent); }
.orders-link span { flex: 1; text-align: left; }

.orders-link b {
  font-size: 11px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: var(--radius-full);
  background: var(--accent-soft);
  color: var(--accent-bright);
  font-variant-numeric: tabular-nums;
}

.orders-hero { text-align: center; padding: 6px 0 20px; }

.orders-hero h2 {
  font-family: var(--serif);
  font-size: 30px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
  margin-bottom: 6px;
}

.orders-hero p { font-size: 13px; color: var(--text-muted); }

.orders-list { display: flex; flex-direction: column; gap: 10px; }

.order-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--surface-1);
  box-shadow: var(--lift);
}

.order-info { flex: 1; min-width: 0; }

.order-name {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text);
}

.order-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 8px;
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--text-faint);
}

/* Status carries colour as well as words, so a glance is enough */
.order-status {
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 2.5px 7px;
  border-radius: var(--radius-full);
  white-space: nowrap;
}

.order-status.ok { background: var(--green-soft); color: var(--green-bright); }
.order-status.wait { background: rgba(232, 196, 119, 0.13); color: #f0c869; }
.order-status.off { background: rgba(255, 107, 157, 0.12); color: #ff9ec0; }

.order-amount {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}

.orders-empty {
  text-align: center;
  padding: 40px 20px;
  font-size: 13.5px;
  color: var(--text-faint);
}

/* ─── Credit gain ───
   Fires when the balance is higher than the last one this device saw. */
.earn-balance-value { position: relative; }

.earn-balance-value.bumped {
  animation: creditBump 0.7s var(--ease);
}

@keyframes creditBump {
  0% { transform: scale(1); }
  35% { transform: scale(1.14); }
  100% { transform: scale(1); }
}

.credit-gain {
  position: absolute;
  top: -4px;
  left: 50%;
  font-size: 15px;
  font-weight: 700;
  color: var(--green-bright);
  white-space: nowrap;
  pointer-events: none;
  animation: creditFloat 1.8s var(--ease) forwards;
}

@keyframes creditFloat {
  0% { opacity: 0; transform: translate(-50%, 6px) scale(0.85); }
  18% { opacity: 1; transform: translate(-50%, -8px) scale(1); }
  70% { opacity: 1; transform: translate(-50%, -26px) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -42px) scale(0.95); }
}

@media (prefers-reduced-motion: reduce) {
  .earn-balance-value.bumped { animation: none; }
  .credit-gain { animation: none; opacity: 1; transform: translate(-50%, -22px); }
}

/* ═══════════════════════════════════════════════════════
   PRESENCE — "Online" beside the wordmark
   Replaces the static "Exclusive Hub" tagline. The dot is
   the whole message; the words just name it.
   ═══════════════════════════════════════════════════════ */
/* Small on purpose. Set at 10.5px uppercase with wide tracking it ran nearly
   as wide as the wordmark above it and competed with the name for the eye —
   this is a status line, so it should be read second or not at all. */
.store-presence {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 8.5px;
  font-weight: 600;
  letter-spacing: 0.045em;
  text-transform: uppercase;
  color: var(--green-bright);
  opacity: 0.88;
}

.presence-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--green-bright);
  flex-shrink: 0;
  animation: presencePulse 2.4s ease-in-out infinite;
}

@keyframes presencePulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(85, 230, 189, 0.5); }
  70%      { box-shadow: 0 0 0 4px rgba(85, 230, 189, 0); }
}

/* ═══════════════════════════════════════════════════════
   BUNDLE ADD-ON
   ═══════════════════════════════════════════════════════ */
.bundle-box { margin-bottom: 22px; animation: fadeUp 0.45s var(--ease) both; }

/* ─── The closed offer ───
   One line, no fill, dashed outline. It has to be findable and ignorable at
   the same time: anyone who wants a second item will read it, and anyone
   deciding whether to buy the first one can pass over it without stopping. */
.bundle-teaser {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  min-height: 46px;
  padding: 11px 13px;
  border-radius: var(--radius-md);
  background: transparent;
  border: 1px dashed var(--border-2);
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-muted);
  text-align: left;
  cursor: pointer;
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease),
              transform 0.15s var(--ease);
}

.bundle-teaser:active {
  transform: scale(0.99);
  background: var(--surface-1);
  border-color: var(--accent-line);
}

.bt-plus {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-3);
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
}

/* The add-on's name and price are the only words here that carry weight —
   everything around them stays at the muted body colour. */
.bt-text { flex: 1; min-width: 0; }
.bt-text b { color: var(--text); font-weight: 600; }

.bt-chev { width: 15px; height: 15px; flex-shrink: 0; color: var(--text-faint); }

.bundle-head {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--text-muted);
  margin-bottom: 8px;
}

.bundle-row {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 12px 13px;
  border-radius: var(--radius-md);
  background: var(--surface-1);
  border: 1px dashed var(--border-2);
  cursor: pointer;
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease),
              transform 0.15s var(--ease);
}

.bundle-row:active { transform: scale(0.99); }

/* Solid border once taken: dashed reads as "optional extra", solid as "part
   of this order now" — the state change is the receipt. */
.bundle-box.added .bundle-row {
  border-style: solid;
  border-color: var(--green-line);
  background: var(--green-soft);
}

.bundle-check {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 700;
  background: var(--surface-3);
  color: var(--text-muted);
  transition: background 0.2s, color 0.2s;
}

.bundle-box.added .bundle-check {
  background: linear-gradient(135deg, var(--green-bright), var(--green));
  color: var(--green-ink);
}

.bundle-info { flex: 1; min-width: 0; }

.bundle-info b {
  display: block;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
}

.bundle-info i {
  display: block;
  font-style: normal;
  font-size: 11px;
  color: var(--text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bundle-price { flex-shrink: 0; text-align: right; }

.bundle-plus {
  display: block;
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.bundle-save {
  display: block;
  font-size: 10px;
  font-weight: 700;
  color: var(--green-bright);
}

/* Reads like a footnote, deliberately. Its job is to remove the "am I being
   charged for both of these?" doubt, not to pitch the add-on a second time —
   set any louder and the offer starts to feel like pressure. */
.bundle-note {
  margin-top: 7px;
  padding: 0 3px;
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--text-faint);
}

/* ═══════════════════════════════════════════════════════
   STICKY PAY BAR
   Appears only once the real button has scrolled out of
   view, so exactly one CTA is ever on screen.
   ═══════════════════════════════════════════════════════ */
.sticky-pay {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 520px;
  margin: 0 auto;
  padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(to top, var(--bg) 62%, rgba(14, 7, 25, 0.92));
  border-top: 1px solid var(--border);
  transform: translateY(115%);
  transition: transform 0.28s var(--ease);
  pointer-events: none;
}

.sticky-pay.show { transform: translateY(0); pointer-events: auto; }

.sticky-pay-info { display: flex; flex-direction: column; gap: 1px; min-width: 0; }

.sticky-pay-label {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.sticky-pay-value {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.sticky-pay-btn {
  flex: 1;
  padding: 14px 18px;
  border: none;
  border-radius: 14px;
  background: linear-gradient(135deg, var(--accent), #9a55d8);
  color: #fff;
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 8px 22px -10px var(--accent-glow), var(--lift);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: transform 0.15s var(--ease);
}

.sticky-pay-btn:active { transform: scale(0.98); }

/* ═══════════════════════════════════════════════════════
   CROSS-SELL (success screen)
   ═══════════════════════════════════════════════════════ */
.cross-sell { width: 100%; margin: 4px 0 18px; animation: fadeUp 0.5s var(--ease) both; }

.cross-title {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 10px;
  text-align: center;
}

.cross-grid { display: flex; gap: 9px; }

.cross-card {
  flex: 1;
  min-width: 0;
  padding: 12px;
  border-radius: var(--radius-md);
  background: var(--surface-1);
  border: 1px solid var(--accent-line);
  box-shadow: var(--lift);
  cursor: pointer;
  text-align: left;
  transition: background 0.2s var(--ease), transform 0.15s var(--ease);
}

.cross-card:active { transform: scale(0.98); background: var(--surface-2); }

.cross-name {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text);
  line-height: 1.3;
  margin-bottom: 8px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.cross-foot { display: flex; align-items: center; justify-content: space-between; gap: 6px; }

.cross-price {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.cross-add {
  font-size: 10px;
  font-weight: 700;
  padding: 4px 10px;
  border-radius: var(--radius-full);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  color: var(--accent-bright);
}

/* ═══════════════════════════════════════════════════════
   RESUMABLE ORDER (My Purchases)
   ═══════════════════════════════════════════════════════ */
.order-card.resumable {
  cursor: pointer;
  border-color: rgba(232, 196, 119, 0.3);
  transition: background 0.2s var(--ease), transform 0.15s var(--ease);
}

.order-card.resumable:active { transform: scale(0.99); background: var(--surface-2); }
.order-card.resuming { opacity: 0.55; pointer-events: none; }

/* Room for the × so a long product name never runs under it. */
.order-card { position: relative; padding-right: 34px; }

/* Quiet until touched. It sits on top of a card that is itself tappable, so
   it has to be findable without ever competing with the card's own action. */
.order-remove {
  /* 24px was a 24px target. The button is visually small on purpose — it
     removes something — but the area a thumb has to hit is not. */
  position: absolute;
  top: 0;
  right: 0;
  width: 40px;
  height: 40px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--text-faint);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 2;
  transition: color 0.2s, background 0.2s;
}

.order-remove svg { width: 13px; height: 13px; }

.order-remove:hover,
.order-remove:active {
  color: #ff9ec0;
  background: rgba(255, 107, 157, 0.14);
}

.order-card.removing {
  opacity: 0;
  transform: scale(0.96);
  transition: opacity 0.2s var(--ease), transform 0.2s var(--ease);
  pointer-events: none;
}

/* ─── Shared top bar (payment / earn / purchases) ───
   Back on the left, language on the right. */
.view-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-top: 6px;
}

/* Same idea as the base rule: the padding IS the target. 14/4 gave a 36px
   control on every inner screen. */
.view-topbar .back-btn { padding: 14px 12px 12px 0; margin-left: -2px; }

/* Slightly smaller than the store header's, so it reads as a utility on these
   screens rather than a second headline element. */
.view-topbar .lang-selector {
  font-size: 11.5px;
  padding: 7px 26px 7px 11px;
  background-position: right 10px center;
}

.order-resume {
  margin-top: 5px;
  font-size: 11px;
  font-weight: 700;
  color: #f0c869;
}

/* ═══════════════════════════════════════════════════════
   FULL-SCREEN OVERLAYS — welcome & wheel
   ═══════════════════════════════════════════════════════ */
body.no-scroll { overflow: hidden; }

.welcome-overlay,
/* Anchored near the top, not dead-centre. Centring a tall sheet inside the
   Telegram webview left the title sitting well below the middle of the screen —
   the viewport is taller than the sheet, but the sheet's own mass is at the
   bottom (wheel + button), so optically the whole thing hung low. Scrolls if a
   short screen can't take it. */
.wheel-overlay {
  position: fixed;
  inset: 0;
  /* Explicit height as well as inset, so the overlay measures the same visual
     viewport the page does when the iOS toolbar is collapsed. */
  height: 100dvh;
  overscroll-behavior: contain;
  z-index: 500;
  display: flex;
  align-items: stretch;
  justify-content: center;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* The bottom padding is what positions the pinned CTA. Flush with the bottom
     edge put it BELOW where a thumb naturally lands, and left the wheel
     stranded with a gap under it; this lifts the button into the reach zone
     and takes the same space out of the gap above it. */
  padding: max(20px, 4vh) 22px calc(env(safe-area-inset-bottom, 0px) + max(28px, 13vh));
  background: rgba(8, 4, 15, 0.9);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  opacity: 0;
  transition: opacity 0.3s var(--ease);
}

.welcome-overlay.open,
.wheel-overlay.open { opacity: 1; }

/* Lighter scrim than the wheel's. The wheel is a game and deserves the room;
   the welcome is a greeting, and the store staying faintly visible behind it
   is the whole point — it frames the gift instead of replacing the shop. */
.welcome-overlay {
  background: rgba(8, 4, 15, 0.62);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}

/* ─── Welcome card ───
   A contained card, not a takeover. Painting the whole screen made a first
   visit feel like an interstitial ad and hid the fact that there was a shop
   behind it at all; letting the store show through the blur at the edges says
   "there's something here, and this is a gift on top of it". */
.welcome-card {
  position: relative;
  width: 100%;
  max-width: 340px;
  padding: 34px 26px 24px;
  border-radius: 26px;
  text-align: center;
  overflow: hidden;
  background:
    linear-gradient(165deg, rgba(59, 28, 86, 0.96), rgba(19, 10, 31, 0.98));
  border: 1px solid rgba(192, 120, 245, 0.28);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.1),
    0 30px 70px -30px rgba(0, 0, 0, 0.9),
    0 0 50px -20px var(--accent-glow);
  animation: welcomeIn 0.5s var(--ease) 0.06s both;
}

@keyframes welcomeIn {
  from { opacity: 0; transform: translateY(16px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* Light source behind the gem, inside the card. */
.welcome-glow {
  position: absolute;
  top: -70px;
  left: 50%;
  width: 240px;
  height: 240px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(192, 120, 245, 0.45), transparent 66%);
  filter: blur(26px);
  pointer-events: none;
  animation: welcomeBreath 4.5s ease-in-out infinite;
}

@keyframes welcomeBreath {
  0%, 100% { opacity: 0.7; transform: translateX(-50%) scale(1); }
  50%      { opacity: 1;   transform: translateX(-50%) scale(1.1); }
}

.welcome-inner { position: relative; }

/* The store's own gem, not an emoji — the same mark that sits on every VIP
   product, so the gift reads as coming from this shop. */
.welcome-crest {
  width: 62px;
  height: 62px;
  margin: 0 auto 18px;
  border-radius: 20px;
  background: linear-gradient(150deg, var(--accent-soft), rgba(192, 120, 245, 0.04));
  border: 1px solid var(--accent-line);
  box-shadow: var(--lift), 0 8px 24px -12px var(--accent-glow);
  display: flex;
  align-items: center;
  justify-content: center;
}

.welcome-crest svg { width: 34px; height: 34px; }

.welcome-title {
  font-family: var(--serif);
  font-size: 33px;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.1;
  margin-bottom: 8px;
  background: linear-gradient(115deg, #ffffff 30%, var(--accent-bright));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

.welcome-sub {
  font-size: 14px;
  line-height: 1.5;
  color: var(--text-muted);
  margin-bottom: 26px;
}

.welcome-cta {
  width: 100%;
  padding: 16px 20px;
  border: none;
  border-radius: 15px;
  background: linear-gradient(135deg, #5fe9b3, #23c88a);
  color: var(--green-ink);
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 800;
  cursor: pointer;
  box-shadow: 0 12px 32px -14px var(--green-glow), var(--lift);
  animation: ctaBreath 2.6s ease-in-out infinite;
}

@keyframes ctaBreath {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.028); }
}

.welcome-cta:active { transform: scale(0.98); animation: none; }

.welcome-skip {
  margin-top: 10px;
  background: none;
  border: none;
  color: var(--text-faint);
  font-family: var(--sans);
  font-size: 12.5px;
  cursor: pointer;
  padding: 8px;
}

/* ═══════════════════════════════════════════════════════
   FORTUNE WHEEL
   ═══════════════════════════════════════════════════════ */
/* A full-height column, not a card floating in the middle. The button used to
   sit wherever the content happened to end — around 60% of the way down, which
   on a phone held one-handed is above where a thumb comfortably reaches. Now
   the stage fills the screen and the button is pinned to the bottom of it, so
   the thing you press is where your thumb already is, on every screen size. */
.wheel-sheet {
  position: relative;
  width: 100%;
  max-width: 380px;
  text-align: center;
  display: flex;
  flex-direction: column;
  animation: fadeUp 0.5s var(--ease) both;
}

.wheel-stage {
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* The wheel takes the slack between the heading and the button, and centres
   itself in whatever is left — so the spacing breathes on a tall phone and
   tightens on a short one instead of overflowing. */
.wheel-frame-wrap {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  padding: 4px 0;
}

/* Fixed to the screen, not to the sheet. The sheet is vertically centred in
   the overlay, so anchoring the × to its top edge parked the button halfway
   down the right-hand side — nowhere anyone looks for a close control. Top
   right of the viewport is where it belongs, and it stays there whether the
   wheel or the result is showing. */
.wheel-close {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 14px);
  right: 14px;
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  color: var(--text-muted);
  border-radius: 50%;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 2;
}

.wheel-title {
  font-family: var(--serif);
  font-size: 32px;
  font-weight: 600;
  margin-bottom: 4px;
}

.wheel-sub {
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 0;
}

.wheel-frame {
  position: relative;
  width: min(80vw, 310px, 44vh);
  aspect-ratio: 1;
  margin: 0 auto;
  flex-shrink: 0;
}

/* Halo behind the wheel. Sells it as a lit object on a dark stage rather than
   a graphic pasted onto the overlay. */
.wheel-frame::before {
  content: '';
  position: absolute;
  inset: -14%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(192, 120, 245, 0.28), transparent 62%);
  filter: blur(20px);
  pointer-events: none;
}

/* The pointer overlaps the bezel rather than floating above it, so which wedge
   it means is never ambiguous at the moment the wheel stops. Built from two
   clipped blocks — a gradient can't be put on a CSS triangle made of borders,
   and a flat triangle beside a brushed-metal ring looks like a placeholder. */
.wheel-pointer {
  position: absolute;
  top: -12px;
  left: 50%;
  transform: translateX(-50%);
  width: 26px;
  height: 34px;
  z-index: 4;
  background: linear-gradient(155deg, #fdf3cf 0%, #e8c477 30%, #8d6716 62%, #f2d491 100%);
  clip-path: polygon(50% 100%, 0 22%, 12% 0, 88% 0, 100% 22%);
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.65));
}

/* Bright bead where the pointer meets the rim — the tiny highlight that makes
   the whole thing read as machined metal. */
.wheel-pointer::after {
  content: '';
  position: absolute;
  top: 7px;
  left: 50%;
  transform: translateX(-50%);
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #fffdf3, #c9962f);
}

.wheel-spinner {
  width: 100%;
  height: 100%;
  /* JS writes rotate() every frame — deliberately NO css transition, or the
     two would fight and the tick positions would stop matching the picture. */
  will-change: transform;
  filter: drop-shadow(0 14px 34px rgba(0, 0, 0, 0.6));
}

.wheel-hub {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 15%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 28%, #3b1c56, #150920 70%);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--serif);
  font-size: clamp(15px, 5.2vw, 21px);
  font-weight: 700;
  line-height: 1;
  color: #f0dcff;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 3px 10px rgba(0, 0, 0, 0.7);
  z-index: 3;
  border: none;
  padding: 0;
  cursor: pointer;
  transition: transform 0.15s var(--ease), box-shadow 0.25s var(--ease);
}

/* A faint pulse ring: the only hint that the centre is pressable. Anything
   louder would compete with the SPIN button below the wheel. */
.wheel-hub::after {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: 50%;
  border: 1px solid rgba(240, 220, 255, 0.35);
  animation: hubPulse 2.4s ease-out infinite;
  pointer-events: none;
}

@keyframes hubPulse {
  0%   { opacity: 0.7; transform: scale(1); }
  70%  { opacity: 0;   transform: scale(1.5); }
  100% { opacity: 0;   transform: scale(1.5); }
}

.wheel-hub:active { transform: translate(-50%, -50%) scale(0.92); }

/* Nothing to press while it's already turning. */
.wheel-hub:disabled { cursor: default; }
.wheel-hub:disabled::after { animation: none; opacity: 0; }

/* Reserved height, so the near-miss flash never shoves the button down. */
.wheel-nearmiss {
  height: 22px;
  margin-top: 4px;
  flex-shrink: 0;
  font-size: 15px;
  font-weight: 800;
  color: var(--gold);
  opacity: 0;
  transform: scale(0.85);
  transition: opacity 0.2s var(--ease), transform 0.2s var(--ease);
}

.wheel-nearmiss.show { opacity: 1; transform: scale(1); }

/* ─── SPIN button ───
   Cut from the same metal as the bezel. A flat violet slab under a brushed-gold
   wheel looked like a default button someone forgot to style — the wheel is the
   most finished object in the app and its own control was the least. Same
   gradient stops as #bezel in the SVG, so they read as one piece of hardware. */
.wheel-spin-btn {
  position: relative;
  width: 100%;
  /* auto top margin = pinned to the bottom of the stage column */
  margin-top: auto;
  flex-shrink: 0;
  padding: 17px 18px;
  border: none;
  border-radius: var(--radius-full);
  overflow: hidden;
  background: linear-gradient(135deg, #fdf3cf 0%, #e8c477 22%, #b78d22 52%, #f2d491 74%, #9c7420 100%);
  color: #2b1a02;
  font-family: var(--sans);
  font-size: 15px;
  font-weight: 800;
  /* Letterspaced small caps rather than shouted uppercase — the wheel above is
     the loud object and the control under it should read as engraved on it,
     not as a banner ad. Sized so the longest translation still fits one line
     on a 320px screen. */
  letter-spacing: 0.11em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  box-shadow:
    inset 0 1px 0 rgba(255, 253, 243, 0.75),
    inset 0 -1px 0 rgba(80, 55, 5, 0.4),
    0 14px 32px -14px rgba(232, 196, 119, 0.85),
    0 4px 12px -6px rgba(0, 0, 0, 0.7);
  animation: ctaBreath 2.2s ease-in-out infinite;
}

/* Light travelling across the metal. Motion is what makes gold look polished
   rather than painted. */
.wheel-spin-btn::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -50%;
  width: 40%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.75), transparent);
  animation: purchaseGlint 3.2s var(--ease) infinite;
  pointer-events: none;
}

.wheel-spin-btn > * { position: relative; z-index: 1; }

.wheel-spin-btn:active { transform: scale(0.98); }

.wheel-spin-btn:disabled,
.wheel-spin-btn.spinning {
  opacity: 0.55;
  cursor: default;
  animation: none;
}

/* The result screen is gone — see celebrateSpin() in app.js. The wheel
   stops on the winning wedge, confetti falls, and the sheet closes itself.
   Four result designs were built here before the simplest answer won: do not
   explain the wheel to someone who is looking at it. */


/* The old result CTA lived here. It is gone with the button — the sheet
   dismisses itself now. */

@media (max-width: 365px) {
  .welcome-title { font-size: 32px; }
  .wheel-title { font-size: 27px; }
  .sticky-pay-btn { font-size: 14px; padding: 13px 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .presence-dot,
  .welcome-glow,
  .welcome-cta,
  .promo-cta,
  .wheel-spin-btn { animation: none; }
}
