@import url("fonts.css");
@import url("tokens.css");

/* ==========================================================================
   Reset
   ========================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* link jumps stop below the sticky glass bar, not under it */
  scroll-padding-top: var(--nav-scroll-pad);
}

body,
h1,
h2,
h3,
p,
figure,
blockquote,
ul,
li {
  margin: 0;
  padding: 0;
}

ul {
  list-style: none;
}

img {
  display: block;
  max-width: none;
}

a {
  color: inherit;
  text-decoration: none;
}

body {
  background: var(--ww-canvas);
  font-family: var(--font-sans);
  font-weight: var(--font-weight-regular);
  color: var(--base-foreground);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-variant-ligatures: none;
}

p,
h1,
h2,
h3 {
  overflow-wrap: break-word;
  font-weight: inherit;
  font-size: inherit;
  line-height: inherit;
}

/* ==========================================================================
   Page canvas — the 1440px Figma frame
   ========================================================================== */

/* The comp is a fixed 1440 canvas. On a window narrower than that — a 1440px
   monitor only offers ~1425 once the scrollbar takes its gutter — the canvas
   scales to fit rather than forcing a horizontal scrollbar. --page-zoom is set
   in main.js from clientWidth; 100vw would include the scrollbar and
   under-correct by exactly the amount that causes the overflow. */
.page {
  width: var(--canvas-width);
  zoom: var(--page-zoom, 1);
  margin-inline: auto;
  background: var(--ww-canvas);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* Figma exports each stacked section with a -1px bottom margin so adjacent
   fills overlap and no hairline seam shows between them. */
.section {
  width: 100%;
  margin-bottom: -1px;
  position: relative;
  overflow: clip;
  flex-shrink: 0;
}

/* WalletWise monogram watermark, extracted from the source render. */
.watermark::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("../assets/icons/watermark.png");
  background-repeat: repeat;
  background-size: 35px 19px;
  pointer-events: none;
}

/* Dark surfaces carry the mark as white; light surfaces as ink. Opacities are
   calibrated against the source render — the mark is a ~3% texture. */
.watermark--on-dark::before {
  opacity: 0.0275;
  filter: invert(1);
}

.watermark--on-light::before {
  /* 0.0495 matched the comp exactly; dialled back 30% by request. */
  opacity: 0.0347;
}

.watermark > * {
  position: relative;
}

/* ==========================================================================
   Appear transitions
   The page fades up as a whole, then each block rises into place as it comes
   into view. Gated on .js so nothing is ever left hidden without scripting.
   Never applied to an ancestor of a sticky element — a transform on one would
   break both the nav and the pinned feature section.
   ========================================================================== */

@keyframes page-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.js body {
  animation: page-in 400ms ease-out both;
}

.js [data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 700ms cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 700ms cubic-bezier(0.22, 0.61, 0.36, 1);
  transition-delay: var(--reveal-delay, 0ms);
}

.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}

/* ==========================================================================
   Typography utilities — Lume Mobile/* styles
   ========================================================================== */

.t-xs-med    { font-size: var(--font-size-10); line-height: var(--line-height-10); font-weight: var(--font-weight-medium); }
.t-sm-med    { font-size: var(--font-size-12); line-height: var(--line-height-12); font-weight: var(--font-weight-medium); }
.t-md-reg    { font-size: var(--font-size-14); line-height: var(--line-height-14); font-weight: var(--font-weight-regular); }
.t-md-med    { font-size: var(--font-size-14); line-height: var(--line-height-14); font-weight: var(--font-weight-medium); }
.t-lg-med    { font-size: var(--font-size-16); line-height: var(--line-height-16); font-weight: var(--font-weight-medium); }
.t-xl-reg    { font-size: var(--font-size-20); line-height: var(--line-height-20); font-weight: var(--font-weight-regular); }
.t-xl-med    { font-size: var(--font-size-20); line-height: var(--line-height-20); font-weight: var(--font-weight-medium); }
.t-xl-semi   { font-size: var(--font-size-20); line-height: var(--line-height-20); font-weight: var(--font-weight-semibold); }
.t-2xl-med   { font-size: var(--font-size-24); line-height: var(--line-height-24); font-weight: var(--font-weight-medium); }
.t-2xl-semi  { font-size: var(--font-size-24); line-height: var(--line-height-24); font-weight: var(--font-weight-semibold); }
.t-3xl-med   { font-size: var(--font-size-32); line-height: var(--line-height-32); font-weight: var(--font-weight-medium); }
.t-4xl-med   { font-size: var(--font-size-40); line-height: var(--line-height-40); font-weight: var(--font-weight-medium); }
.t-4xl-semi  { font-size: var(--font-size-40); line-height: var(--line-height-40); font-weight: var(--font-weight-semibold); }
.t-5xl-med   { font-size: var(--font-size-48); line-height: var(--line-height-48); font-weight: var(--font-weight-medium); }

.nowrap { white-space: nowrap; }

/* ==========================================================================
   Lume Button — outline / pill, size default (40px)
   ========================================================================== */

/* Shipped as a single vector: the pill, its 1px Base/Input border, both store
   glyphs and the label are all in the asset, so it stays sharp at any scale. */
.btn-download {
  display: block;
  width: 155px;
  height: 40px;
  border-radius: 48px;
  flex-shrink: 0;
  cursor: pointer;
  transition: box-shadow 150ms ease-in-out;
}

.btn-download:hover {
  box-shadow: var(--shadow-form-field);
}

.btn-download img {
  width: 155px;
  height: 40px;
}

/* ==========================================================================
   Lume Avatar group
   ========================================================================== */

.avatar-group {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.avatar-group img {
  object-fit: cover;
  border-radius: 9999px;
  flex-shrink: 0;
}

.avatar-group--sm img {
  width: 26.04px;
  height: 26.04px;
  border: 0.294px solid #fff;
}

.avatar-group--sm img:not(:last-child) {
  margin-right: -9.222px;
}

.avatar-group--lg img {
  width: 48px;
  height: 48px;
  border: 1px solid #fff;
}

.avatar-group--lg img:not(:last-child) {
  margin-right: -17px; /* the comp's overlap, scaled with the avatars */
}

/* The nav's download pill, as live HTML so its label can roll on hover:
   the label sits in a one-line window with a copy of itself drawn just
   below (text-shadow); hovering slides it up so the copy rolls into place.
   Sized to match download-app.svg — the white pill, its hairline, the two
   store marks, and Geist Medium 14 for the label. */
.btn-roll {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  width: 155px;
  height: 40px;
  padding-left: 12px;
  border-radius: 999px;
  background: var(--base-background);
  box-shadow: inset 0 0 0 1px var(--border-default);
  color: var(--base-foreground);
  font-size: var(--font-size-14);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
  flex-shrink: 0;
  transition: box-shadow 150ms ease-in-out;
}

.btn-roll:hover {
  box-shadow: inset 0 0 0 1px var(--border-default), var(--shadow-form-field);
}

.btn-roll:focus-visible {
  outline: 2px solid var(--ww-white);
  outline-offset: 3px;
}

.btn-roll__icons {
  flex-shrink: 0;
}

.btn-roll__label {
  display: block;
  overflow: hidden;
}

.btn-roll__in {
  display: block;
  line-height: 1.2;
}

@media (hover: hover) {
  .btn-roll__in {
    text-shadow: 0 1.5em 0 currentColor;
    transition: transform 700ms cubic-bezier(0, 0, 0.2, 1);
  }

  .btn-roll:hover .btn-roll__in,
  .btn-roll:focus-visible .btn-roll__in {
    transform: translateY(-1.5em);
  }
}

@media (prefers-reduced-motion: reduce) {
  .btn-roll__in {
    text-shadow: none;
    transition: none;
  }

  .btn-roll:hover .btn-roll__in,
  .btn-roll:focus-visible .btn-roll__in {
    transform: none;
  }
}

/* ==========================================================================
   1 · Hero  (hero-02)
   One left-hand column — headline, copy, CTA — against the device on the
   right. The comp is 1440 x 829; its content stacks to 768 and the remaining
   gradient runs on beneath. The previous split layout is in variants/hero-01/.
   ========================================================================== */

.hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* room for the nav (which hangs over the hero from .nav-dock) and the
     80px it used to leave before the row */
  padding-top: calc(var(--nav-height) + 80px);
  /* space between the header and the hero row */
  gap: 80px;
  width: var(--canvas-width);
  height: var(--hero-height);
  background-image: var(--ww-hero-gradient);
}

/* The hero gradient, alive: three glows drift slowly across it, like a
   mesh-gradient shader. Painted by whichever element carries the gradient
   (the hero, or body::before, the full-width band behind it on wide screens). Still under
   reduced motion. */
.hero {
  background-image: var(--ww-hero-glow), var(--ww-hero-gradient);
  background-size: 70% 110%, 60% 95%, 90% 60%, 100% 100%;
  background-repeat: no-repeat;
  background-position: 0% 0%, 100% 15%, 15% 100%, 0 0;
  animation: hero-drift 20s ease-in-out infinite alternate;
}

@keyframes hero-drift {
  33% { background-position: 100% 30%, 0% 70%, 100% 100%, 0 0; }
  66% { background-position: 20% 100%, 80% 0%, 0% 100%, 0 0; }
  100% { background-position: 90% 0%, 15% 100%, 60% 100%, 0 0; }
}

@media (prefers-reduced-motion: reduce) {
  .hero,
  body::before {
    animation: none;
  }
}

/* Sticky across the whole page (a direct child of body, before .page).
   Zero height, so the bar hanging from it covers the hero rather than
   pushing it down. */
.nav-dock {
  position: sticky;
  top: 0;
  z-index: 20;
  height: 0;
}

/* The full-width bar: transparent on the hero, frosted once scrolled. */
.nav-bar {
  position: relative;
  width: 100%;
  transition: background-color 300ms ease, box-shadow 300ms ease,
    backdrop-filter 300ms ease, transform 300ms cubic-bezier(0.2, 0, 0, 1);
}

/* Scrolling down tucks the bar away (its shadow too); scrolling up brings
   it straight back (initNav). */
.nav-bar.is-tucked {
  transform: translateY(calc(-100% - 32px));
  transition-timing-function: ease, ease, ease, cubic-bezier(0.4, 0, 1, 1);
}

/* The nav itself keeps the canvas's width and zoom, so its contents sit on
   the page gutters at every desktop width. */
.nav {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: var(--canvas-width);
  margin-inline: auto;
  padding: var(--numbers-24) var(--page-gutter);
  zoom: var(--page-zoom, 1);
  transition: padding 300ms cubic-bezier(0.2, 0, 0, 1);
}

/* Scrolled: a traditional bar pinned to the top, full width, frosted —
   blurred and saturated, tinted ink so the white links hold up over light
   sections, with a hairline along its bottom. A touch slimmer than at rest. */
.nav-bar.is-scrolled {
  background: var(--ww-glass);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  backdrop-filter: blur(20px) saturate(180%);
  box-shadow: inset 0 -1px 0 var(--ww-glass-edge), 0 6px 24px rgba(15, 6, 34, 0.18);
}

