/* ============================================================================
   card-art.css — a loyalty card's artwork, stamp or point, and the ONLY
   stylesheet that draws it. Loaded by the customer webview (customer/app.html)
   and by every portal page that previews a card (stamp-card-form.html,
   points-setup.html, public/onboarding.html).

   BOTH CARD KINDS ARE THE SAME CARD HERE. Everything above the block — surface,
   gradient, texture, sheen, background photo, typography, logo, stickers,
   motion — is written against .sc-hero and applies to both. Only the block
   differs: .sc-grid for stamps, .sc-balance for a point balance. Adding a
   surface option therefore lands on both cards at once, which is the property
   that stopped the point card being drawn three separate ways.

   Before this file existed the webview drew .sc-hero from customer.css and the
   portal drew a lookalike .stampcard-preview from portal.css, each with its own
   gradient formula. That is why the portal preview stopped matching the
   customer card. One stylesheet + one renderer (card-art.js) makes them
   incapable of disagreeing — the same argument CardProgressMapper's class
   comment makes about reward counts.

   SELF-CONTAINED BY CONTRACT. The portal does not define the webview's
   palette, so every token consumed here carries an inline fallback. Do not add
   a bare var(--x) to this file.

   Variation is expressed ONLY as data-* attributes and custom properties set by
   the renderer. No option may branch the markup — that is what keeps each
   option a CSS rule that is tested once instead of a layout that has to be
   re-verified per tenant.
   ============================================================================ */

/* ======================== SCOPED RESET (load-bearing) ======================
   The card must render identically in any document, and the two documents that
   render it do NOT agree on their base styles: customer.css sets
   *{box-sizing:border-box}, zeroes default heading/paragraph margins and sets a
   base font; portal.css does none of those. Inheriting them made the same card
   10px taller in the portal than in the webview, with a 2px taller type pill —
   caught by the parity check, invisible to a reading of the CSS.

   So the card resets what it relies on, scoped to itself. Nothing here may be
   dropped on the assumption that "the page already does that": the whole point
   of this file is that it does not know what page it is on. */
.sc-hero, .sc-hero *, .sc-hero *::before, .sc-hero *::after{ box-sizing:border-box; }
.sc-hero h2, .sc-hero p, .sc-hero small{ margin:0; }

