/* ============================================================================
   THE CARD
   ----------------------------------------------------------------------------
   One object, used in three places: turning in 3D on the stage, sitting on a
   phone screen in the setup steps, and previewing live inside the studio.

   It is driven entirely by four custom properties, so the studio can restyle
   it by writing four strings and nothing else has to know:

       --c1 --c2 --c3   the three gradient stops
       --on             the colour everything printed on it takes

   A skin is therefore just a named set of those four. Adding one is four
   lines, and the studio's swatches are the same four lines read back out.

   What makes it read as a physical card rather than a rounded div:
     · a bloom placed OFF-CENTRE, so the light has a direction
     · a grain layer, drawn by the browser from an SVG turbulence filter —
       no file, no weight, and it stays sharp at any size
     · a bright hairline along the top edge and a dark one along the bottom,
       which is what an extruded plastic edge actually does to light
   ========================================================================== */

.lc{
  --c1:#2B1263; --c2:#6432C8; --c3:#1A0B40; --on:#fff; --stamp:#2B1263;
  position:relative;width:100%;aspect-ratio:1.586/1;border-radius:5.4% / 8.6%;
  background:linear-gradient(145deg,var(--c1) 0%,var(--c2) 52%,var(--c3) 100%);
  color:var(--on);overflow:hidden;isolation:isolate;
  display:flex;flex-direction:column;justify-content:space-between;
  padding:6.2% 6.4%;
  font-family:var(--font,'Figtree',sans-serif);
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.34),
    inset 0 -1px 0 rgba(0,0,0,.30),
    inset 0 0 0 1px rgba(255,255,255,.09);
}

/* The bloom. Off-centre, oversized, and soft enough that you read it as a
   light source rather than a shape. */
.lc:before{content:'';position:absolute;z-index:0;
  top:-42%;left:-16%;width:92%;height:168%;border-radius:50%;
  background:radial-gradient(closest-side,rgba(255,255,255,.40),rgba(255,255,255,0) 72%);
  pointer-events:none}

/* The grain. feTurbulence at a high frequency, screened back almost to
   nothing — enough to kill the plastic flatness of a CSS gradient. */
.lc:after{content:'';position:absolute;inset:0;z-index:1;pointer-events:none;
  opacity:.26;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='.52'/%3E%3C/svg%3E");
  background-size:160px 160px}

.lc > *{position:relative;z-index:2}

/* ── What is printed on it ───────────────────────────────────────────── */
.lc-top{display:flex;align-items:center;gap:3.4%;min-height:0}
.lc-mark{width:10.4%;aspect-ratio:1;border-radius:28%;flex-shrink:0;
  background:color-mix(in srgb,var(--on) 94%,transparent);
  color:var(--c2);display:flex;align-items:center;justify-content:center;
  font-size:4.2cqw;font-weight:700;letter-spacing:-.04em;overflow:hidden}