.nav-bar.is-scrolled .nav {
  padding-block: var(--numbers-16);
}

.nav__brand {
  display: flex;
  align-items: center;
  gap: var(--gap-12);
  flex-shrink: 0;
}

.nav__brand img {
  width: 31.806px;
  height: 32px;
  object-fit: cover;
}

.nav__brand span {
  font-size: var(--font-size-24);
  line-height: var(--line-height-24);
  font-weight: var(--font-weight-semibold);
  color: var(--base-background);
  white-space: nowrap;
}

.nav__right {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--gap-16);
  flex-shrink: 0;
}

.nav__list {
  display: flex;
  align-items: center;
  gap: var(--gap-16);
}

/* The panel's trailing CTA is for the mobile menu only. */
.nav__list-cta {
  display: none;
}

/* Hamburger — only ever shown below the reflow breakpoint. */
.nav__toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 44px;
  height: 44px;
  padding: 10px;
  margin: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.nav__toggle span {
  display: block;
  width: 100%;
  height: 2px;
  border-radius: 2px;
  background: #fff;
  transition: transform 200ms ease-out, opacity 150ms ease-out;
}

.nav__toggle[aria-expanded="true"] span:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

.nav__toggle[aria-expanded="true"] span:nth-child(2) {
  opacity: 0;
}

.nav__toggle[aria-expanded="true"] span:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

.nav__toggle:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
  border-radius: var(--radius-4);
}

.nav__link {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px 12px;
  font-size: var(--font-size-14);
  line-height: var(--line-height-14);
  font-weight: var(--font-weight-medium);
  color: var(--base-background);
  white-space: nowrap;
  transition: opacity 150ms ease-in-out;
}

.nav__link--caret {
  gap: var(--gap-6);
  padding: 10px 0 10px 12px;
}

/* The Figma nav uses an icon-font chevron (element-pack U+E912) that renders
   with no visible glyph but still advances 8px. Reserved so the nav items land
   on their designed x-positions. */
.nav__link--caret::after {
  content: "";
  display: block;
  width: 8px;
  height: 10px;
  flex-shrink: 0;
}

.nav__link:hover {
  opacity: 0.75;
}

.hero__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  height: 648px;
  padding: 0 var(--page-gutter);
  flex-shrink: 0;
}

.hero__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-40);
  width: 608px;
  padding-bottom: 96px;
  flex-shrink: 0;
}

.hero__text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
  width: 100%;
  color: #fff;
  flex-shrink: 0;
}

.hero__text h1 {
  min-width: 100%;
  width: min-content;
  font-size: 56px;
  line-height: var(--ww-title-leading);
  font-weight: var(--font-weight-semibold);
}

.hero__text p {
  width: 458px;
  font-size: var(--font-size-20);
  line-height: 34px;
  font-weight: var(--font-weight-regular);
}

/* A copy of the CTA's code + join line (.cta__social), in white for the
   dark hero. */
.hero__qr {
  display: flex;
  align-items: center;
  gap: var(--gap-24);
}

.hero__qr > img {
  width: 116px;
  height: 116px;
  padding: var(--numbers-8);
  border-radius: var(--radius-8);
  background: var(--ww-white);
}

.hero__qr .avatar-group--lg img {
  width: 40px; /* a touch smaller than the CTA's (client) */
  height: 40px;
  /* half a pixel more white ring on the dark hero (client): as a shadow,
     since browsers floor fractional border widths */
  box-shadow: 0 0 0 0.5px var(--ww-white);
}

.hero__qr .avatar-group--lg img:not(:last-child) {
  margin-right: -14px; /* the overlap, scaled with them */
}

.hero__qr-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-12);
  width: 161px;
}

.hero__qr-text p {
  font-size: 18.393px;
  line-height: 27.589px;
  font-weight: var(--font-weight-medium);
  color: var(--ww-white);
}

.hero__download {
  display: none; /* phones and touch screens only — see below */
}

/* A touch screen (any width — tablets and touch laptops get the desktop
   canvas) can't scan a code shown on itself: give it the smart-link
   button instead. */
@media (hover: none) and (pointer: coarse) {
  .hero__qr {
    display: none;
  }

  .hero__download {
    display: block;
  }
}

/* The phone sits inside a lilac card (a gradient through the testimonials'
   colour), a little
   way down from its top, and runs on past the card's bottom edge, which
   crops it. Proportions are percentages of the box, so the phone layout
   scales with it. */
.hero__device {
  position: relative;
  width: 500px;
  aspect-ratio: 5 / 6;
  overflow: clip;
  border-radius: 32px;
  background: var(--ww-lilac-gradient);
  flex-shrink: 0;
}

.hero__device img {
  position: absolute;
  left: 50%;
  top: 12%; /* 72 of 600 */
  translate: -50% 0;
  width: 62.4%; /* 312 of 500 */
  height: auto;
  max-width: none;
}

/* ==========================================================================
   2 · Trusted partners
   ========================================================================== */

.partners {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--numbers-36); /* label to logos */
  padding: var(--numbers-64) var(--page-gutter); /* well under the comp's height (client) */
  background: var(--base-muted);
}

/* A small eyebrow, untracked; capitals in CSS so it reads as words. */
.partners__label {
  font-size: var(--font-size-14);
  line-height: var(--line-height-14);
  font-weight: var(--font-weight-medium);
  text-transform: uppercase;
  color: var(--base-foreground);
}

/* A still row, the logos grouped in the centre. */
.partners__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--numbers-32) var(--numbers-64);
  width: 100%;
}

.partners__row img {
  /* No width/height here — the scaled-down size lives on the HTML
     attributes; setting both to `auto` in CSS would use each SVG's own
     intrinsic size instead and silently undo that. */
  flex-shrink: 0;
}

@media (prefers-reduced-motion: reduce) {
  .nav,
  .nav-bar {
    transition: none;
  }
}

/* ==========================================================================
   3 · Built for the way you spend and save  (feature-03)
   Figma 2328:114016. Five steps beside one visual. The open step has a
   line beside it that fills over its visual's loop (initFeatures); on
   desktop the next step then opens by itself.
   Previous layouts: variants/feature-01/ (pinned), variants/feature-02/.
   ========================================================================== */

/* Off-white panel (client request) so the section reads apart from the
   white Trusted Partners strip above it. */
.features {
  background: var(--base-background);
}

.features__pin {
  display: flex;
  justify-content: center;
}

.features__frame {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 90px;
  width: var(--canvas-width);
  padding: var(--page-gutter);
  flex-shrink: 0;
}

/* One paragraph, two voices, sharing the 40/58 line box. */
/* Centred title + description (client request, replaces the comp's
   left-aligned two-voice paragraph). */
.features__intro {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-16);
  align-self: center;
  width: 760px;
  text-align: center;
  flex-shrink: 0;
}

.features__head {
  font-size: var(--font-size-40);
  line-height: var(--ww-title-leading);
  font-weight: var(--font-weight-semibold);
  color: var(--base-foreground);
}

.features__lede {
  font-size: var(--font-size-20);
  line-height: var(--line-height-20);
  font-weight: var(--font-weight-regular);
  color: var(--base-muted-foreground);
}

.features__row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  width: 100%;
  flex-shrink: 0;
}

.features__list {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-40);
  width: 563px;
  flex-shrink: 0;
}

/* Closed: a muted 20px label. Open: the label lifts to 24px and the copy
   shows, with a short line beside the title that fills top to bottom over
   the visual's loop (.is-timing, --loop from initFeatures). Without JS, or
   with reduced motion, the open step's line rests full. */
.features__item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding-left: var(--numbers-20);
}

.features__item h3 {
  position: relative;
  font: inherit;
}

.features__item h3::before,
.features__item h3::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(-1 * var(--numbers-20));
  width: 2px;
  border-radius: 999px;
  opacity: 0;
  transition: opacity 260ms ease-out;
}

.features__item h3::before {
  background: var(--neutral-200);
}

.features__item h3::after {
  background: var(--ww-primary);
  transform-origin: top;
}

.features__item.is-active h3::before,
.features__item.is-active h3::after {
  opacity: 1;
}

.js .features__item h3::after {
  transform: scaleY(0);
}

.features__item.is-timing h3::after {
  animation: feature-timer var(--loop, 8s) linear both;
}

@keyframes feature-timer {
  from { transform: scaleY(0); }
  to { transform: scaleY(1); }
}

@media (prefers-reduced-motion: reduce) {
  .js .features__item h3::after {
    transform: none;
  }
}

.features__step {
  display: flex;
  align-items: center;
  gap: var(--gap-12);
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--font-size-20);
  line-height: var(--line-height-20);
  font-weight: var(--font-weight-medium);
  color: var(--neutral-400); /* a step lighter than muted, so the active one leads */
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
  transition: color 260ms ease-out, font-size 260ms ease-out,
    line-height 260ms ease-out;
}

.features__step:hover {
  color: var(--base-foreground);
}

/* Each label is led by its Lucide icon (Lume's set) in a rounded square,
   the same chip as the bill-type rows: grey while closed, brand when open.
   The chip stays one size while the label grows. */
/* Hidden for now (client); delete the display: none below to bring the
   icons back. */
.features__icon {
  display: none;
  /* display: grid; */
  place-items: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-12);
  background: var(--neutral-100);
  transition: background-color 260ms ease-out;
}

.features__icon svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.features__item.is-active .features__icon {
  background: var(--ww-lilac-tint);
}

.features__step:focus-visible {
  outline: 2px solid var(--ww-primary);
  outline-offset: 4px;
  border-radius: var(--radius-4);
}

/* Active label in the WalletWise primary (client request, overrides the
   comp's ink). */
.features__item.is-active .features__step {
  font-size: var(--font-size-24);
  line-height: var(--line-height-24);
  color: var(--ww-primary);
  cursor: default;
}

/* Collapsed with a 0fr grid row so opening animates to the copy's own height. */
.features__desc {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 320ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.features__desc > div {
  min-height: 0;
  overflow: hidden;
}

.features__item.is-active .features__desc {
  grid-template-rows: 1fr;
}

/* The copy only: on phones the visual is moved in here too (initFeatures). */
.features__desc > div > p {
  width: 525px;
  padding-top: var(--gap-16);
  font-size: var(--font-size-20);
  line-height: var(--line-height-20);
  font-weight: var(--font-weight-regular);
  color: var(--base-foreground);
}

/* The five visuals share one cell and cross-fade. --vis-scale is 1 on the
   desktop canvas; on phones initFeatures() shrinks the 508px card to fit. */
/* The 508px card keeps its designed geometry and is scaled as a unit
   (--vis-scale from initFeatures): down to fit a short screen on desktop,
   into the column on phones. */
.features__visuals {
  position: relative;
  width: calc(508px * var(--vis-scale, 1));
  aspect-ratio: 1 / 1;
  flex-shrink: 0;
}

/* Out of flow, or the unscaled 508px would still size the box. */
.features__visual {
  position: absolute;
  top: 0;
  left: 0;
  transform: scale(var(--vis-scale, 1));
  transform-origin: top left;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 508px;
  padding: var(--numbers-16) var(--numbers-20);
  border-radius: 32px;
  opacity: 0;
  transition: opacity 260ms ease-out;
}

.features__visual.is-active {
  opacity: 1;
}

.features__visual-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 32px;
  pointer-events: none;
}