.sc-hero{
  /* ---- token fallbacks ----
     Font stacks are LITERAL, never read from the host: the webview calls its
     display family --disp while the portal calls it --font-disp and points it at
     a different typeface entirely (Bricolage Grotesque). Reading either name
     would make the card's type depend on which page it landed on. */
  --_text:'Hanken Grotesk',system-ui,sans-serif;
  --_disp:'Hanken Grotesk',system-ui,sans-serif;
  --_mono:'JetBrains Mono',ui-monospace,monospace;

  /* Brand ink and shadow tint come from the MODEL (--card-ink / --card-ring),
     emitted by the renderer from the tenant's brand colours. They used to be read
     from the webview's --brand-ink / --brand-ring, which the portal does not
     define — so a previewed card had a near-black tick and a grey shadow while
     the customer saw the tenant's accent colour. */
  --_ink:var(--card-ink,#1C1A17);
  --_ring:var(--card-ring,rgba(28,26,23,.18));
  --_gold-hi:var(--gold-hi,#EBD08A);
  --_ease:var(--ease,cubic-bezier(.32,.72,.3,1));
  font-family:var(--_text);

  /* ---- design defaults: these reproduce the pre-P1 card exactly, so a tenant
          who never opens Card Studio sees no change and any visual diff in
          verification is a real regression. ---- */
  --_base:var(--card-color,#8A5230);
  --_c1:color-mix(in srgb,var(--_base),#000 20%);
  --_c2:var(--card-accent,color-mix(in srgb,var(--_base),#000 52%));
  --_c3:color-mix(in srgb,var(--_base),#fff 24%);
  --_angle:var(--grad-angle,155deg);
  --_radius:var(--card-radius,24px);
  --_title:var(--title-color,#fff);
  --_desc:var(--desc-color,rgba(255,255,255,.76));

  position:relative; overflow:hidden;
  padding:18px;
  border-radius:var(--_radius);
  color:#fff;
  box-shadow:0 12px 30px var(--_ring);
  background:linear-gradient(var(--_angle),var(--_c1),var(--_c2));

  /* A flex column so `order` can move the header, description and stamp grid
     between named positions without the renderer emitting different markup.
     The background, texture and sheen layers are absolutely positioned, so they
     stay out of this flow. Default order matches the historical block layout. */
  display:flex; flex-direction:column;
}
.sc-head{ order:1; }
.sc-chips{ order:2; }
.sc-description{ order:2; }
.sc-grid, .sc-balance{ order:3; }
/* Slots the home hero fills with markup the design does not own — the tier chip
   and ready badge above, the action buttons below. See RCCardArt.point. */
.sc-hero-slot.top{ order:0; }
.sc-hero-slot.foot{ order:4; }

/* ============================== SURFACE ====================================
   data-grad. LINEAR is the default and is byte-identical to the gradient the
   webview used inline before this file.

   --card-accent is what makes these worth having: without it both stops are
   color-mix(base,#000 x%), so every tenant's card is two shades of one hue and
   they all read as the same card recoloured. The var() fallback means an unset
   accent silently keeps the old derived stop. */
.sc-hero[data-grad="solid"]{ background:var(--_base); }
.sc-hero[data-grad="linear"]{ background:linear-gradient(var(--_angle),var(--_c1),var(--_c2)); }
.sc-hero[data-grad="radial"]{ background:radial-gradient(135% 115% at 18% -10%,var(--_c1),var(--_c2) 78%); }
.sc-hero[data-grad="duotone"]{ background:linear-gradient(var(--_angle),var(--_c1) 0 46%,var(--_c2) 46% 100%); }
.sc-hero[data-grad="mesh"]{
  background:
    radial-gradient(70% 60% at 12% 8%,var(--_c3),transparent 62%),
    radial-gradient(80% 70% at 92% 16%,var(--_c2),transparent 60%),
    radial-gradient(95% 90% at 52% 112%,var(--_c1),transparent 70%),
    linear-gradient(var(--_angle),var(--_c1),var(--_c2));
}

/* ================================ SHEEN ==================================== */
.sc-sheen{ position:absolute; inset:0; pointer-events:none;
  background:radial-gradient(120% 90% at 86% -8%,rgba(255,255,255,.22),transparent 55%); }
.sc-hero[data-sheen="none"] .sc-sheen{ display:none; }
.sc-hero[data-sheen="specular"] .sc-sheen{
  background:linear-gradient(112deg,transparent 26%,rgba(255,255,255,.30) 45%,rgba(255,255,255,.06) 53%,transparent 62%); }
.sc-hero[data-sheen="holo"] .sc-sheen{
  mix-blend-mode:overlay; opacity:.55;
  background:linear-gradient(112deg,
    rgba(255,120,180,.42) 8%, rgba(255,214,120,.42) 26%, rgba(140,240,200,.42) 44%,
    rgba(130,180,255,.42) 62%, rgba(210,140,255,.42) 80%);
  background-size:220% 220%;
  background-position:calc(50% + var(--tilt-x,0) * 42%) calc(50% + var(--tilt-y,0) * 32%);
  transition:background-position 120ms ease-out; }

/* =============================== TEXTURE ===================================
   data-texture on the hero, drawn by the .sc-pattern layer. DOTS is today's. */
.sc-pattern{ position:absolute; inset:0; pointer-events:none;
  opacity:var(--texture-op,.19);
  background-image:
    radial-gradient(circle at 20% 25%,rgba(255,255,255,.54) 0 1px,transparent 1.6px),
    linear-gradient(115deg,transparent 0 46%,rgba(255,255,255,.16) 46.5% 47.5%,transparent 48%);
  background-size:54px 54px,160px 160px; }
.sc-hero[data-texture="none"] .sc-pattern{ display:none; }
.sc-hero[data-texture="carbon"] .sc-pattern{
  background-image:
    repeating-linear-gradient(45deg,rgba(255,255,255,.20) 0 1px,transparent 1px 4px),
    repeating-linear-gradient(-45deg,rgba(0,0,0,.20) 0 1px,transparent 1px 4px);
  background-size:auto,auto; }
.sc-hero[data-texture="linen"] .sc-pattern{
  background-image:
    repeating-linear-gradient(0deg,rgba(255,255,255,.16) 0 1px,transparent 1px 3px),
    repeating-linear-gradient(90deg,rgba(255,255,255,.10) 0 1px,transparent 1px 3px);
  background-size:auto,auto; }
.sc-hero[data-texture="topo"] .sc-pattern{
  background-image:repeating-radial-gradient(circle at 26% 18%,
    transparent 0 13px,rgba(255,255,255,.30) 13px 14px);
  background-size:auto; }
.sc-hero[data-texture="noise"] .sc-pattern{
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size:120px 120px; }
.sc-hero[data-texture="glass"] .sc-pattern{
  background-image:
    linear-gradient(160deg,rgba(255,255,255,.34) 0 22%,transparent 42%),
    linear-gradient(0deg,rgba(0,0,0,.22),transparent 55%);
  background-size:auto,auto; }

/* ============================= TYPOGRAPHY ==================================
   data-font. Deliberately system stacks rather than webfonts: card-art.css is
   loaded by pages that do not share the webview's @import, and a webfont here
   would mean a second network dependency plus a flash of fallback text on the
   customer's card. */
.sc-hero[data-font="serif"]{ --_disp:'Iowan Old Style','Palatino Linotype',Palatino,Georgia,'Times New Roman',serif; }
.sc-hero[data-font="mono"]{ --_disp:'JetBrains Mono',ui-monospace,monospace; }
.sc-hero[data-font="rounded"]{ --_disp:ui-rounded,'SF Pro Rounded','Hiragino Maru Gothic ProN','Segoe UI',system-ui,sans-serif; }
.sc-hero[data-case="upper"] .sc-identity h2{ text-transform:uppercase; letter-spacing:.005em; }

/* ============================ HEAD / IDENTITY ============================== */
.sc-head{ position:relative; display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.sc-identity{ min-width:0; display:flex; align-items:center; gap:10px; }
.sc-logo{ width:42px; height:42px; flex:none; display:flex; align-items:center; justify-content:center; overflow:hidden; border:1px solid rgba(255,255,255,.24); border-radius:13px; background:rgba(255,255,255,.16); box-shadow:0 6px 14px rgba(0,0,0,.14); }
.sc-logo img{ width:100%; height:100%; object-fit:cover; }
.sc-identity small{ display:block; max-width:160px; overflow:hidden; opacity:.7; font-size:8.5px; font-weight:850; letter-spacing:.1em; text-overflow:ellipsis; text-transform:uppercase; white-space:nowrap; }
.sc-identity h2{ max-width:190px; margin-top:3px; overflow-wrap:anywhere; color:var(--_title); font-family:var(--_disp); font-size:21px; font-weight:820; line-height:1.06; }
/* Scoped with .sc-hero deliberately. The reset above is `.sc-hero p { margin:0 }`
   at specificity (0,1,1); a bare `.sc-description` is (0,1,0) and loses to it, so
   the description's top margin vanished and every card lost 12px of height. */
.sc-hero .sc-description{ position:relative; max-width:84%; margin-top:12px; color:var(--_desc); font-size:11.5px; font-weight:620; line-height:1.45; }

/* ========================== STAMP GRID / SLOTS ============================= */
.sc-grid{ position:relative; display:grid; grid-template-columns:repeat(var(--stamp-cols,5),minmax(0,1fr)); gap:10px; margin:18px 2px 16px; }
.sc-slot{ position:relative; aspect-ratio:1; min-width:0; border-radius:50%; display:flex; align-items:center; justify-content:center; }
.sc-slot.empty{ background:rgba(255,255,255,.09); border:1.5px dashed rgba(255,255,255,.4); color:rgba(255,255,255,.72); }
.sc-slot.full{ background:#fff; color:var(--_ink); box-shadow:0 3px 8px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.9); }
.sc-slot.reward.empty{ border-style:solid; border-color:rgba(255,255,255,.6); background:rgba(255,255,255,.13); color:rgba(255,255,255,.92); }
.sc-slot svg{ width:44%; height:44%; }
.sc-slot .num{ font-family:var(--_mono); font-size:11px; font-weight:800; opacity:.85; }
.sc-slot img{ width:60%; height:60%; object-fit:contain; }
/* .has-icon strips the circle chrome so transparent PNG/SVG art renders as-is */
.sc-slot.has-icon, .sc-slot.reward.empty.has-icon{ background:none; border:none; box-shadow:none; border-radius:14px; }
.sc-slot.has-icon img{ width:92%; height:92%; }
.sc-slot.empty.has-icon img{ opacity:.42; }
.sc-slot.empty.has-icon.inv img{ filter:invert(1); }
.sc-slot.next:not(.has-icon){ border-style:solid; border-color:rgba(255,255,255,.66); background:rgba(255,255,255,.18); box-shadow:inset 0 0 0 1px rgba(255,255,255,.08); }
.sc-slot.next::after{ content:""; position:absolute; inset:auto auto -4px 50%; width:13px; height:3px; border-radius:999px; background:rgba(255,255,255,.88); transform:translateX(-50%); box-shadow:0 2px 7px rgba(0,0,0,.22); }
.sc-slot.next.has-icon img{ opacity:.72; filter:drop-shadow(0 3px 6px rgba(0,0,0,.22)); }
.sc-slot.reward.glow{ box-shadow:0 0 0 3px rgba(255,255,255,.28), 0 0 22px var(--_gold-hi); }
.sc-slot.reward.has-icon.glow{ box-shadow:none; filter:drop-shadow(0 0 10px var(--_gold-hi)); }

/* ============================== MOTION =====================================
   Entrance stagger is driven by --sc-i on each filled slot. data-anim="1" is
   set by the webview page container; the portal preview omits it, so the
   preview shows the settled state. `both` fill means the final frame is
   identical either way, which is what keeps preview parity exact. */
@keyframes scStampIn{ 0%{ transform:scale(1.9) rotate(-16deg); opacity:0; } 62%{ transform:scale(.94) rotate(2deg); opacity:1; } 100%{ transform:scale(1) rotate(0); opacity:1; } }
@keyframes scGlow{ 0%,100%{ filter:brightness(1); } 50%{ filter:brightness(1.14); } }
@media (prefers-reduced-motion:no-preference){
  /* WHICH animation a filled slot plays is the tenant's `motionEarn` choice, and
     it lives in the earn block further down — one definition serving both the
     card's entrance and a real earn. Only the fallback stagger lives here, for a
     card rendered without a data-earn attribute at all. */
  [data-anim="1"] .sc-slot.full{ animation-delay:calc(var(--sc-i,0)*45ms); }
  .sc-slot.reward.glow:not(.has-icon){ animation:scGlow 2.6s ease-in-out infinite; }
}

/* ========================= BACKGROUND IMAGERY ==============================
   The tenant's own photograph, composited UNDER the content and OVER the
   gradient surface.

   `data-bg` is the treatment, and it is not decoration. A merchant uploads a
   photo that is bright exactly where the white brand name sits; applied raw the
   card is unreadable, and no tooltip fixes that. The treatment is the mechanism
   that makes an arbitrary photo safe, which is why SCRIM — not COVER — is the
   default. --bg-dim drives the scrim, --bg-blur lets a busy photo become
   texture, and the focal point comes from object-position, matching the crop
   semantics the banner editor already uses. */
.sc-bg{ position:absolute; inset:0; overflow:hidden; pointer-events:none; border-radius:inherit; }
.sc-bg img{
  width:100%; height:100%;
  object-fit:cover;
  object-position:var(--bg-x,50%) var(--bg-y,50%);
  transform-origin:var(--bg-x,50%) var(--bg-y,50%);
  transform:scale(var(--bg-zoom,1));
  filter:blur(var(--bg-blur,0px));
}
/* TWO overlay layers, and the split matters.
   ::before TINTS (hue and saturation only, via the `color` blend mode).
   ::after DIMS (plain paint at --bg-dim).

   They were one layer at first, which made DUOTONE unreadable on a bright photo:
   `mix-blend-mode: color` takes hue and saturation from the wash but LUMINANCE
   FROM THE PHOTO, so a pale image stayed pale behind white text. Recolouring is
   not dimming. Duotone now does both — on-palette *and* legible. */
.sc-bg::before,
.sc-bg::after{ content:""; position:absolute; inset:0; pointer-events:none; }
.sc-bg::before{ display:none; }
.sc-bg::after{
  background:linear-gradient(var(--_angle),var(--_c1),var(--_c2));
  /* The 52 fallback IS the default overlay strength — the renderer omits any
     property equal to its default, so this value is what most cards actually
     use. It must track CardDesignResolver.DEFAULT_BACKGROUND_DIM; a check
     asserts every fallback here matches its JS default, because when this said
     45 the raised default silently did nothing. */
  opacity:calc(var(--bg-dim,52) / 100);
}

.sc-hero[data-bg="none"] .sc-bg{ display:none; }
/* COVER is the one treatment with no overlay at all. It is therefore the one that
   can be illegible; the editor's contrast check is what warns about it. */
.sc-hero[data-bg="cover"] .sc-bg::after{ display:none; }

/* brightness(.8) is what makes this a duotone rather than a tint: a real duotone
   maps the photo into the palette's own tonal RANGE, and the card's palette is
   dark. Without it a pale photo stayed pale — the title measured exactly 3.00:1,
   sitting on the AA limit with no headroom for local bright spots. */
.sc-hero[data-bg="duotone"] .sc-bg img{ filter:blur(var(--bg-blur,0px)) grayscale(1) contrast(1.08) brightness(.8); }
.sc-hero[data-bg="duotone"] .sc-bg::before{
  display:block;
  background:linear-gradient(var(--_angle),var(--_c1),var(--_c2));
  mix-blend-mode:color;
}

/* The image occupies a band and dissolves into flat colour, so the stamp grid
   always sits on solid ground. A mask, not a gradient overlay, so the photo
   genuinely fades rather than being veiled. */
.sc-hero[data-bg="top-bleed"] .sc-bg{
  -webkit-mask-image:linear-gradient(180deg,#000 0 46%,transparent 78%);
          mask-image:linear-gradient(180deg,#000 0 46%,transparent 78%);
}
.sc-hero[data-bg="bottom-fade"] .sc-bg{
  -webkit-mask-image:linear-gradient(0deg,#000 0 46%,transparent 78%);
          mask-image:linear-gradient(0deg,#000 0 46%,transparent 78%);
}

/* ---- the header's own legibility band ----
   Only when there is a photo. Dimming the WHOLE image enough to guarantee a
   readable title (measured: ~78%) leaves the photograph a ghost — the tenant
   uploaded art and got a veil. A local band behind the header instead keeps the
   body of the image bright while the text sits on something dark, which is what
   the overlay strength alone could not do.

   Painted as the header's own background with a bleeding negative margin, NOT as
   a child at z-index:-1: .sc-head is positioned but creates no stacking context,
   so a negative z-index child would drop behind the card's own background. */
.sc-hero:not([data-bg="none"])[data-header="top-left"] .sc-head,
.sc-hero:not([data-bg="none"])[data-header="top-center"] .sc-head{
  margin:-18px -18px 0; padding:18px 18px 12px;
  background:linear-gradient(180deg,rgba(0,0,0,.46),rgba(0,0,0,.20) 64%,transparent);
}
.sc-hero:not([data-bg="none"])[data-header="bottom"] .sc-head{
  margin:14px -18px -18px; padding:12px 18px 18px;
  /* The photo already has the card-wide treatment. Keeping another gradient
     on this negative-margin block creates a visible seam when the header is
     moved below the stamp grid. */
  background:none;
}

/* ============================ LAYOUT PLACEMENT =============================
   Every option moves content between NAMED positions, never to free pixels, so
   each is one CSS rule tested once at 320px and in RTL instead of a coordinate
   space that has to be re-validated per tenant. */

/* -- header -- */
.sc-hero[data-header="top-center"] .sc-head{ flex-direction:column; align-items:center; text-align:center; gap:9px; }
.sc-hero[data-header="top-center"] .sc-identity{ flex-direction:column; align-items:center; }
.sc-hero[data-header="top-center"] .sc-identity small{ max-width:none; }
.sc-hero[data-header="top-center"] .sc-identity h2{ max-width:none; }
.sc-hero[data-header="top-center"] .sc-description{ max-width:100%; text-align:center; }

/* The header moves below the stamps. This is what TOP_BLEED imagery is for:
   photo at the top, identity at the foot. */
.sc-hero[data-header="bottom"] .sc-head{ order:4; margin-top:14px; }
.sc-hero[data-header="bottom"] .sc-description{ order:5; }
/* The chips belong to the header, not to the block: leaving them at order 2
   would strand "Points card · Gold" above a balance whose name sits below it. */
.sc-hero[data-header="bottom"] .sc-chips{ order:5; }
.sc-hero[data-header="bottom"] .sc-grid{ margin-top:2px; }

/* Sat on the artwork, with its own legibility gradient — without one, an
   overlaid header is the single easiest way to make a card unreadable. */
.sc-hero[data-header="overlay"] .sc-head{
  order:4; position:relative; z-index:1;
  margin:14px -18px -18px; padding:34px 18px 18px;
  /* Avoid stacking a second photo band over the card-wide treatment. The
     continuous scrim keeps the artwork free of a hard horizontal artifact. */
  background:none;
}
.sc-hero[data-header="overlay"] .sc-description{ order:5; margin:-6px -18px -18px; padding:0 18px 16px; background:none; max-width:none; }
.sc-hero[data-header="overlay"] .sc-chips{ order:5; position:relative; z-index:1; }

/* -- logo -- */
.sc-hero[data-logo="none"] .sc-logo{ display:none; }
/* Lifted into the trailing top corner. Absolute rather than reordered markup;
   the header reserves room so the type pill cannot slide underneath it. */
.sc-hero[data-logo="corner"] .sc-logo{ position:absolute; inset-block:18px auto; inset-inline:auto 18px; }
.sc-hero[data-logo="corner"] .sc-head{ padding-inline-end:54px; }
/* Large, faint, behind everything but above the artwork. Deliberately bleeds off
   the trailing edge and is clipped by the hero's overflow.
   Logical inset, not physical: with `inset: auto -6% -14% auto` the watermark
   stayed pinned to the right in RTL instead of mirroring with the layout. */
.sc-hero[data-logo="watermark"] .sc-logo{
  position:absolute;
  inset-block:auto -14%; inset-inline:auto -6%;
  width:58%; height:auto; aspect-ratio:1;
  border:0; border-radius:0; background:none; box-shadow:none;
  opacity:.12; pointer-events:none;
}
.sc-hero[data-logo="watermark"] .sc-logo img{ object-fit:contain; }
.sc-hero[data-logo="watermark"] .sc-logo svg{ width:100%; height:100%; }

.sc-hero[data-logoshape="circle"] .sc-logo{ border-radius:50%; }
.sc-hero[data-logoshape="bare"] .sc-logo{ border:0; border-radius:0; background:none; box-shadow:none; }
.sc-hero[data-logoshape="bare"] .sc-logo img{ object-fit:contain; }

/* -- stamp block -- */
/* margin-top:auto pushes the grid to the foot of the flex column; CENTER lets
   the free space fall either side of it. Neither shrinks the grid, so the stamps
   stay the card's dominant element at every placement. */
.sc-hero[data-stamps="card-bottom"] .sc-grid,
.sc-hero[data-stamps="card-bottom"] .sc-balance{ margin-top:auto; }
.sc-hero[data-stamps="center"] .sc-grid,
.sc-hero[data-stamps="center"] .sc-balance{ margin-block:auto; }

/* -- ratio -- */
/* WALLET is clamped by the renderer above 12 stamps (see effectiveRatio), so
   this rule is never asked to squeeze slots below a tappable size. */
.sc-hero[data-ratio="wallet"]{ aspect-ratio:1.586; }
.sc-hero[data-ratio="wallet"] .sc-grid{ margin-block:12px; }
.sc-hero[data-ratio="wallet"] .sc-balance{ margin-block:10px; gap:7px; }

/* ============================== DARK THEME =================================
   The hero paints on the tenant colour in both themes; only the filled slot's
   ink needs re-stating, because --brand-ink is re-pointed by the dark palette. */
:root[data-theme="dark"] .sc-slot.full,
.cust-app[data-theme="dark"] .sc-slot.full,
[data-theme="dark"] .sc-slot.full{ color:#1C1A17; }

/* ============================ NARROW SCREENS =============================== */
@media (max-width:360px){
  .sc-hero{ padding:15px; }
  .sc-grid{ gap:8px; }
  .sc-identity h2{ font-size:19px; }
  .sc-balance-value b{ font-size:calc(48px * var(--point-scale,1)); }
}

/* ==========================================================================
   STAMP PRESENTATION (phase 3)

   The stamps are the point of a stamp card, so nothing here shrinks or demotes
   the grid — these options change how the slots READ. Attributes live on
   .sc-grid rather than the hero, because that is the element they describe.

   Layouts split two ways. GRID, ROW and LADDER are CSS flow. ARC, PATH and RING
   are absolutely positioned from per-slot --sc-x / --sc-y / --sc-rot computed by
   pure functions in card-art.js — JS rather than CSS trig so each arrangement
   can be asserted in Node (no overlap, inside the box, above the tap-target
   floor) instead of only being observable in a browser.
   ========================================================================== */

.sc-grid{ gap:var(--stamp-gap,10px); }
/* Identity at the default, so the historical card is untouched; the composition
   exists so scale, the sticker tilt and the absolute layouts' rotation can all
   apply at once without fighting over the transform property. */
.sc-slot{ transform:rotate(var(--sc-tilt,0deg)) scale(var(--stamp-scale,1)); }

/* ------------------------------- layouts -------------------------------- */
.sc-grid[data-layout="row"]{ grid-template-columns:repeat(var(--stamp-count,5),minmax(0,1fr)); }

/* A column of wide bars. Slots stop being square here, so aspect-ratio is
   released and a min-height keeps them tappable. */
.sc-grid[data-layout="ladder"]{ grid-template-columns:1fr; }
.sc-grid[data-layout="ladder"] .sc-slot{ aspect-ratio:auto; min-height:36px; border-radius:999px; }
.sc-grid[data-layout="ladder"] .sc-slot svg{ width:20px; height:20px; }
.sc-grid[data-layout="ladder"] .sc-slot img{ width:auto; height:70%; }

.sc-grid[data-layout="arc"],
.sc-grid[data-layout="path"],
.sc-grid[data-layout="ring"]{
  display:block;
  grid-template-columns:none;
  /* The aspect comes back with the geometry, so the block reserves exactly the
     height its arrangement needs and nothing overlaps what follows.

     Percentage padding, NOT aspect-ratio. The grid is a stretched item of the
     hero's flex column, and an aspect-ratio on such an item leaves the engine a
     width/height cycle to break: stretch the width and transfer it to the
     height, or take a height and transfer it back to the width. Blink takes the
     first road; in the webview deck, where the hero's height is definite
     (`.rc-deck-card .sc-hero{height:100%}`), Gecko measurably did not — a RING
     came out about 13% wider than the column containing it, overflowed to the
     trailing side, and carried the ring's centre ~14px off the card's with it.
     Percentage padding always resolves against a width, so there is no cycle
     left for an engine to break and every one of them reserves the same box.

     The slots' `top` percentages resolve against this padding box, which is why
     the height must live in the padding rather than in a zero content box plus
     a margin. And the side margins go, because percentage padding resolves
     against the CONTAINING BLOCK's width while the slots' `left` resolves
     against the grid's own: leave the 2px on and the two disagree, which is a
     ring 1.4% out of round. The flow layouts keep the margin; the absolute ones
     hold their slots well inside the box already. */
  height:0;
  margin-inline:0;
  padding-bottom:calc(100% / var(--stamp-aspect,2));
}
.sc-grid[data-layout="arc"] .sc-slot,
.sc-grid[data-layout="path"] .sc-slot,
.sc-grid[data-layout="ring"] .sc-slot{
  position:absolute;
  left:var(--sc-x,50%); top:var(--sc-y,50%);
  width:var(--stamp-size,18%); height:auto;
  transform:translate(-50%,-50%)
            rotate(calc(var(--sc-rot,0deg) + var(--sc-tilt,0deg)))
            scale(var(--stamp-scale,1));
}

/* -------------------------------- track --------------------------------- */
/* The connector that turns positions into a route. Drawn as a polyline through
   the computed slot centres, under the slots. */
.sc-track{
  position:absolute; inset:0; width:100%; height:100%;
  pointer-events:none; overflow:visible;
}
.sc-track polyline,
.sc-track polygon{
  fill:none;
  stroke:rgba(255,255,255,.40);
  stroke-width:2;
  stroke-linecap:round;
  stroke-linejoin:round;
  /* The viewBox is stretched (preserveAspectRatio:none), which would smear the
     stroke non-uniformly without this. */
  vector-effect:non-scaling-stroke;
}
.sc-grid[data-track="dotted"] .sc-track polyline,
.sc-grid[data-track="dotted"] .sc-track polygon{ stroke-dasharray:0.1 6; }
.sc-grid[data-track="dashed"] .sc-track polyline,
.sc-grid[data-track="dashed"] .sc-track polygon{ stroke-dasharray:7 6; }
/* Declared rather than left to the base rule: an unbroken stroke is a deliberate
   choice, and stating it means a dasharray leaking in from elsewhere cannot
   silently redefine what "solid" looks like. */
.sc-grid[data-track="solid"] .sc-track polyline,
.sc-grid[data-track="solid"] .sc-track polygon{ stroke-dasharray:none; }

/* ROW draws its own line rather than an SVG: a straight run needs no geometry. */
.sc-grid[data-layout="row"]:not([data-track="none"])::before{
  content:""; position:absolute; z-index:0;
  inset:50% 3% auto; height:2px; margin-top:-1px;
  background:rgba(255,255,255,.40);
}
.sc-grid[data-layout="row"][data-track="dotted"]::before{
  background:repeating-linear-gradient(90deg,rgba(255,255,255,.40) 0 2px,transparent 2px 8px);
}
.sc-grid[data-layout="row"][data-track="dashed"]::before{
  background:repeating-linear-gradient(90deg,rgba(255,255,255,.40) 0 8px,transparent 8px 14px);
}
.sc-grid[data-layout="row"] .sc-slot{ z-index:1; }

/* -------------------------------- shapes -------------------------------- */
.sc-grid[data-shape="squircle"] .sc-slot{ border-radius:30%; }
/* clip-path clips borders and shadows, so the clipped shapes carry their outline
   as an inset shadow instead and empties get a translucent fill to stay visible. */
.sc-grid[data-shape="hex"] .sc-slot,
.sc-grid[data-shape="ticket"] .sc-slot,
.sc-grid[data-shape="star"] .sc-slot,
.sc-grid[data-shape="shield"] .sc-slot{ border:0; border-radius:0; }
.sc-grid[data-shape="hex"] .sc-slot.empty,
.sc-grid[data-shape="ticket"] .sc-slot.empty,
.sc-grid[data-shape="star"] .sc-slot.empty,
.sc-grid[data-shape="shield"] .sc-slot.empty{}
.sc-grid[data-shape="hex"] .sc-slot:not(.has-icon){
  clip-path:polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%);
}
.sc-grid[data-shape="ticket"] .sc-slot:not(.has-icon){
  clip-path:polygon(0% 0%,100% 0%,100% 38%,88% 50%,100% 62%,100% 100%,0% 100%,0% 62%,12% 50%,0% 38%);
}
.sc-grid[data-shape="star"] .sc-slot:not(.has-icon){
  clip-path:polygon(50% 0%,63% 34%,99% 35%,71% 57%,80% 92%,50% 71%,20% 92%,29% 57%,1% 35%,37% 34%);
}
.sc-grid[data-shape="shield"] .sc-slot:not(.has-icon){
  clip-path:polygon(50% 0%,100% 16%,100% 58%,50% 100%,0% 58%,0% 16%);
}
/* A star's points leave little room for a glyph. */
.sc-grid[data-shape="star"] .sc-slot svg{ width:34%; height:34%; }
.sc-grid[data-shape="star"] .sc-slot .num{ font-size:9.5px; }

/* ---- fills on clipped shapes ----
   clip-path erases anything outside the shape, and an inset box-shadow lives on
   the inside edge of the BOX, not the shape — so OUTLINE's ring became four thin
   slivers on a hexagon and PUNCH lost its depth entirely. Backgrounds survive
   clipping, shadows do not, so the clipped shapes express both with gradients.
   Declared before the generic fill rules so the circle and squircle keep the
   sharper shadow-based treatment. */
.sc-grid[data-shape="hex"][data-fill="outline"] .sc-slot.full:not(.has-icon),
.sc-grid[data-shape="ticket"][data-fill="outline"] .sc-slot.full:not(.has-icon),
.sc-grid[data-shape="star"][data-fill="outline"] .sc-slot.full:not(.has-icon),
.sc-grid[data-shape="shield"][data-fill="outline"] .sc-slot.full:not(.has-icon){
  background:rgba(255,255,255,.30); box-shadow:none; color:#fff;
}
.sc-grid[data-shape="hex"][data-fill="punch"] .sc-slot.full:not(.has-icon),
.sc-grid[data-shape="ticket"][data-fill="punch"] .sc-slot.full:not(.has-icon),
.sc-grid[data-shape="star"][data-fill="punch"] .sc-slot.full:not(.has-icon),
.sc-grid[data-shape="shield"][data-fill="punch"] .sc-slot.full:not(.has-icon){
  /* Two layers: a lit rim along the top edge, then the hole itself. The rim is
     what reads as "punched" rather than "smudged" — on a circle that job is done
     by an inset shadow, which a clipped shape would erase. */
  background:
    linear-gradient(180deg,rgba(255,255,255,.30) 0 7%,transparent 7%),
    radial-gradient(120% 130% at 50% -10%,rgba(0,0,0,.62),rgba(0,0,0,.26));
  box-shadow:none;
}

/* --------------------------------- fills -------------------------------- */
.sc-grid[data-fill="glass"] .sc-slot.full:not(.has-icon){
  background:rgba(255,255,255,.26); color:#fff;
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.44), 0 2px 8px rgba(0,0,0,.18);
  backdrop-filter:blur(3px);
}
.sc-grid[data-fill="outline"] .sc-slot.full:not(.has-icon){
  background:none; color:#fff;
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.88);
}
.sc-grid[data-fill="sticker"] .sc-slot.full:not(.has-icon){
  box-shadow:0 4px 11px rgba(0,0,0,.34), inset 0 1px 0 rgba(255,255,255,.9);
}
/* Alternating tilt, so a run of stickers looks applied by hand rather than
   printed. nth-of-type, not nth-child: the track <svg> would shift the parity. */
.sc-grid[data-fill="sticker"] .sc-slot:nth-of-type(odd){ --sc-tilt:-4deg; }
.sc-grid[data-fill="sticker"] .sc-slot:nth-of-type(even){ --sc-tilt:4deg; }

/* A punch is material REMOVED, not ink added — so the collected slot is a hole,
   and a hole carries no tick. */
.sc-grid[data-fill="punch"] .sc-slot.full:not(.has-icon){
  background:rgba(0,0,0,.34); color:transparent;
  box-shadow:inset 0 2px 6px rgba(0,0,0,.55), inset 0 -1px 0 rgba(255,255,255,.16);
}
.sc-grid[data-fill="punch"] .sc-slot.full:not(.has-icon) svg{ display:none; }

/* ---------------------------- reward emphasis --------------------------- */
/* The historical treatment is the glow, which lives on .glow and is applied only
   when the reward is actually earned or ready. The others are decoration on the
   final slot and show regardless, so they read as "this one is the prize". */
.sc-grid[data-reward="none"] .sc-slot.reward.glow{ box-shadow:none; filter:none; animation:none; }

.sc-grid[data-reward="burst"] .sc-slot.reward::before,
.sc-grid[data-reward="crown"] .sc-slot.reward::before,
.sc-grid[data-reward="ribbon"] .sc-slot.reward::before{
  content:""; position:absolute; pointer-events:none;
}
.sc-grid[data-reward="burst"] .sc-slot.reward::before{
  inset:-26%; border-radius:50%; z-index:-1;
  background:repeating-conic-gradient(from 0deg,
    color-mix(in srgb,var(--_gold-hi) 62%,transparent) 0deg 7deg, transparent 7deg 20deg);
  opacity:.5;
}
/* z-index:-1 is safe here: .sc-slot is position:relative AND transformed, so it
   creates a stacking context and the ray layer cannot escape behind the card. */
.sc-grid[data-reward="crown"] .sc-slot.reward::before{
  inset:auto 50% 96% auto; width:44%; aspect-ratio:1.6;
  transform:translateX(50%);
  background:var(--_gold-hi);
  clip-path:polygon(0% 100%,0% 22%,25% 55%,50% 0%,75% 55%,100% 22%,100% 100%);
}
.sc-grid[data-reward="ribbon"] .sc-slot.reward::before{
  inset:auto -14% -6% -14%; height:26%;
  background:var(--_gold-hi);
  clip-path:polygon(0% 0%,100% 0%,88% 100%,12% 100%);
  border-radius:2px;
}

/* ==========================================================================
   STICKERS AND MOTION (phase 4)
   ========================================================================== */

/* ---- the layer scale ----
   Explicit, because this is where "the stamps are always on top" is ENFORCED.
   Sticker placement is genuinely free — drag anywhere, any rotation, any scale —
   but there is no anchor above the grid, so a tenant cannot bury their own
   stamps and nobody has to catch it in review.

   Stated as numbers rather than left to DOM order so the guarantee survives any
   future reordering of the markup. */
.sc-bg{ z-index:0; }
.sc-stickers[data-anchor="back"]{ z-index:1; }
.sc-pattern{ z-index:2; }
.sc-sheen{ z-index:3; }
/* Ambient motion rides directly above the static sheen — see .sc-motion below. */
.sc-motion{ z-index:4; }
.sc-head, .sc-hero .sc-description, .sc-chips{ z-index:5; }
.sc-stickers[data-anchor="front"]{ z-index:6; }
/* The same rung for both blocks, so the "no sticker anchor sits above the
   customer's progress" guarantee covers a point balance too. */
.sc-grid, .sc-balance{ z-index:7; }
.sc-hero-slot{ position:relative; z-index:8; }

.sc-stickers{
  position:absolute; inset:0;
  display:block;
  /* Never eats a tap meant for the card. */
  pointer-events:none;
}
.sc-sticker{
  position:absolute;
  left:var(--st-x,50%); top:var(--st-y,50%);
  width:22%; aspect-ratio:1;
  display:flex; align-items:center; justify-content:center;
  opacity:var(--st-o,1);
  transform:translate(-50%,-50%) rotate(var(--st-r,0deg)) scale(var(--st-s,1));
  transform-origin:50% 50%;
}
.sc-sticker img{ width:100%; height:100%; object-fit:contain; }
.sc-sticker-glyph{ font-size:min(9vw,44px); line-height:1; }
.sc-sticker[data-blend="screen"]{ mix-blend-mode:screen; }
.sc-sticker[data-blend="multiply"]{ mix-blend-mode:multiply; }
.sc-sticker[data-blend="overlay"]{ mix-blend-mode:overlay; }
.sc-sticker[data-blend="soft-light"]{ mix-blend-mode:soft-light; }

/* ============================== MOTION ====================================
   Every ambient option is a CSS animation on a composited property. There is no
   JS animation loop — the only script involved is TILT's throttled sensor read,
   which writes two custom properties and nothing else.

   --motion-intensity (0..1) scales the amplitude, so "subtle" and "obvious" are
   the same animation at different strengths rather than separate keyframes. */
@keyframes scSheenSweep{ 0%{ transform:translateX(-60%); } 100%{ transform:translateX(60%); } }
@keyframes scFloat{ 0%,100%{ transform:translateY(0); } 50%{ transform:translateY(calc(-4px * var(--motion-intensity,.6))); } }
@keyframes scShimmer{ 0%{ background-position:-150% 0; } 100%{ background-position:250% 0; } }
/* The blobs are large and soft, so a small position delta changes almost no
   pixels — the original 12%->30% drift measured under 7/255 of movement, i.e.
   nothing a customer could see. Wider travel over a larger painting area is
   what makes it read as drift. */
@keyframes scAurora{
  0%,100%{ background-position:4% 2%, 98% 22%, 62% 118%, 0 0; }
  50%{ background-position:52% 38%, 58% 0%, 20% 84%, 0 0; }
}
@keyframes scStampPress{
  0%{ transform:scale(1.32); }
  55%{ transform:scale(.92); }
  100%{ transform:scale(1); }
}
@keyframes scRipple{
  0%{ box-shadow:0 0 0 0 rgba(255,255,255,.55); }
  100%{ box-shadow:0 0 0 18px rgba(255,255,255,0); }
}
@keyframes scSparkle{
  0%{ opacity:0; transform:translate(-50%,-50%) scale(.3) rotate(0deg); }
  35%{ opacity:1; }
  100%{ opacity:0; transform:translate(-50%,-50%) scale(1.9) rotate(120deg); }
}

/* The travelling-highlight layer. It exists SEPARATELY from .sc-sheen because
   the two are different choices: the sheen is a static finish, and the tenant
   can set it to None or Holographic. While sheen-sweep and shimmer animated
   .sc-sheen, `Sheen: None` (display:none) made them paint nothing at all, and
   Holographic (overlay blend at .55 opacity) swamped them — so three of the six
   idle options silently did nothing, since AURORA falls back to SHIMMER off
   MESH. Painting on an always-present layer of its own makes idle movement
   independent of the finish. Inert unless an ambient option asks for it. */
.sc-motion{ position:absolute; inset:0; pointer-events:none; display:none; }

@media (prefers-reduced-motion:no-preference){
  .sc-hero[data-motion="sheen-sweep"] .sc-motion{
    display:block;
    /* A band rather than the sheen's corner glow: a sweep has to read as
       something crossing the card, and translating a corner radial barely
       changed a pixel. */
    background:linear-gradient(105deg,transparent 36%,
      rgba(255,255,255,calc(.30 * var(--motion-intensity,.6))) 50%,transparent 64%);
    animation:scSheenSweep 7s var(--_ease) infinite alternate;
  }
  .sc-hero[data-motion="float"]{
    animation:scFloat 5.5s ease-in-out infinite;
  }
  .sc-hero[data-motion="shimmer"] .sc-motion{
    display:block;
    background:linear-gradient(112deg,transparent 34%,
      rgba(255,255,255,calc(.34 * var(--motion-intensity,.6))) 47%,transparent 60%);
    background-size:260% 100%;
    animation:scShimmer 4.2s linear infinite;
  }
  /* AURORA drifts the mesh gradient's own blobs, which is why it needs MESH; the
     renderer substitutes SHIMMER when the gradient cannot supply them. */
  .sc-hero[data-motion="aurora"][data-grad="mesh"]{
    background-size:230% 230%, 230% 230%, 230% 230%, 100% 100%;
    animation:scAurora 14s ease-in-out infinite;
  }
  /* The drift is a keyframe on background-position, so the gyro cannot share
     that property — it gets the LIGHT instead. Two soft blooms on the motion
     layer slide against each other as the phone turns, over blobs that are
     already moving. Oversized inset so the travel never exposes an edge, and
     the travel is a transform, which is the one thing that can be interpolated
     between two throttled sensor reads. */
  .sc-hero[data-motion="aurora"] .sc-motion{
    display:block; inset:-24%;
    background:
      radial-gradient(40% 38% at 32% 30%,
        rgba(255,255,255,calc(.22 * var(--motion-intensity,.6))),transparent 68%),
      radial-gradient(46% 42% at 72% 68%,
        rgba(150,222,255,calc(.20 * var(--motion-intensity,.6))),transparent 70%);
    transform:translate(calc(var(--tilt-x,0) * 9% * var(--motion-intensity,.6)),
                        calc(var(--tilt-y,0) * 9% * var(--motion-intensity,.6)));
    transition:transform 140ms ease-out;
    will-change:transform;
  }
  /* TILT is driven by --tilt-x / --tilt-y, written by the webview's sensor read.
     With no sensor the variables stay 0 and the card simply does not move.

     AMPLITUDE. 8deg, not the 2.8deg this shipped with: at default intensity that
     was ±1.7° of rotation, and on a card with no background photo and no
     stickers a rotation is the WHOLE effect — so the option read as broken
     rather than as subtle. A wrist turn now moves the card about as far as the
     sensor-check page does, which is the amount a customer notices.

     The parallax on the photo and the stickers is what sells the depth, but only
     a card that HAS them gets it. Hence the glare below: the one part of TILT
     that every card can show. */
  .sc-hero[data-motion="tilt"]{
    /* One factor for the whole effect, inherited by the layers below, so
       "stronger" is one number per part rather than --motion-intensity repeated
       a dozen times. */
    --_ti:var(--motion-intensity,.6);
    transform:perspective(760px) rotateX(calc(var(--tilt-y,0) * -13deg * var(--_ti)))
      rotateY(calc(var(--tilt-x,0) * 13deg * var(--_ti)));
    /* The shade is the other half of a card turning in the light: the edge
       going away darkens. An inset shadow rather than another layer, so it
       lands above the background and under the text with nothing to stack. */
    box-shadow:0 12px 30px var(--_ring),
      inset calc(var(--tilt-x,0) * -34px * var(--_ti)) calc(var(--tilt-y,0) * -28px * var(--_ti))
        62px rgba(0,0,0,calc(.34 * var(--_ti)));
    transition:transform 130ms ease-out, box-shadow 130ms ease-out;
    will-change:transform;
  }
  /* THE LIGHT. A flat colour card that only rotates does not read as a physical
     card — the shade has to move across it and the hue has to change with it,
     which is what made this option look like nothing at all.

     Three blooms, and the warm and cool ones travel in OPPOSITE directions: turn
     the card one way and gold sweeps in, the other way and blue does, with a
     white specular head sliding against both. Positioned by background-position
     on an oversized image — the same technique (and the same transition) as the
     holographic sheen above, which is transitionable where a gradient's own
     `at x% y%` is not, so throttled sensor reads interpolate instead of stepping.

     `screen` so the light ADDS to the tenant's colour rather than greying it,
     and painted on .sc-motion — the layer that exists precisely so an ambient
     option never has to fight the tenant's chosen sheen.

     Faded in by --tilt-m, the distance from rest: light this strong parked in the
     middle of a card nobody is moving reads as bleached artwork, not as light. At
     rest the card is exactly what the tenant designed; the light belongs to the
     movement. */
  .sc-hero[data-motion="tilt"] .sc-motion{
    display:block; inset:0;
    mix-blend-mode:screen;
    opacity:var(--tilt-m,0);
    background:
      radial-gradient(46% 42% at 50% 50%,rgba(255,255,255,calc(.95 * var(--_ti))),transparent 66%),
      radial-gradient(54% 48% at 50% 50%,rgba(255,196,96,calc(.75 * var(--_ti))),transparent 70%),
      radial-gradient(58% 50% at 50% 50%,rgba(96,178,255,calc(.72 * var(--_ti))),transparent 72%);
    background-repeat:no-repeat;
    background-size:215% 215%, 245% 245%, 245% 245%;
    background-position:
      calc(50% - var(--tilt-x,0) * 48%) calc(50% - var(--tilt-y,0) * 44%),
      calc(50% + var(--tilt-x,0) * 44%) calc(50% + var(--tilt-y,0) * 40%),
      calc(50% - var(--tilt-x,0) * 44%) calc(50% - var(--tilt-y,0) * 40%);
    transition:background-position 130ms ease-out, opacity 130ms ease-out;
  }
  .sc-hero[data-motion="tilt"] .sc-bg{
    transform:translate(calc(var(--tilt-x,0) * 22px * var(--_ti)),
                        calc(var(--tilt-y,0) * 22px * var(--_ti)));
    transition:transform 130ms ease-out;
  }
  .sc-hero[data-motion="tilt"] .sc-stickers{
    transform:translate(calc(var(--tilt-x,0) * -26px * var(--_ti)),
                        calc(var(--tilt-y,0) * -26px * var(--_ti)));
    transition:transform 130ms ease-out;
  }

  /* ---- earn ----
     ONE definition per style, used at both moments a filled stamp appears:

       · the card's entrance, under [data-anim="1"] — every filled slot, walked
         by --sc-i;
       · a real earn, under [data-earn-play] — only the slots RCCardArt.playEarn
         marked .sc-earned, walked by the --sc-stagger it writes inline on them,
         so a stamp that was already on the card does not re-celebrate itself.

     Both come from the tenant's single `motionEarn` choice in the Card Studio.
     They were previously separate ideas, and the result was that three of the
     four options were dead: POP was hard-coded as the entrance regardless of the
     choice, and STAMP_PRESS / RIPPLE / SPARKLE_BURST rendered nothing anywhere,
     because nothing in the app ever set [data-earn-play]. */
  [data-anim="1"] .sc-hero[data-earn="pop"] .sc-slot.full,
  .sc-hero[data-earn="pop"][data-earn-play] .sc-slot.full.sc-earned{
    animation:scStampIn .5s var(--_ease) both;
    animation-delay:calc(var(--sc-stagger,var(--sc-i,0))*45ms);
  }
  [data-anim="1"] .sc-hero[data-earn="stamp-press"] .sc-slot.full,
  .sc-hero[data-earn="stamp-press"][data-earn-play] .sc-slot.full.sc-earned{
    animation:scStampPress .46s var(--_ease) both;
    animation-delay:calc(var(--sc-stagger,var(--sc-i,0))*45ms);
  }
  [data-anim="1"] .sc-hero[data-earn="ripple"] .sc-slot.full,
  .sc-hero[data-earn="ripple"][data-earn-play] .sc-slot.full.sc-earned{
    animation:scRipple .7s ease-out both;
    animation-delay:calc(var(--sc-stagger,var(--sc-i,0))*45ms);
  }
  /* Sparkle bursts OVER a stamp that also lands, or the flare fires above a slot
     that simply blinked into existence. */
  [data-anim="1"] .sc-hero[data-earn="sparkle-burst"] .sc-slot.full,
  .sc-hero[data-earn="sparkle-burst"][data-earn-play] .sc-slot.full.sc-earned{
    animation:scStampIn .5s var(--_ease) both;
    animation-delay:calc(var(--sc-stagger,var(--sc-i,0))*45ms);
  }
  [data-anim="1"] .sc-hero[data-earn="sparkle-burst"] .sc-slot.full::after,
  .sc-hero[data-earn="sparkle-burst"][data-earn-play] .sc-slot.full.sc-earned::after{
    content:""; position:absolute; inset:50% auto auto 50%;
    width:170%; aspect-ratio:1;
    background:conic-gradient(from 0deg,transparent 0 12deg,rgba(255,255,255,.9) 12deg 14deg,
      transparent 14deg 30deg);
    animation:scSparkle .72s ease-out both;
    animation-delay:calc(var(--sc-stagger,var(--sc-i,0))*45ms);
    pointer-events:none;
  }
  /* NONE must actually mean none, including the entrance. */
  .sc-hero[data-earn="none"] .sc-slot.full{ animation:none; }

  /* ---- earn, point card ----
     The same `motionEarn` choice, played on the thing that changed. A point card
     has no slots to stagger, so there is no --sc-stagger here and no entrance
     variant: a balance is not "collected", it is already there when the card
     paints. Only RCCardArt.playEarn's [data-earn-play] fires these.

     COUNT_UP is the one style whose motion is not CSS — card-art.js tweens the
     digits — so this rule only supplies the pulse around it. */
  .sc-hero[data-earn="pop"][data-earn-play] .sc-balance.sc-earned .sc-balance-value,
  .sc-hero[data-earn="count-up"][data-earn-play] .sc-balance.sc-earned .sc-balance-value{
    animation:scPointPop .52s var(--_ease) both;
  }
  .sc-hero[data-earn="stamp-press"][data-earn-play] .sc-balance.sc-earned .sc-balance-value{
    animation:scStampPress .46s var(--_ease) both;
  }
  .sc-hero[data-earn="ripple"][data-earn-play] .sc-balance.sc-earned .sc-balance-figure::after{
    content:""; position:absolute; inset:50% auto auto 0;
    width:120px; aspect-ratio:1; border-radius:50%;
    animation:scRipple .7s ease-out both;
    pointer-events:none;
  }
  .sc-hero[data-earn="sparkle-burst"][data-earn-play] .sc-balance.sc-earned .sc-balance-value{
    animation:scPointPop .52s var(--_ease) both;
  }
  .sc-hero[data-earn="sparkle-burst"][data-earn-play] .sc-balance.sc-earned .sc-balance-figure::after{
    content:""; position:absolute; inset:50% auto auto 12%;
    width:150px; aspect-ratio:1;
    transform:translate(-50%,-50%);
    background:conic-gradient(from 0deg,transparent 0 12deg,rgba(255,255,255,.9) 12deg 14deg,
      transparent 14deg 30deg);
    animation:scSparkle .72s ease-out both;
    pointer-events:none;
  }
  .sc-hero[data-earn="none"][data-earn-play] .sc-balance .sc-balance-value{ animation:none; }
}

/* Softer than scStampIn: a balance that flew in at 1.9x would read as the card
   re-rendering, not as points landing on a number that was already there. */
@keyframes scPointPop{
  0%{ transform:scale(1.16); opacity:.7; }
  60%{ transform:scale(.98); opacity:1; }
  100%{ transform:scale(1); opacity:1; }
}

/* Idle motion stops when nobody is looking at it: a phone must not burn battery
   animating four cards, only one of which is on screen. .is-idle is set by the
   deck on its non-active slots and on visibilitychange. */
.sc-hero.is-idle,
.sc-hero.is-idle .sc-sheen,
.sc-hero.is-idle .sc-motion,
.sc-hero.is-idle .sc-bg,
.sc-hero.is-idle .sc-stickers{ animation-play-state:paused; }

/* One media query, not a decision per option. */
@media (prefers-reduced-motion:reduce){
  .sc-hero, .sc-hero *{ animation:none!important; transition:none!important; }
  .sc-hero[data-motion="tilt"] .sc-bg,
  .sc-hero[data-motion="tilt"] .sc-stickers,
  .sc-hero[data-motion="tilt"] .sc-motion,
  .sc-hero[data-motion="aurora"] .sc-motion{ transform:none; }
  .sc-hero[data-motion="tilt"] .sc-motion{ background-position:50% 50%; }
  .sc-hero[data-motion="tilt"]{ box-shadow:0 12px 30px var(--_ring); }
  .sc-hero[data-sheen="holo"] .sc-sheen{ background-position:50% 50%; }
}

/* ==========================================================================
   POINT BALANCE

   The point card's counterpart of the stamp grid. Same contract: attributes
   live on the block (.sc-balance) rather than on the hero, because that is the
   element they describe, and no option branches the markup — the renderer emits
   every field and CSS decides what shows.

     data-style     NUMBER | RING | METER   how the figure reads
     data-progress  BAR | RING | SEGMENTS | NONE   the meter
     data-callout   NEXT | LADDER | NONE    the reward footer
     --point-scale  the figure's size multiplier (0.7-1.3)
     --point-pct    progress toward the next reward, 0-100

   RING and SEGMENTS are drawn from --point-pct alone. The renderer resolves
   the substitutions (a dial with nothing to fill, a meter under a figure that
   is already a dial) before it gets here, so these rules never have to ask
   whether the tenant has configured any rewards.
   ========================================================================== */

.sc-balance{ position:relative; display:flex; flex-direction:column; gap:9px; margin:20px 2px 4px; }

/* -- the figure -- */
.sc-balance-figure{ position:relative; display:flex; align-items:center; gap:14px; }
.sc-balance-value{ display:flex; align-items:baseline; gap:7px; min-width:0; }
.sc-balance-value b{
  font-family:var(--_disp);
  /* calc rather than a transform: scaling the glyph keeps the baseline and the
     unit label aligned, where scale() would drag the whole run off it. */
  font-size:calc(56px * var(--point-scale,1));
  font-weight:900; letter-spacing:-.04em; line-height:.95;
  color:var(--_title);
  font-variant-numeric:tabular-nums;
}
.sc-balance-unit{ font-size:calc(14px * var(--point-scale,1)); font-weight:900; letter-spacing:.08em; opacity:.62; text-transform:uppercase; }

/* RING: the figure sits inside the dial, so the number and its progress are one
   object rather than a number with a chart next to it. */
.sc-balance[data-style="ring"] .sc-balance-figure{ justify-content:flex-start; }
.sc-balance[data-style="ring"] .sc-balance-ring{ width:calc(104px * var(--point-scale,1)); flex:none; }
.sc-balance[data-style="ring"] .sc-balance-value{ flex-direction:column; align-items:flex-start; gap:2px; }
.sc-balance[data-style="ring"] .sc-balance-value b{ font-size:calc(40px * var(--point-scale,1)); }

/* METER pulls the meter up against the figure: the two read as one instrument. */
.sc-balance[data-style="meter"]{ gap:7px; }
.sc-balance[data-style="meter"] .sc-balance-track{ margin-top:-2px; }

/* -- the dial -- */
.sc-balance-ring{ display:block; aspect-ratio:1; transform:rotate(-90deg); overflow:visible; }
.sc-balance-ring circle{ fill:none; stroke-width:7; stroke-linecap:round; }
.sc-balance-ring .trk{ stroke:rgba(255,255,255,.2); }
.sc-balance-ring .val{ stroke:rgba(255,255,255,.95); transition:stroke-dasharray .7s var(--_ease); }

/* -- supporting lines -- */
.sc-balance-worth{ font-size:12.5px; font-weight:800; line-height:1.35; opacity:.78; }
.sc-balance-meta{ display:flex; align-items:center; justify-content:space-between; gap:10px; font-size:12px; font-weight:900; }
.sc-balance-meta span{ opacity:.82; white-space:nowrap; font-variant-numeric:tabular-nums; }

/* -- the meter -- */
.sc-balance-track{ position:relative; height:8px; border-radius:999px; overflow:hidden; background:rgba(255,255,255,.22); }
.sc-balance-track > i{ display:block; width:calc(var(--point-pct,0) * 1%); height:100%; border-radius:inherit; background:rgba(255,255,255,.95); transition:width .7s var(--_ease); }
.sc-balance[data-progress="none"] .sc-balance-track{ display:none; }

/* SEGMENTS: countable progress. Fixed pip count (see PROGRESS_SEGMENTS) — a pip
   per reward would read as a queue, and points do not queue. */
.sc-balance-track[data-segments]{ height:9px; display:flex; gap:4px; background:none; overflow:visible; }
.sc-balance-track[data-segments] > i{ width:auto; flex:1; height:100%; border-radius:3px; background:rgba(255,255,255,.2); transition:background .3s var(--_ease); }
.sc-balance-track[data-segments] > i.on{ background:rgba(255,255,255,.95); }

/* A standalone ring meter, for a figure that is not itself a dial: the dial sits
   opposite the figure rather than under the numbers. Left in flow it read as a
   circle dangling below the meta line — a chart that had lost its subject.
   Anchored to the block's top corner it becomes the other half of a
   composition: big number one side, progress the other. */
.sc-balance[data-progress="ring"]{ position:relative; }
.sc-balance[data-progress="ring"] .sc-balance-track{
  position:absolute; inset-block-start:0; inset-inline-end:0;
  height:auto; width:64px; background:none; overflow:visible;
}
/* Room for it, so a long "Worth approximately …" line cannot run underneath. */
.sc-balance[data-progress="ring"] .sc-balance-figure,
.sc-balance[data-progress="ring"] .sc-balance-worth,
.sc-balance[data-progress="ring"] .sc-balance-meta{ padding-inline-end:76px; }

/* -- the reward callout -- */
.sc-balance-reward{ display:flex; flex-direction:column; gap:8px; margin-top:5px; padding:10px 12px; border:1px solid rgba(255,255,255,.18); border-radius:16px; background:rgba(255,255,255,.15); backdrop-filter:blur(6px); }
.sc-balance[data-callout="none"] .sc-balance-reward{ display:none; }
.sc-balance-reward-row{ display:flex; align-items:center; gap:10px; min-width:0; }
.sc-balance-reward-ic{ width:30px; height:30px; flex:none; display:flex; align-items:center; justify-content:center; overflow:hidden; border-radius:10px; background:rgba(255,255,255,.18); }
.sc-balance-reward-ic img{ width:100%; height:100%; object-fit:cover; }
.sc-balance-reward-copy{ min-width:0; }
.sc-balance-reward-copy b{ display:block; overflow:hidden; font-size:11.5px; line-height:1.2; text-overflow:ellipsis; white-space:nowrap; }
.sc-balance-reward-copy em{ display:block; margin-top:2px; font-size:10.5px; font-weight:800; font-style:normal; opacity:.7; }
/* Affordable now, so it reads differently from the one still being saved for. */
.sc-balance-reward-row.is-ready .sc-balance-reward-ic{ background:rgba(255,255,255,.34); }
/* A program with no rewards is a program nobody can spend in. The empty callout
   says so on the card instead of leaving a blank where a reward should be. */
.sc-balance-reward[data-empty]{ border-style:dashed; background:rgba(255,255,255,.08); }
.sc-balance-reward[data-empty] .sc-balance-reward-copy b{ opacity:.9; }

/* -- chips -- */
.sc-chips{ position:relative; display:flex; flex-wrap:wrap; gap:6px; margin-top:13px; }
.sc-chips > span{ min-height:26px; padding:0 8px; display:inline-flex; align-items:center; gap:4px; border:1px solid rgba(255,255,255,.2); border-radius:999px; background:rgba(255,255,255,.13); font-size:8.5px; font-weight:850; letter-spacing:.04em; white-space:nowrap; }
.sc-chips svg{ width:12px; height:12px; flex:none; }

/* ============================ HERO VARIANT =================================
   The home screen's point card. The SAME card and the same design — tighter,
   without the description, and with two slots for the markup the hero owns and
   the design does not (see .sc-hero-slot in the order and z-index blocks).

   A variant rather than a second renderer, because a second renderer is exactly
   how the point card ended up drawn three ways. Only spacing and type scale
   change here; every surface, layout and motion rule above still applies. */
.sc-hero[data-variant="hero"]{ padding:15px 15px 14px; border-radius:22px; }
.sc-hero[data-variant="hero"] .sc-identity h2{ font-size:17px; }
.sc-hero[data-variant="hero"] .sc-logo{ width:34px; height:34px; border-radius:11px; }
.sc-hero[data-variant="hero"] .sc-balance{ margin:12px 0 0; gap:7px; }
.sc-hero[data-variant="hero"] .sc-balance-value b{ font-size:calc(34px * var(--point-scale,1)); }
.sc-hero[data-variant="hero"] .sc-balance-unit{ font-size:calc(12px * var(--point-scale,1)); }
.sc-hero[data-variant="hero"] .sc-balance[data-style="ring"] .sc-balance-ring{ width:calc(66px * var(--point-scale,1)); }
.sc-hero[data-variant="hero"] .sc-balance[data-style="ring"] .sc-balance-value b{ font-size:calc(27px * var(--point-scale,1)); }
.sc-hero[data-variant="hero"] .sc-balance-worth{ font-size:11.5px; }
.sc-hero[data-variant="hero"] .sc-balance-meta{ font-size:11px; }
.sc-hero[data-variant="hero"] .sc-balance-reward{ padding:8px 10px; border-radius:13px; }
.sc-hero[data-variant="hero"] .sc-chips{ margin-top:10px; }
.sc-hero[data-variant="hero"] .sc-hero-slot.top{ margin-bottom:13px; }
.sc-hero[data-variant="hero"] .sc-hero-slot.foot{ margin-top:13px; }
