/* Gbam — the motion system.
 *
 * The brief is explicit: "A grey tile fills gold, the tick strikes through it in a
 * single frame, and the count increments. No confetti, no coin animation."
 *
 * So every movement here does a job. It shows money arriving, it shows a figure
 * changing, it shows where a screen came from. Nothing moves only to be pretty.
 * The one indulgence is the strike, because that moment is the product.
 *
 * Everything obeys prefers-reduced-motion, and every animation is compositor only
 * (transform and opacity) so a cheap Android phone holds sixty frames.
 */

:root {
  /* Durations. Short enough to feel instant, long enough to be read. */
  --t-instant: 90ms;
  --t-quick: 160ms;
  --t-base: 260ms;
  --t-slow: 420ms;
  --t-story: 720ms;
  --t-strike: 560ms;

  /* Easings. Entrance decelerates, exit accelerates, emphasis overshoots once. */
  --e-out: cubic-bezier(.16, 1, .3, 1);
  --e-in: cubic-bezier(.7, 0, .84, 0);
  --e-inout: cubic-bezier(.65, 0, .35, 1);
  --e-spring: cubic-bezier(.34, 1.56, .64, 1);
  --e-settle: cubic-bezier(.22, 1.4, .36, 1);

  --stagger: 34ms;
}

/* ======================================================================
 * The reduced-motion contract. One block, no exceptions elsewhere.
 * Movement is removed; the state change still happens, instantly.
 * ==================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  .rise, .rise-in { opacity: 1 !important; transform: none !important; }
}

/* ======================================================================
 * ENTRANCE — the staged reveal.
 * A screen arrives in reading order, a beat apart, so the eye is led down it
 * instead of being handed everything at once.
 * ==================================================================== */
@media (prefers-reduced-motion: no-preference) {
  .rise { opacity: 0; transform: translate3d(0, 14px, 0); }
  .rise-in {
    opacity: 1;
    transform: none;
    transition: opacity var(--t-slow) var(--e-out), transform var(--t-slow) var(--e-out);
  }

  /* The route itself. Direction aware: forward arrives from the right, back from
     the left, so the app keeps a sense of place. */
  @keyframes routeFwd {
    from { opacity: 0; transform: translate3d(18px, 0, 0); }
    to { opacity: 1; transform: none; }
  }
  @keyframes routeBack {
    from { opacity: 0; transform: translate3d(-18px, 0, 0); }
    to { opacity: 1; transform: none; }
  }
  [data-nav="fwd"] #app > * { animation: routeFwd var(--t-base) var(--e-out) both; }
  [data-nav="back"] #app > * { animation: routeBack var(--t-base) var(--e-out) both; }
}

/* ======================================================================
 * THE STRIKE — the signature moment.
 *
 * A tile stops being unpaid. It swells, the gold lands, the tick is drawn through
 * it in one stroke, and it settles. This is the only thing in the product allowed
 * to be beautiful for its own sake, because it is what the product is named after.
 * ==================================================================== */
@keyframes strikeFill {
  0% { transform: scale(1); }
  30% { transform: scale(1.34); }
  62% { transform: scale(.96); }
  100% { transform: scale(1); }
}
@keyframes strikeGlow {
  0% { box-shadow: 0 0 0 0 rgba(212, 168, 67, .55); }
  55% { box-shadow: 0 0 0 7px rgba(212, 168, 67, 0); }
  100% { box-shadow: 0 0 0 0 rgba(212, 168, 67, 0); }
}
@keyframes tickDraw {
  from { stroke-dashoffset: var(--tick-len, 44); }
  to { stroke-dashoffset: 0; }
}

@media (prefers-reduced-motion: no-preference) {
  .grid-tiles i.just-paid {
    animation: strikeFill var(--t-strike) var(--e-settle), strikeGlow var(--t-strike) var(--e-out);
    z-index: 1;
  }
  .strike-tick path {
    stroke-dasharray: var(--tick-len, 44);
    animation: tickDraw 300ms var(--e-out) 120ms both;
  }

  /* The wall of tiles fills in a wave on first paint, so it reads as a roll call
     being called rather than as a texture that was always there. */
  .grid-tiles.cascade i {
    animation: tileIn 300ms var(--e-settle) both;
    animation-delay: calc(var(--i, 0) * 9ms);
  }
  @keyframes tileIn {
    from { opacity: 0; transform: scale(.55); }
    to { opacity: 1; transform: none; }
  }
}