.lc-mark img{width:68%;height:68%;object-fit:contain}
.lc-name{font-size:5.2cqw;font-weight:600;letter-spacing:-.03em;line-height:1.1;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lc-kicker{margin-left:auto;font-size:2.5cqw;font-weight:600;letter-spacing:.2em;
  text-transform:uppercase;opacity:.62;white-space:nowrap}

.lc-reward{font-size:3.2cqw;line-height:1.3;opacity:.8;max-width:78%;font-weight:450}
.lc-reward b{font-weight:600;opacity:1}

/* ── The stamp row ───────────────────────────────────────────────────── */
/* Ten stamps, five to a row — the shape almost every Malaysian shop already
   prints. Five across instead of ten makes each circle nearly twice the
   width it would otherwise be, which is the only way the stamp icon is
   legible at card size. */
/* Ten stamps, five to a row, spanning the card's full measure and starting
   at the same left edge as the name and the reward line above them.

   Five circles across a card this shape cannot be both modest and edge to
   edge: 1fr columns fill the measure exactly, but they also force the stamp
   to a fifth of the card. So the tracks are sized and the row starts at the
   left, stopping a little short — on the same logic as the reward line
   above it, which also runs to a measure rather than to the edge.

   The gap is set against the circle, not left over from it. 3.5 against 14
   is a quarter, which reads as a row of stamps rather than ten unrelated
   rings; the version before this had it at nearly two thirds, because the
   gap was whatever remained once the circle had been shrunk.

   14 and 3.5 together come to a hair under the full measure — about 15px
   short on the studio card — so the row reads as filling the width while
   still stopping, like the reward line, before the edge. */
.lc-slots{display:grid;grid-template-columns:repeat(5,16cqw);gap:4cqw;
  justify-content:start}
.lc-slot{aspect-ratio:1;border-radius:50%;
  border:1.5px solid color-mix(in srgb,var(--on) 36%,transparent);
  display:flex;align-items:center;justify-content:center;
  color:color-mix(in srgb,var(--on) 28%,transparent);font-size:7cqw;
  transition:background-color .35s,color .35s,border-color .35s,box-shadow .35s}

/* The filled state is one rule for all twenty skins, and it is why --stamp
   exists. The circle fills with the card's own ink (white on a dark skin,
   dark on a pale one) and the icon takes --stamp, which every skin declares
   as the colour that reads ON that fill. Deriving the icon colour from a
   gradient stop instead — which is what this did — produced pale-on-pale
   icons the moment a skin's mid stop was light. */
.lc-slot.on{background:var(--on);border-color:transparent;color:var(--stamp)}
.lc-slot i{line-height:0}
/* The one that is about to be filled — a quiet invitation, not a blinker. */
.lc-slot.next{border-color:color-mix(in srgb,var(--on) 78%,transparent);
  box-shadow:0 0 0 2.5px color-mix(in srgb,var(--on) 13%,transparent)}

@supports not (color:color-mix(in srgb,red,blue)){
  .lc-mark{background:rgba(255,255,255,.94)}
  .lc-slot{border-color:rgba(255,255,255,.38);color:rgba(255,255,255,.3)}
}

/* Container queries drive every size above, so one card scales from a swatch
   to a hero without a single media query. */
.lc{container-type:inline-size}

/* ── Skins ───────────────────────────────────────────────────────────────
   Three stops, an ink, and a stamp colour. Twenty of them, laid out five to
   a row in the studio so the grid closes cleanly instead of leaving one
   swatch stranded on a line of its own.

   --on    what everything printed on the card takes, AND what a filled
           stamp circle fills with
   --stamp what the stamp icon takes once it is sitting on that fill, so it
           is the inverse of --on and is chosen, never derived

   Fourteen dark, six pale. The pale ones invert on both counts: their ink
   goes dark, which means their filled circles go dark too, which means the
   stamp inside has to go white. */

/* Dark */
.lc[data-skin="violet"]  {--c1:#3C1A8C;--c2:#6432C8;--c3:#200C52;--on:#fff;--stamp:#2B1263}
.lc[data-skin="indigo"]  {--c1:#3B3A9E;--c2:#26247A;--c3:#121042;--on:#fff;--stamp:#1B1A58}
.lc[data-skin="midnight"]{--c1:#2B3C74;--c2:#16224A;--c3:#080E24;--on:#fff;--stamp:#16224A}
.lc[data-skin="ocean"]   {--c1:#1E6E8C;--c2:#0F4A61;--c3:#06242F;--on:#fff;--stamp:#0F4A61}
.lc[data-skin="teal"]    {--c1:#1C7A6E;--c2:#0E5248;--c3:#052723;--on:#fff;--stamp:#0E5248}
.lc[data-skin="forest"]  {--c1:#2F6B52;--c2:#1C4634;--c3:#0E2619;--on:#fff;--stamp:#1C4634}
.lc[data-skin="moss"]    {--c1:#5E7A3A;--c2:#3F5522;--c3:#1E290F;--on:#fff;--stamp:#3F5522}
.lc[data-skin="ember"]   {--c1:#FF8A4C;--c2:#FF6432;--c3:#B9330E;--on:#fff;--stamp:#B9330E}
.lc[data-skin="clay"]    {--c1:#C98A5A;--c2:#A06E28;--c3:#5E3C12;--on:#fff;--stamp:#5E3C12}
.lc[data-skin="cocoa"]   {--c1:#7A5240;--c2:#523324;--c3:#26150E;--on:#fff;--stamp:#523324}
.lc[data-skin="wine"]    {--c1:#8E2F4A;--c2:#5E1A2E;--c3:#2C0A15;--on:#fff;--stamp:#5E1A2E}
.lc[data-skin="rose"]    {--c1:#D97A92;--c2:#B0455F;--c3:#5E1C2C;--on:#fff;--stamp:#8E2F45}
.lc[data-skin="ink"]     {--c1:#2A2140;--c2:#171026;--c3:#0C0718;--on:#fff;--stamp:#171026}
.lc[data-skin="charcoal"]{--c1:#4A4A52;--c2:#2C2C33;--c3:#14141A;--on:#fff;--stamp:#2C2C33}

/* Pale */
.lc[data-skin="peach"]   {--c1:#FFE2C2;--c2:#F7C891;--c3:#E0A258;--on:#4A2B08;--stamp:#FFF6EA}
.lc[data-skin="lilac"]   {--c1:#F3EAFF;--c2:#DEC7FF;--c3:#B695E8;--on:#2B1263;--stamp:#F6F0FF}
.lc[data-skin="pearl"]   {--c1:#FFFFFF;--c2:#F1EDE4;--c3:#D8D0C0;--on:#241A3A;--stamp:#FFFFFF}
.lc[data-skin="sand"]    {--c1:#F4EFE6;--c2:#E2D6C0;--c3:#BFAE90;--on:#3A2A12;--stamp:#FBF7F0}
.lc[data-skin="sky"]     {--c1:#E4F1FB;--c2:#C3DFF5;--c3:#93BFE0;--on:#10324A;--stamp:#F2F9FF}
.lc[data-skin="mint"]    {--c1:#E4F6EC;--c2:#C2E8D2;--c3:#94CFAF;--on:#0E3A24;--stamp:#F2FBF6}

/* The pale skins need their light handled the other way round: the bloom
   becomes a soft shadow, and the extruded edge reverses. */
.lc[data-skin="peach"]:before,.lc[data-skin="lilac"]:before,
.lc[data-skin="pearl"]:before,.lc[data-skin="sand"]:before,
.lc[data-skin="sky"]:before,.lc[data-skin="mint"]:before{
  background:radial-gradient(closest-side,rgba(0,0,0,.09),rgba(0,0,0,0) 72%)}
.lc[data-skin="peach"],.lc[data-skin="lilac"],
.lc[data-skin="pearl"],.lc[data-skin="sand"],
.lc[data-skin="sky"],.lc[data-skin="mint"]{
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9),inset 0 -1px 0 rgba(0,0,0,.12),
             inset 0 0 0 1px rgba(0,0,0,.05)}
.lc[data-skin="peach"] .lc-mark,.lc[data-skin="lilac"] .lc-mark,
.lc[data-skin="pearl"] .lc-mark,.lc[data-skin="sand"] .lc-mark,
.lc[data-skin="sky"] .lc-mark,.lc[data-skin="mint"] .lc-mark{
  background:var(--on);color:var(--c1)}

/* ============================================================================
   THE 3D RIG
   ----------------------------------------------------------------------------
   A card that turns has to do three things at once or it reads as a div with
   a transform on it:

     1. Both faces have to exist. A back that is just the front mirrored is
        the single clearest tell, so the back carries the QR and the terms —
        which is what the back of a real loyalty card carries.
     2. The highlight has to move. A sheen that stays put while the geometry
        rotates is the thing that gives it away. The sweep angle is derived
        from the same rotation value as the transform, in JS, so they cannot
        drift apart.
     3. The shadow has to foreshorten. A card edge-on casts almost nothing.
   ========================================================================== */
.stage3d{perspective:1400px;perspective-origin:50% 44%}
.card3d{position:relative;width:100%;aspect-ratio:1.586/1;
  transform-style:preserve-3d;will-change:transform}
/* The cast shadow goes on the FACES, not on the rig. A filter or a shadow on
   the rig itself would give it its own containing block and flatten the 3D
   transform of everything inside it. */
.card3d .face{position:absolute;inset:0;backface-visibility:hidden;
  -webkit-backface-visibility:hidden;border-radius:5.4% / 8.6%;
  box-shadow:0 44px 72px -30px rgba(0,0,0,.65)}
.card3d .face.back{transform:rotateY(180deg)}

/* The sheen rides above both faces and is clipped to the card's own radius. */
.card3d .sheen{position:absolute;inset:0;z-index:5;border-radius:5.4% / 8.6%;
  pointer-events:none;overflow:hidden;mix-blend-mode:screen;opacity:.0}
.card3d .sheen i{position:absolute;top:-60%;bottom:-60%;left:-30%;width:34%;
  background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,.75),rgba(255,255,255,0));
  filter:blur(6px)}

/* The contact shadow, scaled and faded by the same rotation. */
.card3d-shadow{position:absolute;left:8%;right:8%;bottom:-9%;height:16%;
  background:radial-gradient(closest-side,rgba(25,14,46,.38),rgba(25,14,46,0) 76%);
  filter:blur(10px);will-change:transform,opacity;z-index:-1}

/* ── The back ────────────────────────────────────────────────────────────
   What the back of a loyalty card is actually for: who it belongs to, which
   shop and which branch honours it, and the code the counter scans. It is
   plainer than the front on purpose — the front sells, the back is read at
   the till.

   No magstripe. A stripe says "swipe me", and nothing about this card is
   swiped; it was decoration pretending to be an affordance. */
.lc-back{display:grid;grid-template-columns:1fr auto;gap:5%;
  align-items:start;height:100%}
.lc-b-who{display:flex;flex-direction:column;gap:3.4cqw;min-width:0}
.lc-b-field{min-width:0}
.lc-b-field b{display:block;font-size:2.6cqw;font-weight:600;letter-spacing:.17em;
  text-transform:uppercase;opacity:.56;margin-bottom:.9cqw}
.lc-b-field span{display:block;font-size:4cqw;font-weight:500;letter-spacing:-.02em;
  line-height:1.15;overflow-wrap:anywhere}
/* The shop's name is the one thing on this side that has to be readable
   across a counter, so it is the only thing set larger than its label. */
.lc-b-field.lead span{font-size:4.9cqw;font-weight:600;letter-spacing:-.028em}
.lc-b-field.sm span{font-size:3.4cqw;font-weight:450;opacity:.84}
/* THE QUIET ZONE IS NOT PADDING, IT IS PART OF THE CODE.
   A QR needs four clear modules of white around it or a scanner has nothing to
   lock onto. qrcodejs draws none, so this plate's padding IS that margin, and
   it has to be ≥ 4/37 of the code's own width — about 8.9% of the plate once
   you solve for it. It was 1.4cqw against a 23cqw code: 2.2 modules, barely
   half of what the standard asks for.

   The card is also the wrong place to scan from. A customer's phone showing a
   37-module code at 88px is a third of a millimetre per module, which a staff
   phone held over it will often fail to read — so tapping it opens the
   full-screen version, which is the one actually meant to be scanned. */
.lc-qr{width:32cqw;aspect-ratio:1;border-radius:2.4cqw;background:#fff;padding:3cqw;
  display:flex;align-items:center;justify-content:center;flex-shrink:0;
  cursor:pointer;-webkit-tap-highlight-color:transparent}
.lc-qr svg,.lc-qr canvas,.lc-qr img{
  width:100%!important;height:100%!important;display:block;border-radius:1px}
/* qrcodejs draws the code twice — a canvas and an <img> of the same thing, so
   that one works wherever the other does not. Both are left in place and one
   is simply hidden, because which of the two is the live one differs by
   browser and picking the wrong one leaves an empty white square. */
.lc-qr canvas + img{display:none}
.lc-fine{grid-column:1/-1;align-self:end;font-size:2.4cqw;line-height:1.45;
  opacity:.52;font-weight:450;margin:0}

/* On a pale skin the QR tile would be white on near-white, so it gets the
   card's own ink as a hairline to sit in. */
.lc[data-skin="peach"] .lc-qr,.lc[data-skin="lilac"] .lc-qr,
.lc[data-skin="pearl"] .lc-qr,.lc[data-skin="sand"] .lc-qr,
.lc[data-skin="sky"] .lc-qr,.lc[data-skin="mint"] .lc-qr{
  box-shadow:0 0 0 1px color-mix(in srgb,var(--on) 22%,transparent)}

/* The next empty circle is a target when the shop is looking at the card.
   Never on the customer's own copy — see the onSlot option in app.js. */
.lc-slot.tappable{cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:transform 110ms ease-out,background-color 110ms ease-out}
.lc-slot.tappable:hover{background:color-mix(in srgb,var(--on) 16%,transparent)}
.lc-slot.tappable:active{transform:scale(.92)}
.lc-slot.tappable:focus-visible{outline:2px solid var(--on);outline-offset:3px}