.features__visual-stage {
  position: relative;
  width: 100%;
  height: 476px;
  overflow: clip;
  border-radius: var(--radius-24);
}

/* The app's payment sheet ("Bottom draw" in the comp), set in Inter like the
   app. Odd sizes are the comp's own — the sheet was scaled in Figma. */
.pay-card {
  position: absolute;
  left: calc(50% + 0.2px);
  top: calc(50% + 0.36px);
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24.64px;
  width: var(--ww-mock-size);
  padding: 32.853px 24.64px;
  border: 1.027px solid var(--ww-card-stroke);
  border-radius: var(--ww-mock-radius);
  background: var(--base-background);
  filter: var(--drop-shadow-md);
  font-family: var(--font-fallback);
  color: var(--ww-foreground);
}

.pay-card__who {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14.965px;
  width: 100%;
  padding: 0 16.427px;
}

.pay-card__avatar {
  width: 89.024px;
  height: 89.024px;
  flex-shrink: 0;
}

.pay-card__id {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7.482px;
}

.pay-card__name {
  font-size: 23.917px;
  line-height: 1.4;
  font-weight: var(--font-weight-semibold);
  white-space: nowrap;
}

.pay-card__handle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6.239px;
  padding: 6.239px;
  border-radius: 37.435px;
  background: var(--ww-pill);
}

.pay-card__handle img {
  width: 22.311px;
  height: 22.447px;
  object-fit: cover;
  border-radius: 37.412px;
  flex-shrink: 0;
}

.pay-card__handle p {
  font-size: 18.717px;
  line-height: normal;
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
}

.pay-card__amount {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8.213px 16.427px;
}

.pay-card__amount p {
  font-size: 57.493px;
  line-height: normal;
  font-weight: var(--font-weight-medium);
  text-align: center;
  white-space: nowrap;
}

/* ---- Step 1 visual: instant transfer --------------------------------------
   The payment card at its full rendered size, with the recipient and the
   result stacked in one centred cell. initTransfer() drives the phases on
   .is-playing: .is-who brings "Send to", the avatar, name and handle in one
   by one → .is-who-out lifts them away one by one → .is-amount brings the
   amount (label + ₦10,000) in → .is-amount-out lifts it away → .is-paid
   springs in a big success circle, draws its check and brings "Transfer
   successful" up below → .is-leaving fades it before the loop. The amount
   and the success screen share the centre, never on screen together.
   Without .is-playing (no JS, reduced motion) the card shows the success
   screen. */
.pay-card--live {
  display: grid;
  place-items: center;
  height: var(--ww-mock-size);
}

.pay-card--live .pay-card__who,
.pay-card__result {
  grid-area: 1 / 1;
}

.pay-card__result {
  display: grid;
  place-items: center;
}

.pay-card__sum,
.pay-card__done {
  grid-area: 1 / 1;
}

.pay-card__sum {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.pay-card__label {
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
  font-weight: var(--font-weight-medium);
  color: var(--base-muted-foreground);
}

/* Success: a big check in Lume's success-accent circle, the words below. */
.pay-card__done {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-16);
}

.pay-card__tick {
  display: grid;
  place-items: center;
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: var(--base-success-accent-background);
  color: var(--base-success-accent-foreground);
}

.pay-card__tick path {
  stroke-dasharray: 100;
  stroke-dashoffset: 0;
}

.pay-card__done-text {
  font-size: var(--font-size-20);
  line-height: var(--line-height-20);
  font-weight: var(--font-weight-semibold);
  color: var(--base-foreground);
  white-space: nowrap;
}

/* Recipient parts: in from below, staggered by --i… */
.pay-card--live .pay-card__part {
  opacity: 0;
  translate: 0 12px;
  transition: opacity 400ms ease-out, translate 400ms cubic-bezier(0.22, 1, 0.36, 1);
  transition-delay: calc(var(--i) * 150ms);
}

.pay-card--live.is-who .pay-card__part {
  opacity: 1;
  translate: 0 0;
}

/* …and out upward, staggered the same way. */
.pay-card--live.is-who-out .pay-card__part {
  opacity: 0;
  translate: 0 -14px;
  transition-duration: 300ms;
  transition-timing-function: ease-in;
  transition-delay: calc(var(--i) * 110ms);
}

.pay-card--live .pay-card__amount p {
  font-variant-numeric: tabular-nums; /* every column the same width */
  line-height: 1.2;
}

/* Odometer digits (built by buildOdometer): a one-cell window onto a
   column of numerals, rolled to --to over --dur once .is-rolled is set.
   The masked edges let numerals glide in and out rather than clip. */
.roll {
  --cell: 1.2em;
  display: inline-block;
  height: var(--cell);
  overflow: hidden;
  vertical-align: top;
  -webkit-mask-image: linear-gradient(transparent, #000 26%, #000 74%, transparent);
  mask-image: linear-gradient(transparent, #000 26%, #000 74%, transparent);
}

.roll__strip {
  display: flex;
  flex-direction: column;
  transition: translate var(--dur) cubic-bezier(0.16, 1, 0.3, 1);
}

.roll__strip span {
  height: var(--cell);
}

.is-rolled .roll__strip {
  translate: 0 calc(var(--to) * -1 * var(--cell));
}

.pay-card--live .pay-card__sum {
  opacity: 0;
  translate: 0 12px;
  transition: opacity 400ms ease-out, translate 400ms cubic-bezier(0.22, 1, 0.36, 1);
}

.pay-card--live.is-amount .pay-card__sum {
  opacity: 1;
  translate: 0 0;
}

.pay-card--live.is-amount-out .pay-card__sum {
  opacity: 0;
  translate: 0 -14px;
  transition-duration: 300ms;
  transition-timing-function: ease-in;
}

/* The circle springs in, its check draws, then the words rise below. */
.pay-card--live .pay-card__tick {
  transition: opacity 250ms ease-out, scale 450ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.pay-card--live .pay-card__tick path {
  transition: stroke-dashoffset 350ms ease-out 250ms;
}

.pay-card--live .pay-card__done-text {
  transition: opacity 300ms ease-out 450ms, translate 300ms ease-out 450ms;
}

.pay-card--live.is-playing .pay-card__tick {
  opacity: 0;
  scale: 0.6;
}

.pay-card--live.is-playing .pay-card__tick path {
  stroke-dashoffset: 100;
}

.pay-card--live.is-playing .pay-card__done-text {
  opacity: 0;
  translate: 0 8px;
}

.pay-card--live.is-paid .pay-card__tick {
  opacity: 1;
  scale: 1;
}

.pay-card--live.is-paid .pay-card__tick path {
  stroke-dashoffset: 0;
}

.pay-card--live.is-paid .pay-card__done-text {
  opacity: 1;
  translate: 0 0;
}

.pay-card--live.is-leaving .pay-card__done {
  opacity: 0;
  transition: opacity 300ms ease-in;
}

.pay-card--live.is-snapping * {
  transition: none !important;
}

/* ---- Step 2 visual: virtual dollar card ------------------------------------
   Three screens from the app's Dollar card flow (Figma Jz7Mb6Mi9NnCC36xi8tPTs
   2115:12736), in the bill card's box and set in Inter like the app — the box
   is about the app screen's width, so the comp's own sizes are used 1:1.
   initDollarCard() plays it under .is-playing, pushing screen to screen the
   way the bill card does (.is-on in from the right, .is-off out to the left):
   Select Your Card (.is-picked checks Visa) → Customise card (--at moves the
   ring, the card's tint follows) → the card home (.is-rolled rolls the
   balance) → .is-leaving fades it. Without .is-playing (no JS, reduced
   motion) it rests on the card home. */
.dflow {
  position: absolute;
  left: calc(50% + 0.2px);
  top: calc(50% + 0.36px);
  translate: -50% -50%;
  display: grid;
  place-items: center;
  /* the bill card's box */
  width: var(--ww-mock-size);
  height: var(--ww-mock-size);
  padding: 0 24.64px;
  overflow: clip;
  border: 1.027px solid var(--ww-card-stroke);
  border-radius: var(--ww-mock-radius);
  background: var(--base-background);
  filter: var(--drop-shadow-md);
  font-family: var(--font-fallback);
  color: var(--ww-foreground);
}

.dflow__scene {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  gap: var(--gap-16);
  width: 100%;
}

.dflow__heading {
  font-size: var(--font-size-24);
  line-height: var(--line-height-24);
  font-weight: var(--font-weight-semibold);
}

.dflow__lede {
  margin-top: var(--numbers-4);
  font-size: var(--font-size-14);
  line-height: var(--line-height-14);
  color: var(--ww-muted);
}

/* The card's faint outline shapes (one asset, placed and sized per card). */
.dflow__lines {
  position: absolute;
  max-width: none;
  pointer-events: none;
}

/* -- Select Your Card ---------------------------------------------------- */
.dflow__option {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--gap-8);
  height: 150px;
  padding: 0 var(--numbers-24);
  overflow: clip;
  border-radius: var(--radius-16);
}

.dflow__option .dflow__lines {
  left: 138px;
  top: -56px;
  width: 409px;
  height: auto;
}

.dflow__option--visa {
  background: var(--ww-streak-gradient); /* "WalletWise/Gradient 1.0" */
  color: var(--ww-white);
}

.dflow__option--visa .dflow__visa {
  width: 64px;
  height: 20.725px;
}

.dflow__option--mc {
  border: 1px solid var(--ww-mc-stroke);
  background: var(--neutral-100);
}

.dflow__mc {
  width: 52px;
  height: 37px;
}

.dflow__option-name {
  font-size: var(--font-size-14);
  line-height: var(--line-height-14);
  font-weight: var(--font-weight-semibold);
}

.dflow__option--visa .dflow__option-name {
  padding: 0 var(--numbers-8);
}

.dflow__pick,
.dflow__radio {
  position: absolute;
  top: var(--numbers-16);
  right: var(--numbers-16);
  width: 24px;
  height: 24px;
}

/* An empty white ring on the gradient until the check springs in over it. */
.dflow__pick {
  border-radius: 50%;
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.7);
}

.dflow__pick-check {
  display: block;
}

/* -- Customise card ------------------------------------------------------ */
.dflow__scene--custom {
  align-items: center;
  gap: var(--gap-24);
}

.dflow__scene--custom .dflow__heading {
  align-self: flex-start;
}

/* The landscape card, turned upright as the app shows it. */
.dflow__upright {
  position: relative;
  width: 164px;
  height: 270px;
}

.dflow__card {
  position: relative;
  overflow: clip;
  border-radius: 18px;
  background: var(--ww-streak-gradient);
  color: var(--ww-white);
}

.dflow__upright .dflow__card {
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  width: 270px;
  height: 164px;
  rotate: 90deg;
  border-radius: 14px;
}

.dflow__upright .dflow__lines {
  left: 147px;
  top: -66px;
  width: 416px;
  height: auto;
}

.dflow__upright .dflow__visa {
  position: absolute;
  left: 19px;
  top: 18px;
  width: 42px;
  height: 13.6px;
}

.dflow__upright .dflow__balance {
  position: absolute;
  left: 19px;
  bottom: 22px;
  gap: 5px;
}

.dflow__upright .dflow__balance-label {
  font-size: 11px;
  letter-spacing: 0.11px;
}

.dflow__upright .dflow__balance-sum {
  font-size: 25px;
}

/* The chosen colour, laid over the brand gradient (--tint-on 0 shows the
   gradient through). Content sits above it. */
.dflow__card--tint::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--tint, transparent);
  opacity: var(--tint-on, 0);
  transition: opacity 350ms ease, background-color 350ms ease;
}