/* ======================================================================
 * THE RING — draws itself to the figure it is reporting.
 * ==================================================================== */
@media (prefers-reduced-motion: no-preference) {
  .ring__fill.draw { animation: ringDraw var(--t-story) var(--e-out) both; }
  @keyframes ringDraw {
    from { stroke-dashoffset: var(--ring-c, 400); }
    to { stroke-dashoffset: var(--ring-off, 0); }
  }
  /* A faint sweep travels the ring, the way a counter's finger travels a page. */
  .ring-wrap.live::after {
    content: "";
    position: absolute; inset: -2px; border-radius: 50%;
    background: conic-gradient(from 0deg, transparent 0 84%, rgba(212, 168, 67, .22) 93%, transparent 100%);
    animation: ringSweep 4.2s linear infinite;
    pointer-events: none;
  }
  @keyframes ringSweep { to { transform: rotate(360deg); } }
}

/* ======================================================================
 * FIGURES — a number that changes should be seen to change.
 * ==================================================================== */
@keyframes figureBump {
  0% { transform: translateY(0); }
  35% { transform: translateY(-3px); }
  100% { transform: translateY(0); }
}
@media (prefers-reduced-motion: no-preference) {
  .bumped { animation: figureBump 380ms var(--e-settle); }
}

/* ======================================================================
 * THE BAR — fills from where it was, never from zero.
 * ==================================================================== */
.bar > i { transition: width var(--t-story) var(--e-out); }
@media (prefers-reduced-motion: no-preference) {
  .bar.shine > i { position: relative; overflow: hidden; }
  .bar.shine > i::after {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .34), transparent);
    transform: translateX(-100%);
    animation: barShine 2.6s var(--e-inout) infinite;
  }
  @keyframes barShine { to { transform: translateX(260%); } }
}

/* ======================================================================
 * SURFACES — press, lift, and the sheet that springs.
 * ==================================================================== */
.btn, .pill, .item, .card, .panel, .mcard, .queue-card {
  transition: transform var(--t-quick) var(--e-out),
              box-shadow var(--t-quick) var(--e-out),
              border-color var(--t-quick) var(--e-out),
              background-color var(--t-quick) var(--e-out),
              filter var(--t-quick) var(--e-out);
}
.btn:active, .pill:active { transform: scale(.975); }
@media (hover: hover) {
  .mcard:hover, .queue-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-2); }
  .card.liftable:hover { transform: translateY(-3px) rotate(-.15deg); }
  .item:hover { background: var(--surface-2); }
}

@media (prefers-reduced-motion: no-preference) {
  .scrim { animation: scrimIn var(--t-base) var(--e-out) both; }
  .sheet { animation: sheetUp var(--t-slow) var(--e-settle) both; }
  @keyframes scrimIn { from { opacity: 0; } to { opacity: 1; } }
  @keyframes sheetUp {
    from { opacity: 0; transform: translate3d(0, 26px, 0) scale(.985); }
    to { opacity: 1; transform: none; }
  }
  .toast { animation: toastIn var(--t-base) var(--e-settle) both; }
  @keyframes toastIn {
    from { opacity: 0; transform: translate3d(0, 12px, 0) scale(.97); }
    to { opacity: 1; transform: none; }
  }
}

/* ======================================================================
 * THE RECEIPT — tears off the roll, then the stamp lands.
 * ==================================================================== */
@media (prefers-reduced-motion: no-preference) {
  .receipt-wrap .card {
    animation: receiptOut var(--t-story) var(--e-settle) both;
    transform-origin: 50% 0;
  }
  @keyframes receiptOut {
    0% { opacity: 0; transform: translate3d(0, -22px, 0) rotate(-1.2deg) scaleY(.94); }
    60% { opacity: 1; }
    100% { opacity: 1; transform: none; }
  }
  .stamp { animation: stampDown 420ms var(--e-settle) 400ms both; }
  @keyframes stampDown {
    0% { opacity: 0; transform: rotate(-11deg) scale(1.9); }
    70% { opacity: .9; transform: rotate(-11deg) scale(.94); }
    100% { opacity: .88; transform: rotate(-11deg) scale(1); }
  }
}