.dflow__card > * {
  position: relative;
}

.dflow__card > .dflow__lines {
  position: absolute;
}

.dflow__swatches {
  position: relative;
  display: flex;
  gap: 14px;
}

.dflow__swatch {
  position: relative;
  z-index: 1;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--c);
}

.dflow__swatch--brand {
  background: var(--ww-streak-gradient);
}

/* The selection ring: 5px out from swatch --at. */
.dflow__ring {
  position: absolute;
  left: -5px;
  top: -5px;
  width: 42px;
  height: 42px;
  border: 2px solid var(--ww-swatch-ring);
  border-radius: 50%;
  translate: calc(var(--at, 3) * 46px) 0;
  transition: translate 400ms cubic-bezier(0.2, 0, 0, 1);
}

/* -- Card home ----------------------------------------------------------- */
.dflow__scene--home .dflow__card {
  height: 208px;
}

.dflow__scene--home .dflow__lines {
  left: 186px;
  top: -84px;
  width: 527.841px;
  height: 421.197px;
}

.dflow__card-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: var(--numbers-24) var(--numbers-24) 0;
  font-size: var(--font-size-14);
  font-weight: var(--font-weight-semibold);
}

.dflow__card-top .dflow__visa {
  width: 53.282px;
  height: 17.255px;
}

.dflow__balance {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
}

.dflow__scene--home .dflow__balance {
  position: absolute;
  left: var(--numbers-24);
  bottom: var(--numbers-24);
}

.dflow__balance-label {
  font-size: var(--font-size-14);
  letter-spacing: 0.14px;
  color: rgba(255, 255, 255, 0.6);
}

.dflow__balance-sum {
  font-size: 32px;
  line-height: 1.2; /* one odometer cell (.roll) */
  font-variant-numeric: tabular-nums;
}

.dflow__actions {
  display: flex;
  justify-content: space-between;
  margin-top: var(--numbers-12);
  padding: 0 var(--numbers-32);
}

.dflow__action {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-16);
  font-size: var(--font-size-14);
  line-height: var(--line-height-14);
  font-weight: var(--font-weight-semibold);
  white-space: nowrap;
}

/* -- The flow -------------------------------------------------------------
   At rest only the card home shows. */
.dflow__scene {
  opacity: 0;
}

.dflow__scene--home {
  opacity: 1;
}

.dflow.is-playing .dflow__scene {
  opacity: 0;
  translate: 48px 0;
  transition: opacity 350ms ease-out 240ms, translate 500ms cubic-bezier(0.2, 0, 0, 1) 240ms;
}

.dflow.is-playing .dflow__scene.is-on {
  opacity: 1;
  translate: 0 0;
}

/* Out to the left first, so screens never overlap. */
.dflow.is-playing .dflow__scene.is-off {
  opacity: 0;
  translate: -40px 0;
  transition: opacity 220ms ease-in, translate 280ms cubic-bezier(0.4, 0, 1, 1);
}

/* Visa gets checked. */
.dflow.is-playing .dflow__pick-check {
  opacity: 0;
  scale: 0.4;
  transition: opacity 160ms ease, scale 380ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.dflow.is-playing.is-picked .dflow__pick-check {
  opacity: 1;
  scale: 1;
}

.dflow.is-playing .dflow__option--visa {
  transition: scale 150ms ease-out;
}

.dflow.is-pressing-visa .dflow__option--visa {
  scale: 0.98;
}

.dflow.is-leaving .dflow__scene--home {
  opacity: 0;
  transition: opacity 300ms ease-in;
}

.dflow.is-snapping * {
  transition: none !important;
}

/* ---- Step 3 visual: bill payments ------------------------------------------
   The services section's bill types as a stack of row cards (the app's
   provider-list rows), in the payment card's box and set in Inter like the
   app. initBills() plays it under .is-playing, one type per loop:
   .is-list brings the rows in one by one → .is-on on a row selects it (its
   check draws) → .is-page slides the list off left and that type's page
   (.is-on: its saved biller, logos cut from the service cards, and amount
   grid) in from the right → .is-amount picks an amount → .is-out fades the
   page → .is-paid springs in the success screen (the payment card's, as on
   Instant Transfers) → .is-leaving fades it. Without .is-playing (no JS,
   reduced motion) the card shows the success screen. */
.bills {
  position: absolute;
  left: calc(50% + 0.2px);
  top: calc(50% + 0.36px);
  translate: -50% -50%;
  display: grid;
  place-items: center;
  /* the payment card's width, stroke, radius and shadow, but nearly the
     stage's full height */
  width: var(--ww-mock-size);
  height: var(--ww-mock-size);
  padding: 0 24.64px;
  overflow: clip; /* the pages slide in and out at its edges */
  border: 1.027px solid var(--ww-card-stroke);
  border-radius: var(--ww-mock-radius);
  background: var(--base-background);
  filter: var(--drop-shadow-md);
  font-family: var(--font-fallback);
  color: var(--ww-foreground);
}

.bills__types,
.bills__page,
.bills .pay-card__done {
  grid-area: 1 / 1;
}

.bills__types,
.bills__page {
  display: flex;
  flex-direction: column;
  width: 100%;
}

/* -- The list ---------------------------------------------------------------- */
.bills__types {
  gap: var(--gap-12);
}

.bills__type {
  display: flex;
  align-items: center;
  gap: var(--gap-12);
  height: 64px;
  padding: 0 var(--numbers-16) 0 var(--numbers-12);
  border: 1px solid var(--ww-card-stroke);
  border-radius: var(--radius-16);
  background: var(--base-background);
}

.bills__icon {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-12);
  background: var(--ww-pill);
  color: var(--ww-muted);
}

.bills svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.bills__label {
  flex: 1;
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
  font-weight: var(--font-weight-medium);
}

/* An empty ring; filled purple with a drawn check once selected. */
.bills__radio {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1.5px var(--ww-card-stroke);
  color: var(--ww-white);
}

.bills__radio svg {
  stroke-width: 3;
}

.bills__radio path {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
}

/* Selected: the services cards' checked row. */
.bills__type.is-on,
.bills.is-amount .bills__opt--pick {
  border-color: var(--ww-primary);
  background: var(--ww-lilac-tint);
}

.bills__type.is-on .bills__icon {
  background: var(--base-background);
  color: var(--ww-primary);
}

.bills__type.is-on .bills__radio,
.bills.is-amount .bills__opt--pick .bills__radio {
  box-shadow: inset 0 0 0 11px var(--ww-primary);
}

.bills__type.is-on .bills__radio path,
.bills.is-amount .bills__opt--pick .bills__radio path {
  stroke-dashoffset: 0;
}

/* -- A biller's page ------------------------------------------------------- */
.bills__page {
  gap: var(--gap-16);
}

.bills__head {
  font-size: var(--font-size-20);
  line-height: var(--line-height-20);
  font-weight: var(--font-weight-semibold);
}

.bills__biller {
  display: flex;
  align-items: center;
  gap: var(--gap-12);
  padding: var(--numbers-12) var(--numbers-14);
  border-radius: var(--radius-16);
  background: var(--ww-pill);
}

.bills__logo {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border-radius: 50%;
}

/* Ikeja's mark is on white, so it gets the app's hairline ring. */
.bills__logo--ringed {
  box-shadow: 0 0 0 1px var(--ww-card-stroke);
}

.bills__logo--card {
  width: 64px;
  height: 39px;
  border-radius: var(--radius-6);
}

.bills__name {
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
  font-weight: var(--font-weight-semibold);
  white-space: nowrap;
}

.bills__sub {
  font-size: var(--font-size-12);
  line-height: var(--line-height-12);
  color: var(--base-muted-foreground);
  white-space: nowrap;
}

.bills__amount {
  margin-bottom: calc(var(--gap-8) - var(--gap-16));
  font-size: var(--font-size-14);
  line-height: var(--line-height-14);
  font-weight: var(--font-weight-medium);
  color: var(--base-muted-foreground);
}

/* Amounts as a 2×2 grid of cards, styled like the bill-type rows: the
   amount (a data plan's size, its price below), the radio to its right. */
.bills__opts {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gap-10);
}

.bills__opt {
  display: grid;
  grid-template-columns: 1fr auto;
  align-content: center;
  align-items: center;
  column-gap: var(--gap-8);
  height: 72px;
  padding: 0 var(--numbers-14) 0 var(--numbers-16);
  border: 1px solid var(--ww-card-stroke);
  border-radius: var(--radius-16);
  font-variant-numeric: tabular-nums;
}

.bills__opt .bills__radio {
  grid-column: 2;
  grid-row: 1 / span 2;
}

.bills__opt .bills__label {
  font-weight: var(--font-weight-semibold);
}

.bills__price {
  font-size: var(--font-size-12);
  line-height: var(--line-height-12);
  font-weight: var(--font-weight-medium);
  color: var(--base-muted-foreground);
}

/* -- The flow ----------------------------------------------------------------
   At rest only the success screen shows. */
.bills__type,
.bills__page {
  opacity: 0;
}

.bills__page {
  visibility: hidden;
}

.bills__page.is-on {
  visibility: visible;
}

/* Rows in from below, staggered by --i. */
.bills.is-playing .bills__type {
  translate: 0 12px;
  transition-property: opacity, translate, border-color, background-color;
  transition-duration: 400ms, 400ms, 250ms, 250ms;
  transition-timing-function: ease-out, cubic-bezier(0.22, 1, 0.36, 1), ease, ease;
  transition-delay: calc(var(--i) * 90ms), calc(var(--i) * 90ms), 0ms, 0ms;
}

.bills.is-list .bills__type {
  opacity: 1;
  translate: 0 0;
}

.bills.is-playing .bills__icon {
  transition: color 250ms ease, background-color 250ms ease;
}

.bills.is-playing .bills__radio {
  transition: box-shadow 250ms cubic-bezier(0.2, 0, 0, 1);
}

.bills.is-playing .bills__radio path {
  transition: stroke-dashoffset 300ms ease-out 150ms;
}

/* The next page: the list clears off left, then the page arrives from the
   right — one after the other, so the two never overlap. */
.bills.is-page .bills__types {
  opacity: 0;
  translate: -40px 0;
  transition: opacity 220ms ease-in, translate 280ms cubic-bezier(0.4, 0, 1, 1);
}

.bills.is-playing .bills__page {
  translate: 48px 0;
  transition: opacity 350ms ease-out 240ms, translate 500ms cubic-bezier(0.2, 0, 0, 1) 240ms;
}

.bills.is-page .bills__page {
  opacity: 1;
  translate: 0 0;
}

.bills.is-playing .bills__opt {
  transition: border-color 250ms ease, background-color 250ms ease;
}

/* Then the page fades away before the success screen. */
.bills.is-out .bills__page {
  opacity: 0;
  translate: 0 -14px;
  transition: opacity 300ms ease-in, translate 300ms ease-in;
}

/* Success: the payment card's circle springs in, its check draws, then the
   words rise below. */
.bills .pay-card__tick {
  transition: opacity 250ms ease-out, scale 450ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.bills .pay-card__tick path {
  stroke: currentColor;
  stroke-width: 2.5;
  transition: stroke-dashoffset 350ms ease-out 250ms;
}

.bills .pay-card__done-text {
  transition: opacity 300ms ease-out 450ms, translate 300ms ease-out 450ms;
}

.bills.is-playing .pay-card__tick {
  opacity: 0;
  scale: 0.6;
}

.bills.is-playing .pay-card__tick path {
  stroke-dashoffset: 100;
}

.bills.is-playing .pay-card__done-text {
  opacity: 0;
  translate: 0 8px;
}

.bills.is-paid .pay-card__tick {
  opacity: 1;
  scale: 1;
}

.bills.is-paid .pay-card__tick path {
  stroke-dashoffset: 0;
}

.bills.is-paid .pay-card__done-text {
  opacity: 1;
  translate: 0 0;
}

.bills.is-leaving .pay-card__done {
  opacity: 0;
  transition: opacity 300ms ease-in;
}

.bills.is-snapping * {
  transition: none !important;
}

/* ---- Step 4 visual: biometrics ---------------------------------------------
   Face ID in the style of Apple's glyph sequence (in brand purple): the
   face glances around inside its corner brackets, the brackets close into
   a circle, the face fades, the circle spins in 3D and a check draws in.
   Then the glyph fades and a Lume success badge ("Identity verified")
   takes the centre of the card. The card's box matches the payment card's
   (.pay-card) exactly. The glyph
   geometry is driven per frame by initFaceId(); the phase classes
   (.is-scanning → .is-verified → .is-complete → .is-leaving) drive the
   fades here. The markup's resting state is the circle + check (no JS,
   reduced motion). */
.faceid {
  position: absolute;
  left: calc(50% + 0.2px);
  top: calc(50% + 0.36px);
  translate: -50% -50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* the shared step-card box: size, stroke, radius, shadow */
  width: var(--ww-mock-size);
  height: var(--ww-mock-size);
  border: 1.027px solid var(--ww-card-stroke);
  border-radius: var(--ww-mock-radius);
  background: var(--base-background);
  filter: var(--drop-shadow-md);
  font-family: var(--font-sans);
}

.faceid__visual {
  display: grid;
  place-items: center;
  width: 150px;
  height: 150px;
  margin-bottom: var(--gap-16);
  perspective: 420px;
}

.faceid__glyph {
  width: 128px;
  height: 128px;
  color: var(--ww-primary);
  fill: none;
  stroke: currentColor;
  stroke-width: 5;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: opacity 250ms ease-out, scale 250ms ease-out;
}

.faceid__face {
  transform-origin: 50px 50px;
}

.faceid__status {
  display: grid;
  place-items: center;
  height: 30px;
}

.faceid__scanning {
  grid-area: 1 / 1;
  white-space: nowrap;
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
  color: var(--base-muted-foreground);
  opacity: 0;
  transition: opacity 250ms ease-in;
}

/* Lume's success badge as a pill, sized up to stand alone in the card. */
.faceid__badge {
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  display: inline-flex;
  align-items: center;
  padding: var(--numbers-8) var(--numbers-20);
  border-radius: 999px;
  background: var(--base-success-accent-background);
  color: var(--base-success-accent-foreground);
  font-size: var(--font-size-20);
  line-height: var(--line-height-20);
  font-weight: var(--font-weight-semibold);
  white-space: nowrap;
  opacity: 0;
  scale: 0.92;
  transition: opacity 300ms ease-out, scale 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

.faceid.is-scanning .faceid__scanning--face {
  opacity: 1;
  transition-timing-function: ease-out;
}

/* The check has landed: the glyph and its caption clear the stage… */
.faceid.is-verified .faceid__glyph {
  opacity: 0;
  scale: 0.9;
  transition: opacity 300ms ease-in, scale 300ms ease-in;
}

/* …and the badge takes it. */
.faceid.is-complete .faceid__badge {
  opacity: 1;
  scale: 1;
}

.faceid.is-leaving .faceid__badge {
  opacity: 0;
  transition: opacity 300ms ease-in;
}

.faceid.is-snapping * {
  transition: none !important;
}

/* ---- Step 5 visual: rewards streak ------------------------------------
   After Figma 1987:66117, restyled off its gradient into the other steps'
   language (client): plain on the white frame, set in Inter like the app,
   the count in brand purple and each day just its label and a check that
   fills brand purple. The streak and the commission share one centred cell. The markup's resting state is
   the finished streak card (all five days, 5) — what shows without JS or
   with reduced motion. While the step is open, initStreak() replays it:
   .is-pending unticks the days, they tick one by one, the count turns 0 → 5
   as Day 5 lands (--n), then .is-paying lifts the streak card out and
   brings the commission in the way Instant Transfers brings its amount
   (same size, label above, rising in) while ₦500.00 rolls up
   odometer-style (the shared .roll digits), then .is-leaving lifts it
   away before the loop restarts. */
/* The other steps' white frame (the bill card's box), holding the cards. */
.streak-frame {
  position: absolute;
  left: calc(50% + 0.2px);
  top: calc(50% + 0.36px);
  translate: -50% -50%;
  width: var(--ww-mock-size);
  height: var(--ww-mock-size);
  border: 1.027px solid var(--ww-card-stroke);
  border-radius: var(--ww-mock-radius);
  background: var(--base-background);
  filter: var(--drop-shadow-md);
}

.streak {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-fallback);
  color: var(--ww-foreground);
}

.streak__earned,
.streak__card {
  grid-area: 1 / 1;
  transition: opacity 450ms ease, transform 450ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* The commission, in from below like the transfer's amount… */
.streak__earned {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 400ms ease-out, transform 400ms cubic-bezier(0.22, 1, 0.36, 1);
}

.streak.is-paying .streak__earned {
  opacity: 1;
  transform: none;
  transition-delay: 320ms; /* once the streak card has gone */
}

.streak.is-paying .streak__card {
  opacity: 0;
  transform: translateY(-14px);
  transition: opacity 300ms ease-in, transform 300ms ease-in;
}

/* Label over the figure, the transfer card's .pay-card__sum and amount. */
.streak__earned {
  display: flex;
  flex-direction: column;
  align-items: center;
  white-space: nowrap;
}

.streak__label {
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
  font-weight: var(--font-weight-medium);
  color: var(--base-muted-foreground);
}

.streak__amount {
  padding: 8.213px 16.427px;
  font-size: 57.493px;
  line-height: 1.2; /* one odometer cell (.roll) */
  font-weight: var(--font-weight-medium);
  font-variant-numeric: tabular-nums;
  color: var(--ww-foreground);
}

.streak__card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-24);
  width: 351px; /* the frame's inner width, as the bill card's rows */
}

.streak__goal {
  width: 260px;
  margin-bottom: var(--numbers-16); /* lifted a little off the count (client) */
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
  font-weight: var(--font-weight-medium);
  text-align: center;
}

.streak__count {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.streak__count p {
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
  color: var(--base-muted-foreground);
  white-space: nowrap;
}

/* The count is a two-digit column (0, 5) behind a one-digit window: --n 0
   shows 0, 1 shows 5. The soft mask lets the digits glide through the
   window's edges. */
.streak__digits {
  --digit: 117.65px; /* Inter's normal line height at 97.218px */
  display: block;
  width: 69.101px;
  height: var(--digit);
  overflow: hidden;
  font-size: 97.218px;
  line-height: var(--digit);
  font-weight: var(--font-weight-semibold);
  color: var(--ww-primary);
  text-align: center;
  -webkit-mask-image: linear-gradient(transparent, #000 14%, #000 86%, transparent);
  mask-image: linear-gradient(transparent, #000 14%, #000 86%, transparent);
}

.streak__roll {
  display: flex;
  flex-direction: column;
  transform: translateY(calc(var(--n, 1) * -1 * var(--digit)));
  transition: transform 450ms cubic-bezier(0.33, 1, 0.68, 1);
}

.streak__days {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--gap-8);
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A day: just its label and its check — no tile (client). Pending days
   are muted with an empty grey ring. */
.streak__day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-10);
  font-size: var(--font-size-14);
  line-height: var(--line-height-14);
  font-weight: var(--font-weight-medium);
  color: var(--ww-foreground);
  white-space: nowrap;
  transition: color 250ms ease;
}

.streak__day.is-pending {
  color: var(--ww-muted);
}

/* An empty ring that fills brand purple as its white check draws in. */
.streak__tick {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 12px var(--ww-primary);
  color: var(--ww-white);
  transition: box-shadow 250ms cubic-bezier(0.2, 0, 0, 1);
}

.streak__tick svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.streak__tick path {
  stroke-dasharray: 100;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 300ms ease-out 150ms;
}

.streak__day.is-pending .streak__tick {
  box-shadow: inset 0 0 0 1.5px var(--ww-card-stroke);
}

.streak__day.is-pending .streak__tick path {
  stroke-dashoffset: 100;
}

/* Resets happen out of view, so they skip their transitions — except the
   streak card itself, which fades back in. */
.streak.is-snapping .streak__earned,
.streak.is-snapping .streak__earned *,
.streak.is-snapping .streak__card * {
  transition: none !important;
}

/* …and out upward at the end of the loop, as the transfer's amount leaves. */
.streak.is-leaving .streak__earned {
  opacity: 0;
  transform: translateY(-14px);
  transition: opacity 300ms ease-in, transform 300ms ease-in;
}

@media (prefers-reduced-motion: reduce) {
  .features__step,
  .features__desc,
  .features__visual {
    transition: none;
  }

  .streak__roll,
  .streak__day,
  .streak__tick,
  .streak__tick path,
  .pay-card--live *,
  .dflow *,
  .bills *,
  .faceid * {
    transition: none;
  }
}

/* ==========================================================================
   4 · Explore our services
   ========================================================================== */

.services {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 56px;
  height: 941.477px;
  padding: var(--page-gutter);
  background: var(--ww-ink-900);
}

.services__head {
  width: 822px;
  font-size: var(--font-size-40);
  line-height: var(--ww-title-leading);
  font-weight: var(--font-weight-medium);
  color: var(--base-background);
  flex-shrink: 0;
}

.services__head em {
  font-style: normal;
  color: rgba(255, 255, 255, 0.6);
}

/* The rail runs from the section's left gutter to the frame edge (100 → 1440),
   so at rest it shows exactly what the comp shows — the fourth card clipped at
   1440 — and scrolls the remaining 311px to reveal it. */
.services__rail {
  display: flex;
  align-items: flex-start;
  gap: 11.945px;
  width: 1340px;
  flex-shrink: 0;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scroll-behavior: smooth;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.services__rail::-webkit-scrollbar {
  display: none;
}

/* Trailing gutter so the last card lands on the same 100px margin as the
   first. A margin on the last card rather than padding on the scroller,
   which older Safari drops from the scrollable area. */
.services__rail > :last-child {
  margin-right: var(--page-gutter);
}

.services__rail:focus-visible {
  outline: 2px solid var(--base-background);
  outline-offset: 4px;
}

.services__rail.is-dragging {
  scroll-behavior: auto;
  cursor: grabbing;
}

.services__rail.is-dragging * {
  pointer-events: none;
}

.service-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-24);
  overflow: clip;
  border-radius: 17.917px;
  flex-shrink: 0;
}