/* ======================================================================
 * NAV — the icon settles into place rather than snapping.
 * ==================================================================== */
.tabbar button, .sidenav button {
  transition: color var(--t-quick) var(--e-out), background-color var(--t-quick) var(--e-out);
}
.tabbar button svg { transition: transform var(--t-base) var(--e-settle); }
.tabbar button[aria-current="page"] svg { transform: translateY(-2px) scale(1.08); }

/* ======================================================================
 * THE LANDING — the headline arrives a word at a time, the page breathes.
 * ==================================================================== */
@media (prefers-reduced-motion: no-preference) {
  .hero__mark { animation: markIn var(--t-story) var(--e-settle) both; }
  @keyframes markIn {
    from { opacity: 0; transform: translate3d(0, 10px, 0) rotate(-8deg) scale(.86); }
    to { opacity: 1; transform: none; }
  }
  .hero h1 .word {
    display: inline-block;
    animation: wordUp 620ms var(--e-out) both;
    animation-delay: calc(var(--w, 0) * 58ms);
  }
  @keyframes wordUp {
    from { opacity: 0; transform: translate3d(0, 22px, 0); }
    to { opacity: 1; transform: none; }
  }
  .fade-swap { animation: fadeSwap var(--t-base) var(--e-out) both; }
  @keyframes fadeSwap { from { opacity: 0; } to { opacity: 1; } }
}

/* Parallax layers on the landing. The engine sets --py from the scroll position. */
.depth { will-change: transform; transform: translate3d(0, calc(var(--py, 0) * 1px), 0); }

/* ======================================================================
 * THE ILLUSTRATIONS.
 * Art is drawn in SVG, so it animates with the same clock as everything else.
 * ==================================================================== */
.art { display: block; width: 100%; height: auto; }
.art--seg { width: 68px; height: 68px; }
.art--empty { width: 168px; height: 144px; margin: 0 auto var(--sp-3); }
.art--hero { max-width: 420px; margin-inline: auto; }

@media (prefers-reduced-motion: no-preference) {
  /* The hero wall fills one tile at a time, then the twenty-sixth strikes. */
  .hero-tile.on {
    animation: heroTileOn 420ms var(--e-settle) both;
    animation-delay: var(--d, 0ms);
  }
  @keyframes heroTileOn {
    0% { opacity: .25; transform: scale(.6); }
    55% { opacity: 1; transform: scale(1.22); }
    100% { opacity: 1; transform: none; }
  }
  .hero-tile { transform-box: fill-box; transform-origin: 50% 50%; }

  .hero-strike {
    transform-box: fill-box; transform-origin: 50% 50%;
    animation: heroStrike 3.6s var(--e-settle) 1.5s infinite;
  }
  @keyframes heroStrike {
    0% { opacity: 0; transform: scale(.5); }
    8% { opacity: 1; transform: scale(1.32); }
    16% { opacity: 1; transform: scale(1); }
    76% { opacity: 1; transform: scale(1); }
    88% { opacity: 0; transform: scale(1); }
    100% { opacity: 0; }
  }

  .hero-card { animation: heroCardIn 900ms var(--e-settle) 180ms both; transform-box: fill-box; transform-origin: 50% 30%; }
  @keyframes heroCardIn {
    from { opacity: 0; transform: translateY(26px) scale(.965) rotate(-.6deg); }
    to { opacity: 1; transform: none; }
  }

  /* Gold leaf catches the light once, slowly, then rests. */
  .art-mark__sheen {
    transform-box: fill-box; transform-origin: 50% 50%;
    animation: sheen 5.5s var(--e-inout) 1s infinite;
  }
  @keyframes sheen {
    0%, 70%, 100% { opacity: 0; transform: translateX(-120%); }
    78% { opacity: .9; }
    86% { opacity: 0; transform: translateX(120%); }
  }
}

/* The engraved band that heads a receipt. */
.art-band { opacity: .85; }

/* ======================================================================
 * THE PHOTOGRAPHS.
 *
 * A photograph never arrives as a white hole and never moves the layout. The box
 * is reserved at the true aspect ratio, the blurred placeholder is the background
 * from the first frame, and the real file crossfades over it when it decodes.
 * ==================================================================== */