.service-card__visual {
  width: 403.877px;
  height: 377.897px;
  overflow: clip;
  border-radius: var(--radius-16);
  background: var(--ww-lilac);
  flex-shrink: 0;
}

/* Sources are 2x, matched on the card's height so each lands at its designed
   width. */
.service-card__visual img {
  width: auto;
  height: 377.897px;
}

.service-card__text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 12.691px;
  width: 403.877px;
  padding: 11.945px;
  color: var(--base-background);
  font-weight: var(--font-weight-medium);
  flex-shrink: 0;
}

.service-card__text h3 {
  width: 100%;
  font-size: var(--font-size-24);
  line-height: var(--line-height-24);
}

.service-card__text p {
  width: 100%;
  font-size: var(--font-size-14);
  line-height: var(--line-height-14);
}

/* ==========================================================================
   5 · Trust trio
   ========================================================================== */

.trust {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-24);
  height: 500px;
  padding: var(--page-gutter);
  background: var(--base-background);
}

.trust__col {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-32);
  width: 396px;
  flex-shrink: 0;
}

/* The three marks have different silhouettes, so they are matched on height
   (64px, down from the comp's 90.82px, client) and left to take their own
   width. */
.trust__col img {
  width: auto;
  height: 64px;
  flex-shrink: 0;
}

.trust__text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-24);
  width: 100%;
  color: var(--base-foreground);
  font-weight: var(--font-weight-medium);
  flex-shrink: 0;
}

.trust__col--narrow .trust__text {
  width: 363px;
}

.trust__text h3 {
  font-size: var(--font-size-24);
  line-height: var(--line-height-24);
  white-space: nowrap;
}

.trust__text p {
  min-width: 100%;
  width: min-content;
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
}

/* ==========================================================================
   6 · Metrics
   ========================================================================== */

/* Intro on top, the three cards side by side below it. */
.metrics {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 56px;
  padding: var(--page-gutter);
  background: var(--ww-metrics-bg);
}

/* Title on the left, the description on the right, sharing a baseline. */
.metrics__intro {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--gap-40);
  width: 100%;
  color: var(--base-foreground);
  font-weight: var(--font-weight-medium);
  flex-shrink: 0;
}

.metrics__intro h2 {
  font-size: var(--font-size-40);
  line-height: var(--ww-title-leading);
  white-space: nowrap;
  flex-shrink: 0;
}

.metrics__intro > p {
  width: 427px;
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
  flex-shrink: 0;
}

.metrics__cards {
  display: flex;
  gap: var(--gap-16);
  width: 100%;
  flex-shrink: 0;
}

.metric {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  height: 246px;
  padding: var(--numbers-32);
  overflow: clip;
  border-radius: var(--radius-24);
  background: var(--base-card);
  flex-shrink: 0;
}

.metric__inner {
  display: flex;
  flex: 1 0 0;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  min-height: 1px;
  width: 100%;
  color: var(--base-foreground);
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
}

/* The features steps' icon tile, in its active lilac. Hidden for now
   (client); delete the display: none below to bring them back. */
.metric__icon {
  display: none;
  /* display: grid; */
  place-items: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  margin-bottom: var(--numbers-24);
  border-radius: var(--radius-12);
  background: var(--ww-lilac-tint);
  color: var(--ww-primary);
}

.metric__icon svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.metric__value {
  font-size: var(--font-size-40);
  line-height: var(--line-height-40);
  flex-shrink: 0;
}

.metric__label {
  font-size: var(--font-size-20);
  line-height: var(--line-height-20);
  flex-shrink: 0;
}

/* ==========================================================================
   7 · Testimonials
   ========================================================================== */

.voices {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 168px;
  padding: var(--page-gutter);
  background: var(--ww-lilac);
}

.voices__inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--numbers-48);
  width: 100%;
  flex-shrink: 0;
}

.voices__inner > h2 {
  width: 596px;
  font-size: var(--font-size-40);
  line-height: var(--ww-title-leading);
  font-weight: var(--font-weight-medium);
  color: var(--base-foreground);
}

/* Three equal columns: 1240 content width, two 24px gutters, so each card is
   397.33px. `stretch` gives them a common height and `space-between` drops
   every attribution onto the same baseline. */
.voices__row {
  display: flex;
  align-items: stretch;
  gap: var(--gap-24);
  width: 100%;
  flex-shrink: 0;
}

.voice {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  min-width: 0;
  /* the card height the comp sets, kept as a floor so short quotes still
     read with the same air beneath them */
  min-height: 302.5px;
  padding: var(--numbers-24);
  border-radius: 17.013px;
  background: var(--base-card);
  font-size: var(--font-size-20);
  line-height: var(--line-height-20);
  color: var(--base-foreground);
}

.voice__quote {
  width: 100%;
  font-weight: var(--font-weight-regular);
  flex-shrink: 0;
}

.voice__name {
  font-weight: var(--font-weight-medium);
  white-space: nowrap;
  flex-shrink: 0;
}

/* ==========================================================================
   8 · FAQ + closing CTA
   ========================================================================== */

.faq {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 150px;
  padding: var(--page-gutter);
  background: var(--base-background);
}

.faq__container {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  width: 100%;
  flex-shrink: 0;
}

/* Heading on the left, the answers in two open columns to the right. */
.faq__head {
  display: flex;
  flex-direction: column;
  gap: var(--gap-16);
  width: 400px;
  font-weight: var(--font-weight-medium);
}

.faq__head h2 {
  font-size: var(--font-size-40);
  line-height: var(--ww-title-leading);
  color: var(--neutral-700);
}

.faq__head > p {
  width: 320px;
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
  color: var(--base-muted-foreground);
}

.faq__list {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--numbers-48);
  width: 744px;
  padding-top: var(--numbers-8); /* first questions sit on the heading's cap line */
}

.faq__item {
  display: flex;
  flex-direction: column;
  gap: var(--gap-8);
}

.faq__item h3 {
  font-size: var(--font-size-20);
  line-height: var(--line-height-20);
  font-weight: var(--font-weight-semibold);
  color: var(--neutral-800);
}

.faq__item p {
  font-size: var(--font-size-16);
  line-height: 26px;
  color: var(--base-muted-foreground);
}

.faq__band {
  position: absolute;
  left: 0;
  bottom: -15.98px; /* off the bottom, so the FAQ above can change height */
  width: var(--canvas-width);
  height: 572px;
  background: var(--ww-ink-950);
}

.cta {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 198px;
  width: 1240px;
  height: 672px;
  padding: 78.376px 66.988px;
  overflow: clip;
  border-radius: 21.436px;
  background: var(--ww-lilac);
  flex-shrink: 0;
}

.cta__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: var(--numbers-48);
  flex-shrink: 0;
}

.cta__copy h2 {
  width: 530px;
  font-size: var(--font-size-48);
  line-height: var(--ww-title-leading);
  font-weight: var(--font-weight-medium);
  color: var(--base-secondary-foreground);
}

.cta__social {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-24);
  flex-shrink: 0;
}

.cta__social > img {
  width: 116px; /* the height of the avatars + text beside it */
  height: 116px;
  object-fit: cover;
  flex-shrink: 0;
  /* the code is black on transparent: a white tile behind it, with a small
     quiet zone so it scans cleanly off the lilac card */
  padding: var(--numbers-8);
  border-radius: var(--radius-8);
  background: var(--ww-white);
}

.cta__download {
  display: none; /* phones and touch screens only — see below */
}

/* As in the hero: anything that can't scan its own screen gets the
   smart-link button in place of the code. */
@media (max-width: 1023px), (hover: none) and (pointer: coarse) {
  .cta__social > img {
    display: none;
  }

  .cta__download {
    display: block;
  }

  /* With the button in the code's place, stack button → avatars → line,
     following the CTA copy's own alignment (centred on phones, left on
     wide touch screens). */
  .cta .cta__social,
  .cta .cta__social-text {
    flex-direction: column;
    align-items: inherit;
    text-align: inherit;
  }

  .cta .cta__social {
    gap: var(--gap-24);
    flex-wrap: nowrap;
  }

  .cta .cta__social-text {
    width: auto;
  }

  .cta .cta__social-text p {
    width: auto; /* one line, not the desktop's min-content wrap */
  }
}

.cta__social-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-12); /* avatars sit just over the text */
  width: 161px;
  flex-shrink: 0;
}

.cta__social-text p {
  min-width: 100%;
  width: min-content;
  font-size: 18.393px;
  line-height: 27.589px;
  font-weight: var(--font-weight-medium);
  color: var(--base-secondary-foreground);
  flex-shrink: 0;
}

/* Centred on the footprint the old bezelled render occupied, so the card's
   composition is unchanged. The card clips it at 602px — the device itself
   must not clip, or the near edge of the hover tilt gets shaved off.
   perspective-origin sits at the middle of the visible 602px, not the full
   876px box, so the tilt reads as pivoting around what you can see. */
.cta__device {
  position: absolute;
  left: 729.5px;
  top: 69.99px;
  width: 412px;
  height: 876px;
  perspective: 1400px;
  perspective-origin: 50% 34%;
}

/* The hero's phone (bezel and all), at the CTA's 412px width — its own 2x
   export (phone-cta.webp, from the same source as phone-hero.webp) so it
   stays sharp at this size; its height follows the image. --tilt is set by initTilt() in main.js from the
   pointer's x-position over the phone. */
.cta__device img {
  width: 412px;
  height: auto;
  transform: rotateY(var(--tilt, 0deg));
  transition: transform 500ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ==========================================================================
   9 · Footer
   ========================================================================== */

.footer {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* The wordmark is bottom-anchored, so this height sets the space beneath
     the copyright line. 630 leaves ~113px before the mark begins. */
  width: 100%; /* .page doesn't stretch its children; every section sets this */
  height: 630px;
  padding: 40px var(--page-gutter) 0;
  overflow: clip;
  background: var(--ww-ink-950);
  flex-shrink: 0;
}

.footer__container {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 56px;
  width: 1240px;
  height: 248.172px;
  flex-shrink: 0;
}

.footer__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  width: 100%;
  flex-shrink: 0;
}

.footer__brand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-16);
  flex-shrink: 0;
}

/* Two lines, sized to break under the logo lockup. */
.footer__motto {
  width: 324px;
  font-size: var(--font-size-16);
  line-height: var(--line-height-16);
  font-weight: var(--font-weight-regular);
  color: rgba(255, 255, 255, 0.6);
  flex-shrink: 0;
}

.footer__logo {
  display: flex;
  align-items: center;
  gap: var(--gap-12);
  flex-shrink: 0;
}

.footer__logo img {
  width: 31.806px;
  height: 32px;
  object-fit: cover;
}

.footer__logo span {
  font-size: var(--font-size-24);
  line-height: var(--line-height-24);
  font-weight: var(--font-weight-semibold);
  color: var(--base-background);
  white-space: nowrap;
}

/* Live accounts, taken from walletwise.ng. */
.footer__social {
  display: flex;
  align-items: center;
  gap: var(--gap-16);
  margin-top: var(--gap-8);
  flex-shrink: 0;
}

.footer__social-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  color: rgba(255, 255, 255, 0.55);
  transition: color 200ms ease-out;
}

.footer__social-link:hover,
.footer__social-link:focus-visible {
  color: #fff;
}

.footer__social-link:focus-visible {
  outline: 2px solid var(--base-background);
  outline-offset: 4px;
  border-radius: var(--radius-4);
}

.footer__social-link svg {
  width: 20px;
  height: 20px;
  display: block;
}

/* Back to top: a pill after the Connect column, outlined like the rest of
   the dark footer's furniture. A plain #home link, so it works without JS;
   html's smooth scrolling carries it up. */
.footer__totop {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-8);
  padding: var(--numbers-10) var(--numbers-16);
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.24);
  font-size: var(--font-size-14);
  line-height: var(--line-height-14);
  font-weight: var(--font-weight-medium);
  color: var(--ww-white);
  white-space: nowrap;
  transition: background-color 150ms ease;
}

.footer__totop svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: translate 200ms cubic-bezier(0.2, 0, 0, 1);
}

.footer__totop:hover {
  background: rgba(255, 255, 255, 0.08);
}

.footer__totop:hover svg {
  translate: 0 -2px;
}

.footer__totop:focus-visible {
  outline: 2px solid var(--ww-white);
  outline-offset: 3px;
}

.footer__nav {
  display: flex;
  align-items: flex-start;
  gap: 96px;
  flex-shrink: 0;
}

.footer__col {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-16);
  flex-shrink: 0;
}

.footer__col--company { width: 124.531px; }
.footer__col--support { width: 88.641px; }
.footer__col--connect { width: 160px; }

.footer__label {
  position: relative;
  width: 100%;
  height: 22.4px;
  flex-shrink: 0;
}

/* Same size as the links beneath; the muted colour still marks it as a label. */
.footer__label p {
  position: absolute;
  left: 0;
  top: -0.5px;
  font-size: 14px;
  line-height: 22.4px;
  font-weight: var(--font-weight-medium);
  color: var(--base-muted-foreground);
  white-space: nowrap;
}

.footer__list {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 18px;
  width: 100%;
  flex-shrink: 0;
  font-size: 14px;
  line-height: 22.4px;
  font-weight: var(--font-weight-regular);
  color: var(--base-background);
  white-space: nowrap;
}

.footer__list-item {
  position: relative;
  height: 25.594px;
  flex-shrink: 0;
  font-size: 14px;
  line-height: 22.4px;
  font-weight: var(--font-weight-regular);
  color: var(--base-background);
  white-space: nowrap;
}

.footer__list-item a,
.footer__list-item p {
  position: absolute;
  left: 0;
  top: 2.5px;
  white-space: nowrap;
  transition: opacity 150ms ease-in-out;
}

.footer__col a:hover {
  opacity: 0.7;
}

.footer__col--support .footer__list-item { width: 88.641px; }
.footer__col--connect .footer__list-item { width: 100%; }

.footer__legal {
  position: relative;
  width: 100%;
  height: 45.797px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  flex-shrink: 0;
}

.footer__legal-copy {
  position: absolute;
  left: 0;
  top: 24px;
  width: 237.555px;
  height: 20.797px;
}

.footer__legal-copy p {
  position: absolute;
  left: 0;
  top: -0.5px;
  font-size: 13px;
  line-height: 20.8px;
  color: #888;
  white-space: nowrap;
}

.footer__legal-note {
  position: absolute;
  left: 845.96px;
  top: 24.8px;
  width: 274.039px;
  height: 19.195px;
  opacity: 0.45;
}

.footer__legal-note p {
  position: absolute;
  left: 0;
  top: -0.5px;
  font-size: 12px;
  line-height: 19.2px;
  color: #888;
  white-space: nowrap;
}

/* Inlined rather than shipped as a flat <img> so its fill follows the
   element's `color`. At rest it is the comp's white-at-6% ghost; on hover it
   warms into the brand purple. */
.footer__wordmark {
  position: absolute;
  bottom: 0.09px;
  left: calc(50% - 2.64px);
  transform: translateX(-50%);
  width: 1442.197px;
  height: 197.922px;
  color: #fff;
  opacity: 0.06;
  transition: color 450ms ease-out, opacity 450ms ease-out;
}

.footer__wordmark:hover {
  color: var(--ww-primary);
  opacity: 0.24;
}

/* ==========================================================================
   Reduced motion
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* Content still appears, it just does not travel. */
  .js [data-reveal] {
    transform: none;
  }
}

/* ==========================================================================
   Responsive
   The page above is a fixed 1440 canvas that scales to fit down to 1024px.
   Below that it stops scaling and reflows: single column, fluid widths, and
   every `nowrap` released so text can break.
   ========================================================================== */