.ph {
  position: relative;
  display: block;
  width: 100%;
  height: 0;                       /* height comes from padding-bottom, set inline */
  overflow: hidden;
  border-radius: var(--r-lg);
  background-size: cover;
  background-position: center;
  background-color: var(--surface-2);
  isolation: isolate;
}
.ph picture, .ph img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.ph img {
  object-fit: cover;
  opacity: 0;
  transform: scale(1.04);
}
.ph--in img { opacity: 1; transform: none; }
.ph--fail img { display: none; }

/* A photograph that has no master on disk keeps its box and stays quiet. */
.ph--none { background: var(--surface-2); }

@media (prefers-reduced-motion: no-preference) {
  .ph img {
    transition: opacity 520ms var(--e-out), transform 700ms var(--e-out);
  }
  /* The blur under a landed photograph is dead weight, so it is faded out too. */
  .ph--in { background-image: none; }
}
@media (prefers-reduced-motion: reduce) {
  .ph img { opacity: 1; transform: none; }
}

/* ======================================================================
 * THE HERO.
 *
 * Words on the left, one photograph on the right at full strength. Nothing is
 * printed over the picture, so it needs no veil and no contrast compromise.
 * Below 900px it stacks: words, picture, then the figures.
 * ==================================================================== */
.hero--split { display: grid; gap: var(--sp-5) var(--sp-6); align-items: center; }
.hero__words { min-width: 0; }
.hero__foot { min-width: 0; }

.hero__shot {
  margin: 0;
  min-width: 0;
  position: relative;
}
.hero__shot .ph {
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-3);
}
/* The gold rule along the top edge, the same one the receipts carry. */
.hero__shot .ph::after {
  content: "";
  position: absolute; inset: 0 0 auto 0; height: 4px; z-index: 2;
  background: linear-gradient(90deg, var(--gold-dp, #A87C1E), var(--gold, #D4A843) 45%, var(--gold-lt, #E8C36B));
}
.hero__shot figcaption {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--mut);
  letter-spacing: .01em;
}

@media (min-width: 900px) {
  .hero--split {
    grid-template-columns: minmax(0, 1.02fr) minmax(0, .98fr);
    grid-template-areas: "words shot" "foot foot";
  }
  .hero__words { grid-area: words; }
  .hero__shot  { grid-area: shot; }
  .hero__foot  { grid-area: foot; }
}


/* A photograph used as the face of a card or a segment. */
.ph--seg { border-radius: var(--r-lg); }
.ph--empty { max-width: 300px; margin: 0 auto var(--sp-3); }
.ph--band { border-radius: 0; }

/* Slow, almost invisible drift. It makes a still photograph feel alive without
   ever competing with the figures on top of it. */
@media (prefers-reduced-motion: no-preference) {
  .ph--drift.ph--in img { animation: phDrift 26s var(--e-inout) infinite alternate; }
  @keyframes phDrift {
    from { transform: scale(1.06) translate3d(-1%, 0, 0); }
    to { transform: scale(1.12) translate3d(1%, -1.5%, 0); }
  }
}

/* The bed the hero sits in. It is a real full-width element rather than a `100vw`
   trick, because `100vw` includes the scrollbar and would add a horizontal
   scrollbar of exactly that width. */
.herobed { position: relative; overflow: clip; }

/* The institution wall. Six photographs, captioned, that cascade in as the
   section is reached. */
.segwall {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(184px, 1fr));
  gap: var(--sp-3);
  margin-bottom: var(--sp-4);
}
.segtile { margin: 0; }
.segtile figcaption {
  margin-top: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--mut);
  letter-spacing: .01em;
}
@media (hover: hover) {
  .segtile:hover .ph img { transform: scale(1.05); }
}

/* A full-bleed photographic band inside a section. */
.bandshot {
  margin: 0 0 var(--sp-4);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.bandshot .ph { border-radius: 0; padding-bottom: 0 !important; height: clamp(180px, 30vw, 320px); }

/* An empty state now leads with a photograph of the thing that is not there. */
.empty .ph--empty { margin-bottom: var(--sp-3); opacity: .92; }

/* A photograph that opens a screen. Kept short, so the working part of the
   screen is still above the fold on a phone. */
.ph--lead { padding-bottom: 0 !important; height: clamp(150px, 34vw, 210px); border-radius: var(--r-lg); }
.ph--lead img { object-position: 50% 45%; }