@media (max-width: 1023px) {
  :root {
    --page-gutter: 24px;
  }

  .page {
    width: 100%;
    zoom: 1;
  }

  /* ---- Hero ------------------------------------------------------------ */
  .hero {
    width: 100%;
    height: auto;
    gap: 40px;
    padding-bottom: 56px;
  }

  :root {
    --nav-height: 76px; /* the phone bar: 16px padding + its 44px row */
    --nav-scroll-pad: 80px;
  }

  .hero {
    padding-top: calc(var(--nav-height) + 40px); /* + the phone hero's gap */
  }

  .nav {
    width: 100%;
    padding: 16px var(--page-gutter);
  }

  .nav-bar.is-scrolled .nav {
    padding-block: var(--numbers-12);
  }

  /* Under the frosted bar the menu goes solid ink — glass read too thin for
     a menu over busy content (client). */
  .nav-bar.is-scrolled .nav__menu {
    background: var(--ww-ink-900);
  }

  /* On phones the bar stays put rather than tucking away on scroll down. */
  .nav-bar.is-tucked {
    transform: none;
  }

  .nav__toggle {
    display: flex;
  }

  .nav__right {
    gap: var(--gap-8);
  }

  /* Logo + pill + burger overrun a phone bar; the hero's own CTA sits just
     below, so the bar keeps the mark and the menu. */
  .nav > .nav__right > .btn-roll {
    display: none;
  }

  /* The links drop out of the bar into a panel under it. */
  .nav__menu {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 30;
    display: none;
    padding: var(--gap-8) var(--page-gutter) var(--numbers-24);
    /* picks the hero gradient up where the bar leaves it, so the panel reads
       as an extension of the nav rather than a separate dark sheet */
    background: linear-gradient(180deg, #431d9b 0%, #2d126a 100%);
    box-shadow: 0 18px 40px rgba(0, 0, 0, 0.28);
  }

  .nav__menu.is-open {
    display: block;
  }

  .nav__list {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }

  .nav__link {
    justify-content: flex-start;
    padding: 11px 0;
    font-size: var(--font-size-16);
    line-height: var(--line-height-16);
    border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  }

  .nav__list > li:nth-last-child(2) .nav__link {
    border-bottom: 0;
  }

  .nav__link--caret {
    padding: 11px 0;
  }

  .nav__link--caret::after {
    display: none;
  }

  /* the CTA closes the panel, set apart from the links above it */
  .nav__list-cta {
    display: block;
    margin-top: var(--numbers-24);
  }

  .nav__list-cta .btn-download {
    display: block;
  }

  .hero__row {
    flex-direction: column;
    align-items: center;
    gap: 40px;
    height: auto;
    padding: 0 var(--page-gutter);
  }

  /* headline, copy and CTA all centre on a phone */
  .hero__copy {
    align-items: center;
    text-align: center;
    width: 100%;
    padding-bottom: 0;
    gap: var(--gap-32);
  }

  .hero__text {
    align-items: center;
    text-align: center;
  }

  .hero__text h1 {
    min-width: 0;
    width: 100%;
    font-size: clamp(34px, 9vw, 56px);
    line-height: 1.16;
  }

  .hero__qr {
    display: none;
  }

  .hero__download {
    display: block;
  }

  .hero__text p {
    width: 100%;
    font-size: var(--font-size-16);
    line-height: 26px;
  }

  /* Same composition, scaled to the column. */
  .hero__device {
    width: 100%;
    max-width: 420px;
    margin-inline: auto;
    border-radius: 24px;
  }

  /* ---- Partners ---------------------------------------------------------- */
  .partners {
    gap: var(--gap-16);
    padding: var(--numbers-32) var(--page-gutter);
  }

  .partners__row {
    justify-content: center;
    gap: var(--numbers-24) var(--numbers-48);
  }

  /* Each logo's size is on its HTML attributes, so scale them all by one
     factor here rather than restating six widths. */
  .partners__row img {
    zoom: 0.7;
  }

  /* ---- Features -------------------------------------------------------- */
  /* The steps become a tap-to-open list under the card. */
  .features__pin {
    display: block;
  }

  .features__frame {
    width: 100%;
    gap: var(--gap-32);
    padding: 56px var(--page-gutter);
  }

  .features__intro {
    align-items: flex-start;
    width: 100%;
    gap: var(--gap-12);
    text-align: left;
  }

  .features__head {
    font-size: clamp(26px, 6.4vw, 40px);
    line-height: var(--ww-title-leading);
  }

  .features__lede {
    font-size: var(--font-size-16);
    line-height: var(--line-height-16);
  }

  .features__row {
    flex-direction: column;
    gap: var(--gap-32);
  }

  .features__list {
    width: 100%;
    gap: var(--gap-32);
  }

  .features__step {
    white-space: normal;
    font-size: var(--font-size-16);
    line-height: var(--line-height-16);
  }

  .features__icon {
    width: 36px;
    height: 36px;
  }

  .features__item.is-active .features__step {
    font-size: var(--font-size-20);
    line-height: var(--line-height-20);
  }

  .features__desc > div > p {
    width: 100%;
    padding-top: var(--gap-12);
    font-size: var(--font-size-16);
    line-height: var(--line-height-16);
  }

  /* The card, filling the column (up to its designed 508px): above the
     list without JS, inside the open step under its copy with it
     (initFeatures moves it). */
  .features__visuals {
    order: -1;
    width: 100%;
    max-width: 508px;
    margin-inline: auto;
  }

  .features__desc .features__visuals {
    margin-top: var(--gap-16);
  }

  /* ---- Services -------------------------------------------------------- */
  .services {
    height: auto;
    gap: var(--gap-32);
    padding: 56px var(--page-gutter);
  }

  .services__head {
    width: 100%;
    font-size: clamp(26px, 6.4vw, 40px);
    line-height: var(--ww-title-leading);
  }

  .services__head br {
    display: none;
  }

  /* still bleeds off the right edge, as designed */
  .services__rail {
    width: calc(100% + var(--page-gutter));
  }

  .service-card__visual {
    width: 280px;
    height: 262px;
  }

  .service-card__visual img {
    width: 280px;
    height: auto;
  }

  .service-card__text {
    width: 280px;
  }

  .service-card__text h3 {
    font-size: var(--font-size-20);
  }

  /* ---- Trust ----------------------------------------------------------- */
  .trust {
    flex-direction: column;
    align-items: flex-start;
    height: auto;
    gap: var(--gap-40);
    padding: 56px var(--page-gutter);
  }

  .trust__col,
  .trust__col--narrow .trust__text {
    width: 100%;
  }

  .trust__text h3 {
    white-space: normal;
  }

  /* ---- Metrics --------------------------------------------------------- */
  .metrics {
    flex-direction: column;
    gap: var(--gap-32);
    padding: 56px var(--page-gutter);
  }

  .metrics__intro {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--gap-16);
  }

  .metrics__intro h2 {
    white-space: normal;
    font-size: clamp(26px, 6.4vw, 40px);
    line-height: var(--ww-title-leading);
  }

  .metrics__intro > p {
    width: 100%;
  }

  /* Back to a stack: three across is too tight for the labels. */
  .metrics__cards {
    flex-direction: column;
  }

  .metric {
    flex: 0 0 auto; /* 1 1 0 collapses once the row is a column */
    height: auto;
    min-height: 160px;
    padding: var(--numbers-24);
  }

  .metric__inner {
    flex: 1 0 auto; /* a 0 basis lets the icon push the label out of the card */
    white-space: normal;
    gap: var(--gap-16);
  }

  .metric__icon {
    width: 36px;
    height: 36px;
    margin-bottom: var(--numbers-16);
  }

  .metric__value {
    font-size: var(--font-size-32);
    line-height: var(--line-height-32);
  }

  /* ---- Testimonials ---------------------------------------------------- */
  .voices {
    gap: var(--gap-40);
    padding: 56px var(--page-gutter);
  }

  .voices__inner > h2 {
    width: 100%;
    font-size: clamp(26px, 6.4vw, 40px);
    line-height: var(--ww-title-leading);
  }

  .voices__row {
    flex-direction: column;
  }

  .voice {
    /* `flex: 1 1 0` is a row value; in a column it collapses the card to
       nothing and the quote spills out. */
    flex: 0 0 auto;
    min-height: 0;
    gap: var(--gap-24);
    font-size: var(--font-size-16);
    line-height: var(--line-height-16);
  }

  .voice__quote {
    min-width: 0;
    width: 100%;
  }

  /* ---- FAQ + CTA ------------------------------------------------------- */
  .faq {
    gap: 56px;
    padding: 56px var(--page-gutter);
  }

  .faq__container {
    flex-direction: column;
    gap: var(--gap-40);
  }

  .faq__head,
  .faq__head > p {
    width: 100%;
  }

  .faq__head h2 {
    font-size: clamp(26px, 6.4vw, 40px);
    line-height: var(--ww-title-leading);
  }

  .faq__list {
    grid-template-columns: 1fr;
    gap: var(--gap-32);
    width: 100%;
    padding-top: 0;
  }

  .faq__item h3 {
    font-size: var(--font-size-16);
    line-height: var(--line-height-16);
  }

  /* the dark band is placed off a desktop-only offset */
  .faq__band {
    display: none;
  }

  .cta {
    flex-direction: column;
    align-items: center;
    gap: var(--gap-40);
    width: 100%;
    height: auto;
    padding: var(--numbers-32) var(--numbers-24) 0; /* the phone runs off the bottom */
  }

  .cta__copy {
    align-items: center;
    text-align: center;
    width: 100%;
    gap: var(--gap-32);
  }


  .cta__copy h2 {
    width: 100%;
    font-size: clamp(26px, 7vw, 48px);
    line-height: var(--ww-title-leading);
  }

  /* the QR + count row keeps desktop fixed widths otherwise, and overruns */
  /* QR + avatars/text as one centred group; the pair sits side by side
     from ~375px and wraps under the code below that. */
  .cta__social {
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--gap-12);
    width: 100%;
  }

  .cta__social > img {
    width: 116px; /* the avatars + two lines of text beside it */
    height: 116px;
  }

  /* Avatars and text left-aligned under each other, as on desktop. */
  .cta__social-text {
    flex: 0 0 auto;
    width: 150px;
    align-items: flex-start;
    gap: var(--gap-12);
    text-align: left;
  }

  .cta__social-text p {
    min-width: 0;
    width: 100%;
    font-size: var(--font-size-16);
    line-height: var(--line-height-24);
  }

  /* Cut where the desktop card cuts it — just under the Services row's
     Airtime / Data / Betting / Cable — and flush with the card's bottom. */
  .cta__device {
    position: static;
    width: 100%;
    max-width: 300px;
    height: auto;
    aspect-ratio: 412 / 602;
    margin-inline: auto;
    overflow: clip;
  }

  .cta__device img {
    display: block;
    width: 100%;
    height: auto;
  }

  /* ---- Footer ---------------------------------------------------------- */
  .footer {
    height: auto;
    gap: var(--gap-40);
    padding: 40px var(--page-gutter) 0;
  }

  .footer__container,
  .footer__motto {
    width: 100%;
  }

  .footer__container {
    height: auto;
    gap: var(--gap-32);
  }

  .footer__top {
    flex-direction: column;
    gap: var(--gap-40);
  }

  .footer__nav {
    flex-wrap: wrap;
    gap: var(--gap-32);
  }

  /* its own row under the columns, rather than wedged beside Connect */
  .footer__col--totop {
    flex-basis: 100%;
  }

  /* the legal row is positioned absolutely on desktop; stack it here */
  .footer__legal {
    display: flex;
    flex-direction: column;
    gap: var(--gap-8);
    height: auto;
    padding-top: var(--numbers-24);
  }

  .footer__legal-copy,
  .footer__legal-note {
    position: static;
    width: 100%;
    height: auto;
    opacity: 1;
  }

  .footer__legal-copy p,
  .footer__legal-note p {
    position: static;
    white-space: normal;
  }

  .footer__legal-note p {
    color: #6f6f7a;
  }

  .footer__wordmark {
    position: relative;
    bottom: auto;
    left: auto;
    transform: none;
    width: 100%;
    height: auto;
    margin-top: var(--gap-32);
  }
}

@media (max-width: 640px) {
  .services__rail {
    padding-bottom: var(--gap-8);
  }

  .footer__nav {
    gap: var(--gap-24);
  }
}

/* ==========================================================================
   Wide screens
   The canvas is a fixed 1440 and sits centred, so past that width the body
   colour shows down both sides. Each section's fill is carried out to the
   viewport edges instead.

   Solid sections use a pair of box-shadows offset by exactly the canvas
   width: a shadow is painted by the element itself, so — unlike a
   pseudo-element — it is not clipped by the section's own `overflow: clip`,
   and offsetting rather than spreading keeps it from bleeding vertically over
   its neighbours. The hero carries a gradient, which a shadow cannot
   reproduce, so it is lifted onto a full-width band behind the canvas and the
   section itself goes transparent — one gradient across the whole width, so
   there is no seam at the canvas edge.
   ========================================================================== */

@media (min-width: 1441px) {
  /* Sized off body rather than 100vw: vw includes the scrollbar, so a 100vw
     band is wider than the viewport and pushes out a horizontal scrollbar. */
  body {
    position: relative;
  }

  body::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: var(--hero-height);
    background-image: var(--ww-hero-glow), var(--ww-hero-gradient);
    background-size: 70% 110%, 60% 95%, 90% 60%, 100% 100%;
    background-repeat: no-repeat;
    background-position: 0% 0%, 100% 15%, 15% 100%, 0 0;
    animation: hero-drift 20s ease-in-out infinite alternate;
  }

  /* every section now paints its own full-width fill, so the canvas colour
     underneath would only cover the gradient band */
  .page {
    background: transparent;
  }

  .hero {
    background-image: none;
    animation: none;
  }

  .partners {
    box-shadow: calc(-1 * var(--canvas-width)) 0 0 var(--base-muted),
      var(--canvas-width) 0 0 var(--base-muted);
  }

  .trust,
  .faq {
    box-shadow: calc(-1 * var(--canvas-width)) 0 0 var(--base-background),
      var(--canvas-width) 0 0 var(--base-background);
  }

  .features {
    box-shadow: calc(-1 * var(--canvas-width)) 0 0 var(--base-background),
      var(--canvas-width) 0 0 var(--base-background);
  }

  .services {
    box-shadow: calc(-1 * var(--canvas-width)) 0 0 var(--ww-ink-900),
      var(--canvas-width) 0 0 var(--ww-ink-900);
  }

  .metrics {
    box-shadow: calc(-1 * var(--canvas-width)) 0 0 var(--ww-metrics-bg),
      var(--canvas-width) 0 0 var(--ww-metrics-bg);
  }

  .voices {
    box-shadow: calc(-1 * var(--canvas-width)) 0 0 var(--ww-lilac),
      var(--canvas-width) 0 0 var(--ww-lilac);
  }

  .footer {
    box-shadow: calc(-1 * var(--canvas-width)) 0 0 var(--ww-ink-950),
      var(--canvas-width) 0 0 var(--ww-ink-950);
  }

  /* The dark strip behind the closing CTA has to reach the edges too. It
     overhangs the section by ~16px, which `overflow: clip` was trimming —
     the footer beneath is the same colour, so releasing it is invisible. */
  .faq {
    overflow: visible;
  }

  .faq__band {
    box-shadow: calc(-1 * var(--canvas-width)) 0 0 var(--ww-ink-950),
      var(--canvas-width) 0 0 var(--ww-ink-950);
  }
}
