/* ============================================================
   Lexend - self-hosted latin subset, variable weight (39KB).
   Same register as Vitals' Lexend Deca. Self-hosted rather than
   linked so there is no third-party request on the critical path,
   and font-display:swap so text paints immediately.
   ============================================================ */

@font-face {
  font-family: "Lexend";
  src: url("/assets/fonts/lexend-latin-var.woff2") format("woff2-variations"),
       url("/assets/fonts/lexend-latin-var.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* ============================================================
   scarcity.ai - design system

   Minimal by discipline:
     · ONE accent. Everything else is ink, grey, or white.
     · A large spacing scale - sections breathe at 120px.
     · Four type sizes, not nine.
     · Mono reserved for figures. Nowhere else.
     · No per-app colours, no pills, no tinted plates.

   The products are counters, so numbers are the only thing that
   ever gets colour. That is the whole visual idea.
   ============================================================ */

:root {
  --bg:      #FFF8F5;
  /* warm tint, as Loox uses #FFF6F4 */
  --card:    #FFFFFF;
  --sunk:    #FDF0EA;
  --ink:     #16181A;
  --ink-2:   #5A6165;
  --mute:    #646B6E;
  /* 5.15:1 on white - safe for real text too */
  --line:    #F0E2DA;
  --accent:      #E8410F;
  /* fill */
  --accent-ink:  #1A0A04;
  /* label on that fill - 4.77:1 */
  --accent-text: #C4340A;
  /* accent as text on light - 5.46:1 */

  --shadow: 0 1px 2px rgba(22,24,26,.03), 0 12px 32px -18px rgba(22,24,26,.18);
  --sans: "Lexend", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --gap: 120px;
  --navh: 107px;
}

/* Light only, deliberately.
   Checked 3 Aug 2026: Vitals, Essential Apps, Upsell.com and Hextom all serve
   the same light background regardless of the OS setting. A marketing site's
   mockup imitates a Shopify storefront, and merchants see those in light - so
   following the OS meant half of visitors saw a version nobody had reviewed.

   The dark tokens are kept below under :root[data-theme="dark"], so a manual
   toggle is one attribute away if it is ever wanted. This block is what made
   it automatic, and it is intentionally disabled: */
/* @media (prefers-color-scheme: dark) { */
/*   :root { */
/*     --bg:#101314; --card:#181C1E; --sunk:#1F2426; --ink:#ECEEEC; --ink-2:#A9B0B3; */
/*     --mute:#7C8386; --line:#272D2F; */
/*     --accent:#FF6236; --accent-ink:#14181A; --accent-text:#FF7A52; */
/*     --shadow: 0 1px 2px rgba(0,0,0,.4), 0 14px 36px -20px rgba(0,0,0,.8); */
/*   } */
/* } */
:root[data-theme="dark"] {
  --bg:#101314; --card:#181C1E; --sunk:#1F2426; --ink:#ECEEEC; --ink-2:#A9B0B3;
  --mute:#7C8386; --line:#272D2F;
  --accent:#FF6236; --accent-ink:#14181A; --accent-text:#FF7A52;
  --shadow: 0 1px 2px rgba(0,0,0,.4), 0 14px 36px -20px rgba(0,0,0,.8);
}
:root[data-theme="light"] {
  --bg:#FFF8F5; --card:#FFFFFF; --sunk:#FDF0EA; --ink:#16181A; --ink-2:#5A6165;
  --mute:#646B6E; --line:#F0E2DA;
  --accent:#E8410F; --accent-ink:#1A0A04; --accent-text:#C4340A;
  --shadow: 0 1px 2px rgba(22,24,26,.03), 0 12px 32px -18px rgba(22,24,26,.18);
}

@media (max-width: 760px) { :root { --gap: 76px; } }

* { box-sizing: border-box; }

/* iOS Safari's rubber-band overscroll pulls the whole page past its own
   top/bottom edge and exposes whatever sits behind it - here that was the
   plain --bg cream, which reads fine above the light hero but shows as a
   jarring pale band over the dark footer when someone drags past the
   bottom. Turning off the bounce keeps the page's own colors in frame at
   both ends instead of matching them, since the top and bottom tiers are
   different colors and can't both be "fixed" with one background. */
html { overscroll-behavior-y: none; }

body {
  margin: 0; background: var(--bg); color: var(--ink);
  font-family: var(--sans); font-size: 17px; line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior-y: none;
}
a { color: inherit; }
img { max-width: 100%; display: block; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

/* ---------- four type sizes ---------- */

h1 {
  font-size: clamp(44px, 6.6vw, 86px); line-height: .98; letter-spacing: -.045em;
  font-weight: 800; margin: 0 0 22px; text-wrap: balance; text-transform: uppercase;
}
h2 {
  font-size: clamp(25px, 2.9vw, 33px); line-height: 1.18; letter-spacing: -.032em;
  font-weight: 600; margin: 0 0 16px; text-wrap: balance;
}
h3 {
  font-size: 21px; line-height: 1.25; letter-spacing: -.02em;
  font-weight: 600; margin: 0 0 10px;
}
p { margin: 0 0 20px; }
.lede { font-size: 20px; line-height: 1.55; color: var(--ink-2); max-width: 46ch; }
.body { color: var(--ink-2); max-width: 56ch; }
.fig { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.num { color: var(--accent-text); font-family: var(--mono); font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }

/* ---------- layout ---------- */

.wrap { max-width: 1080px; margin: 0 auto; padding: 0 32px; }
section { padding: calc(var(--gap) * .82) 0; }
section.first { padding-top: 72px; }
section.last { padding-bottom: var(--gap); }
.rule { border: 0; border-top: 1px solid var(--line); margin: 0; }

/* ---------- nav ---------- */

/* padding-block, not the padding shorthand: .nav also carries .wrap, and a
   shorthand here silently zeroed the 32px side padding that keeps the brand and
   the Install button off the edge of the screen. */
.nav { display: flex; align-items: center; gap: 32px; padding-block: 26px; flex-wrap: wrap; }
.brand {
  display: flex; align-items: center; gap: 11px; text-decoration: none;
  font-size: 19px; font-weight: 600; letter-spacing: -.03em;
}
.brand .mark {
  width: 30px; height: 30px; flex: 0 0 30px; border-radius: 9px; overflow: hidden;
  background: var(--sunk); display: grid; place-items: center;
  font-size: 12px; font-weight: 700; color: var(--ink-2);
}
.brand .mark img { width: 100%; height: 100%; object-fit: cover; }
/* margin-left:auto used to sit on the Install button, which is gone from the
   nav: five link labels plus a button did not fit 1080px, so the button wrapped
   to a second row and the header stood 168px tall. The links take the right
   edge now and the header is one row again. */
.nav .links { display: flex; align-items: center; gap: 26px; font-size: 16px; margin-left: auto; }
.nav .links a { text-decoration: none; color: var(--ink-2); }
.nav .links a:hover, .nav .links a[aria-current="page"] { color: var(--ink); }
.nav .right { margin-left: auto; }

/* ---------- buttons ---------- */

.btn {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 16px; font-weight: 600; letter-spacing: -.01em;
  padding: 13px 24px; border-radius: 10px; text-decoration: none;
  border: 1px solid transparent;
}
.btn-fill { background: var(--accent); color: #fff; }
.btn-fill:hover { filter: brightness(1.06); }
.btn-line { border-color: var(--line); background: var(--card); }
.btn-line:hover { background: var(--sunk); }

/* one spacing utility, so templates never need a style attribute for it -
   inline styles are what forced style-src to allow 'unsafe-inline' */
.push { margin-top: 44px; }

/* ---------- press feedback ----------
   Nothing on this page acknowledged a pointer-down: hover changed colour, then
   the next thing that happened was a page navigation. A press that answers on
   the way down is what makes a control feel connected rather than reported. */

.btn, .appcard, .tab, .pb-link, .go, .integ {
  transition: transform .12s ease-out, filter .16s ease,
              background-color .18s ease, border-color .18s ease, box-shadow .18s ease;
}
.btn:active, .pb-link:active, .go:active { transform: scale(.975); }
.appcard:active, .tab:active { transform: scale(.99); }
/* the pointer is already committed on the way down, so the press must not wait
   for the browser's own active delay to look different */
.btn:active { filter: brightness(.97); }
.btnrow { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }

/* ---------- CTA labels never wrap ----------
   A button that breaks across two lines stops reading as a button. Measured in
   Lexend at 600 weight, "See Scarcity Timer on the App Store" needs 262px even
   at 13.5px, and a 320px phone leaves only 213px inside the pill - it cannot
   fit at any size worth reading. So the long label is swapped for a short one
   below 420px rather than shrunk into illegibility. Both live in the markup and
   exactly one is display:none, which also keeps it out of the accessibility
   tree, so nothing is announced twice.

   The app name is never actually lost: every one of these buttons sits directly
   under a kicker and heading that name the app. */
.cta-sm { display: none; }
.btn, .go, .pb-link, .minicta, .mini-cta, .window .cta,
.rev-links a, .badge-plus, .co-pay { white-space: nowrap; }

@media (max-width: 420px) {
  .cta-lg { display: none; }
  .cta-sm { display: inline; }
  /* 24px of side padding is 48px of a 162px-wide card. "Ask Perplexity →" is
     126px of text at 15px, so the padding - not the label - was what pushed the
     button past its container once wrapping was off. Trimming the padding keeps
     the type readable; shrinking to fit would have meant 13px. */
  .btn { padding-inline: 15px; font-size: 14.5px; }
}
.note { font-size: 14.5px; color: var(--ink-2); margin: 20px 0 0; }

/* ---------- hero ---------- */

.hero {
  text-align: center;
  position: relative;
  /* order matters: the shorthand has to come before the longhand that survives
     it, or the merge silently changes the value */
  padding: 34px 0 8px;
}
.hero h1 { max-width: 15ch; margin-inline: auto; margin-bottom: 18px; }
.hero .lede { margin-inline: auto; text-align: center; max-width: 50ch; font-size: 21px; }
.green-grad {
  background: linear-gradient(135deg, #00c853, #00e676, #69f0ae);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.hero .btnrow { justify-content: center; margin-top: 30px; }

/* ---------- product mockup ----------
   One frame, large and quiet. Only the widget carries colour. */

.shot { margin: 68px auto 0; max-width: 940px; }
.window {
  text-align: left;
  background: var(--card); border: 1px solid var(--line); border-radius: 14px;
  overflow: hidden; box-shadow: var(--shadow);
}
.window .bar-top {
  display: flex; align-items: center; gap: 7px;
  padding: 13px 18px; border-bottom: 1px solid var(--line);
}
.window .bar-top i { width: 9px; height: 9px; border-radius: 50%; background: var(--line); }
.window .bar-top .url {
  margin-left: 14px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--mute);
  transition: opacity .3s ease;
}

.pdp { display: grid; grid-template-columns: 1.1fr 1fr; gap: 44px; padding: 44px; align-items: center; }
@media (max-width: 760px) { .pdp { grid-template-columns: 1fr; gap: 28px; padding: 26px; } }

.pdp .photo {
  aspect-ratio: 1; border-radius: 10px; border: 1px solid var(--line);
  background:
    radial-gradient(80% 60% at 30% 25%, rgba(255,255,255,.9), transparent 70%),
    linear-gradient(155deg, var(--sunk), var(--bg));
}
.pdp .detail { display: flex; flex-direction: column; gap: 16px; }
.pdp .name { font-size: 22px; font-weight: 600; letter-spacing: -.02em; }
.pdp .tag { font-family: var(--mono); font-size: 17px; color: var(--ink-2); }
.pdp .line { height: 8px; border-radius: 4px; background: var(--sunk); }
.pdp .line.short { width: 42%; }
/* Outline, not a solid slab. Filled black, the Add to cart button was the
   heaviest thing in the frame and pulled the eye straight past the widget the
   step is actually about. Outlined, it reads as a real button but sits behind
   the widget, and it takes the live app's colour so the frame belongs to
   whichever app is on screen. */
.window .cta {
  margin-top: 4px;
  padding: 13px;
  border-radius: 10px;
  text-align: center;
  background: transparent;
  color: var(--app-accent, var(--ink));
  border: 1.5px solid currentColor;
  font-weight: 600;
  font-size: 16px;
  transition: transform .16s ease, filter .16s ease;
}

/* the widget - the only coloured thing inside the frame */
.widget {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 0;
  border-top: 1px solid var(--line);
  font-size: 15.5px;
  color: var(--ink-2);
  transition: opacity .4s ease, transform .4s ease, border-color .4s ease, padding .4s ease;
  flex-wrap: wrap;
}
.widget:first-of-type { border-top: 0; }
/* A flex row turns every text run into its own item, so on a narrow mock
   "Only 4 left in stock" broke after the number. The row only needs flex for
   the dot and the bar; below the mock's comfortable width, let it be text. */
@media (max-width: 620px) {
  .widget { display: block; }
  .widget .dot, .widget .eye { display: inline-block; margin-right: 10px; vertical-align: middle; }
  .widget .bar { margin-top: 8px; }
}
.widget .num { font-size: 17px; }
.widget .dot {
  width: 10px; height: 10px; border-radius: 50%; flex: 0 0 10px;
  background: #ff0000;
  box-shadow: 0 0 0 0 #ff0000;
  /* Pulse-red's box-shadow ring spreads to 8px at its peak (see keyframes
     below). The flex row's own 10px gap already clears that, so the dot
     needs no margin of its own here - it previously carried a negative
     margin that ate into the gap and let the ring bleed into "Only". */
  align-self: center; margin-right: 0;
}
/* Below 620px .widget switches to display:block (see above) and loses that
   flex gap, so the dot needs an explicit margin again. This has to come
   after the unconditional rule above - same specificity, so whichever is
   later in the file wins regardless of the media query - or the narrow-width
   value silently loses, which is what let the ring bleed into the text. */
@media (max-width: 620px) {
  .widget .dot { margin-right: 10px; }
}

/* The visitor count leads with an eye rather than a bare dot - it says what the
   number is before the sentence does. It carries the pulse, so the live reading
   is the thing that moves. */
.widget .eye {
  display: inline-flex; flex: 0 0 auto;
  color: var(--app-accent, var(--accent));
}
.widget .eye svg { width: 17px; height: 17px; display: block; }

/* Deliberately not gated on prefers-reduced-motion: a live counter that never
   moves is indistinguishable from a static label, and "it is live" is the whole
   claim these two widgets make. Same call as the deck and the marquee. */
.widget .dot { animation: pulse-red 2s infinite; }
.widget .eye { animation: blip 2.4s ease-in-out infinite; }

@keyframes blip { 0%,100% { opacity: 1 } 50% { opacity: .3 } }

@keyframes pulse-red {
  0%   { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7); }
  70%  { transform: scale(1);    box-shadow: 0 0 0 8px rgba(255, 0, 0, 0); }
  100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); }
}

.hero-eye {
  display: inline-flex; vertical-align: middle; margin-right: 8px; margin-bottom: 10px;
  color: var(--accent); animation: blip 2.4s ease-in-out infinite;
}
.hero-eye svg { width: 56px; height: 56px; display: block; }

.hero-dot {
  display: inline-block; width: 36px; height: 36px; border-radius: 50%;
  background: var(--accent); vertical-align: middle; margin-right: 8px; margin-bottom: 6px;
  box-shadow: 0 0 0 0 var(--accent);
  transform: scale(1); animation: pulse-hero 2s infinite;
}
@keyframes pulse-hero {
  0%   { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(232, 65, 15, 0.7); }
  70%  { transform: scale(1);    box-shadow: 0 0 0 10px rgba(232, 65, 15, 0); }
  100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(232, 65, 15, 0); }
}

.caption {
  margin: 22px auto 0; text-align: center; font-size: 14.5px; color: var(--ink-2); max-width: 54ch;
}

/* ---------- app rows ---------- */

.apps { display: flex; flex-direction: column; gap: 68px; }
.app { display: grid; grid-template-columns: 46px 1fr auto; gap: 24px; align-items: start; }
@media (max-width: 700px) { .app { grid-template-columns: 40px 1fr; } .app .go { grid-column: 2; } }
.app .icon {
  width: 46px; height: 46px; border-radius: 12px; overflow: hidden; background: var(--sunk);
  display: grid; place-items: center; font-size: 17px; font-weight: 700; color: var(--ink-2);
}
.app .icon img { width: 100%; height: 100%; object-fit: cover; }
.app .rating { font-family: var(--mono); font-size: 13.5px; color: var(--ink-2); margin-top: 6px; }
.app .go { text-decoration: none; font-weight: 600; color: var(--accent-text); white-space: nowrap; padding-top: 2px; }
.app .go:hover { text-decoration: underline; }

/* ---------- guides ---------- */

.guides { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 20px; }
.guide {
  background: var(--card); border: 1px solid var(--line); border-radius: 14px;
  padding: 26px; text-decoration: none; display: flex; flex-direction: column; gap: 12px;
}
.guide:hover { border-color: var(--ink-2); }
.guide h3 { margin: 0; font-size: 19px; }
.guide .meta { font-size: 14px; color: var(--ink-2); margin-top: auto; }

/* ---------- close ---------- */

.close { text-align: left; }
.close h2 { max-width: 24ch; margin-inline: auto; }
.close .btnrow { justify-content: center; }

/* ---------- footer ---------- */

footer {
  padding: 52px 0 64px; border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: 22px 48px; align-items: baseline;
  font-size: 15px; color: var(--ink-2);
}
footer a { text-decoration: none; }
footer a:hover { color: var(--ink); }
footer .links { display: flex; flex-wrap: wrap; gap: 22px; }
footer .end { margin-left: auto; color: var(--mute); font-size: 14px; }
footer b { color: var(--ink); font-weight: 600; }

footer .social { display: flex; flex-wrap: wrap; gap: 10px; }
footer .social a {
  display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 10px;
  background: var(--card); border: 1px solid var(--line);
  color: var(--accent-text);
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
footer .social a:hover { background: var(--accent); border-color: var(--accent); color: #fff; }
footer .social svg { width: 16px; height: 16px; }

/* ---------- guide article ---------- */

.article { max-width: 64ch; }
.article p { color: var(--ink-2); font-size: 18px; line-height: 1.7; }
.article h2 { margin-top: 52px; }
.article h3 { margin-top: 38px; font-size: 20px; }
.byline { font-size: 14.5px; color: var(--ink-2); padding: 18px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); display: flex; flex-wrap: wrap; gap: 8px 24px; }
.tblwrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 14px; background: var(--card); }
table { border-collapse: collapse; width: 100%; min-width: 620px; }
th, td { text-align: left; padding: 15px 20px; border-bottom: 1px solid var(--line); font-size: 15.5px; }
th { font-size: 13px; color: var(--ink-2); font-weight: 500; background: var(--sunk); white-space: nowrap; }
td.n { font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
tbody tr:last-child td { border-bottom: 0; }
tr.us td { font-weight: 600; }
tr.us td:first-child { box-shadow: inset 3px 0 0 var(--accent); }

/* ---------- prices ---------- */

.prices { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }
.price { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 26px; display: flex; flex-direction: column; gap: 14px; }
.price.hi { border-color: var(--ink-2); }
.price .tier { font-size: 14px; color: var(--ink-2); }
.price .amt { font-family: var(--mono); font-size: 34px; font-weight: 600; letter-spacing: -.04em; }
.price .amt small { font-size: 14px; font-weight: 400; color: var(--mute); }
.price ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; font-size: 15px; color: var(--ink-2); }
.tbd { font-size: 13px; color: var(--accent-text); }

/* ---------- surfaces ---------- */

.surfaces { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 20px; }
.surface { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 22px; text-decoration: none; display: flex; flex-direction: column; gap: 8px; }
.surface:hover { border-color: var(--ink-2); }
.surface strong { font-size: 17px; font-weight: 600; letter-spacing: -.02em; }
.surface span { font-size: 15px; color: var(--ink-2); }

/* ---------- skip link ---------- */

.skip { position: absolute; left: -9999px; top: 0; z-index: 100; background: var(--ink); color: var(--bg); padding: 12px 18px; border-radius: 0 0 10px 0; font-weight: 600; text-decoration: none; }
.skip:focus { left: 0; top: 0; }

/* build banner - delete at launch */
.buildbar { background: var(--ink); color: var(--bg); padding: 8px 20px; font-family: var(--mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; display: flex; gap: 16px; flex-wrap: wrap; }
.buildbar nav { margin-left: auto; display: flex; gap: 14px; }
.buildbar a { text-decoration: none; opacity: .65; }
.buildbar a:hover, .buildbar a[aria-current="page"] { opacity: 1; color: var(--accent); }

/* Mascot art is not in yet. Show a letter, never a broken image. */
.plateimg.is-missing img { display: none; }
.plateimg .fb { font-weight: 700; color: var(--ink-2); }

/* ============================================================
   Scroller

   A sticky mockup on the left; four steps scroll past on the right.
   State is set by IntersectionObserver on the steps - no scroll
   listeners, no hijacking. Everything animates on opacity and
   transform only, so it stays on the compositor.

   Without JS, or with reduced motion, every step is readable and
   the frame simply shows all three widgets.
   ============================================================ */

.scroller {
  padding: var(--gap) 0;
  max-width: 1340px;
  margin: 0 auto;
  scroll-margin-top: 80px;
  padding-top: 26px;
}
.scroll-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.42fr) minmax(0, .78fr);
  gap: 48px;
  align-items: start;
}

.scroll-stage {
  position: sticky;
  top: calc(var(--navh) + 28px);
  transition: transform .6s cubic-bezier(.2,.7,.3,1);
}

/* progress rail - encodes which of the four steps you are on */
.rail { display: flex; gap: 6px; margin-bottom: 22px; }
.rail i {
  height: 3px; flex: 1; border-radius: 2px; background: var(--line);
  transition: background .35s ease;
}
.rail i.on { background: var(--accent); }

.steps { display: flex; flex-direction: column; }
.step {
  min-height: 78vh; display: flex; flex-direction: column; justify-content: center;
  transition: opacity .45s ease;
}
/* Dimming is opt-in: the observer adds .js-on, so with JS off or unsupported
   every step stays fully readable. */
.scroller.js-on .step { opacity: .32; }
.scroller.js-on .step.on { opacity: 1; }
.step .kicker {
  font-family: var(--mono); font-size: 12px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--accent-text); margin-bottom: 14px;
}
.step .steph { font-size: 25px; letter-spacing: -.028em; margin: 0 0 14px; }
.step p { color: var(--ink-2); max-width: 42ch; font-size: 16.5px; }
.step .rating { font-family: var(--mono); font-size: 13.5px; color: var(--ink-2); }
/* Plus is a capability worth naming where the app is described, not only in the
   integrations table further down. */
.badge-plus {
  display: inline-flex; align-items: center; gap: 7px;
  margin-top: 16px; padding: 6px 12px 6px 10px;
  border-radius: 999px;
  background: var(--app-surface, #E3ECFF);
  border: 1px solid color-mix(in srgb, var(--app-accent, #2B61D9) 30%, transparent);
  color: var(--app-accent, #2B61D9);
  font-size: 13px; font-weight: 600; letter-spacing: -.01em;
}
.badge-plus::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; flex: 0 0 7px;
}

/* Wix has no downloadable "Verified by Wix" badge kit - on their own App
   Market listing it's plain styled text, no logo mark. So this is a plain
   text chip in our own visual language, not an imported asset. Deliberately
   neutral (ink, not --app-accent): it shows up in the hero, outside any
   data-app context, and needs to read the same inside the cct step too. */
.badge-wix {
  display: inline-flex; align-items: center; gap: 7px;
  margin-top: 16px;
  padding: 6px 12px 6px 10px;
  border-radius: 999px;
  background: var(--card);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: 13px; font-weight: 600; letter-spacing: -.01em;
}
.badge-wix::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; flex: 0 0 7px;
}
/* Badges sit in a row wherever two appear together (hero, cct step) - the
   row owns the top margin and the gap, so a wrapped second badge starts
   flush left instead of carrying a stray left-offset from its neighbour. */
.badge-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
.badge-row .badge-plus, .badge-row .badge-wix { margin-top: 0; }
/* .bfs's own rule carries margin: 0 auto 24px (needed where it stands alone).
   Left in place here, that bottom margin inflates this row's flex-line height,
   and align-items:center then centers .bfs's taller MARGIN box - not its
   visible 44.5px artwork - against .badge-wix's true box, so the badge with
   no margin sits lower than the one that has some. Zero it out in every row. */
.badge-row .bfs { margin: 0; }
/* .bfs renders at ~44.5px (it's Shopify's own fixed-proportion badge artwork,
   168x45 - not ours to rescale). Centering a ~35px pill next to it aligns
   their midpoints but leaves the top/bottom edges visibly mismatched. Only
   when .badge-wix follows .bfs (hero, install modal) does it need to match
   that height - paired with .badge-plus (cct step) both are already the
   same slim pill, so that pairing is untouched. */
.badge-row .bfs + .badge-wix { min-height: 44.5px; padding: 0 16px 0 14px; }

/* Wix gets its own real CTA, not a subordinate text link - same shape and
   weight as the platform's own button, distinguished by a neutral palette
   (not --accent-text, which means "this converts" everywhere else on the
   page) so the two don't read as duplicates of the same action. */
.step .go-wix {
  color: var(--ink); border-color: var(--line); background: transparent;
}
.step .go-wix:hover { background: var(--ink); border-color: var(--ink); color: var(--bg); }

/* Two CTAs side by side wrap the same way the badges do - flush left,
   never offset by a margin baked into one of the buttons. */
.step-ctas { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 20px; }
.step-ctas .go { margin-top: 0; }

/* .pb-ctas lives inside .panel-big, which is pinned to a fixed, measured
   height with overflow:hidden (deck-stage's 540px, matched deliberately -
   see the comment above .panel-big). The column is ~406px wide even on a
   wide desktop, so two full-size .pb-link buttons never fit on one line -
   wrapping to a second line is what overflowed the fixed stage. Keeping
   .pb-link-wix compact (smaller pad/type, no shadow) is what lets both
   sit on one line instead, so the stage's measured height stays true. */
.pb-ctas { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.pb-ctas .pb-link { margin-top: 0; }
.pb-link-wix {
  padding: 9px 16px; font-size: 13.5px; box-shadow: none;
  color: var(--ink); border-color: var(--line); background: transparent;
}
.pb-link-wix:hover { background: var(--ink); border-color: var(--ink); color: var(--bg); }

/* Platform stats read as a caption under the buttons, same weight as any
   other stat line on the page - not a link, so it can't be mistaken for a
   third, weaker CTA next to the two real ones. */
.also-on-stat { margin-top: 12px; font-size: 14px; color: var(--ink-2); }
.also-on-line .also-on { margin-left: 4px; }

/* Was plain underlined text - the one thing on each step that actually sends
   someone to install was styled identically to a footnote. Same accent orange
   as "Install free" in the hero, on purpose: that colour already means
   "this converts" everywhere else on the page, so borrowing it here instead of
   each step's own app-accent tint is what makes it read as a button rather
   than as themed decoration.

   --accent-text, not --accent, for the resting border/label: on this page's
   cream background plain --accent measures 4.04:1, just under the 4.5:1 body
   text needs. --accent-text is the same hue two shades darker (5.46:1) -
   already how the file handles this everywhere else (see the star rating a
   few sections up). Hover still fills solid --accent, where the contrast is
   white-on-orange and has room to spare. */
.step .go {
  display: inline-flex; align-items: center; gap: 8px;
  margin-top: 12px; padding: 8px 16px;
  text-decoration: none; font-weight: 600; font-size: 15px; justify-content: center;
  color: var(--accent-text); border: 1.5px solid var(--accent-text);
  border-radius: 999px; background: transparent;
  transition: background .16s ease, color .16s ease, border-color .16s ease;
}
.step .go:hover { background: var(--accent); border-color: var(--accent); color: #fff; }

/* ---- the frame reacts to state ---- */

.window.stage { transition: box-shadow .6s ease; position: relative; }

.panels { position: relative; }
.panel { transition: opacity .5s ease, transform .5s ease; }
.panel-cart {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: translateY(10px);
  pointer-events: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.stage[data-state="cct"] .panel-pdp { opacity: 1; transform: none; pointer-events: auto; }
.stage[data-state="cct"] .panel-cart { opacity: 0; transform: none; pointer-events: auto; }

/* isolate one widget per step */
.stage[data-state="lis"] .widget:not([data-w="lis"]),
.stage[data-state="pvc"] .widget:not([data-w="pvc"]),
.stage[data-state="cct"] .widget:not([data-w="cct"]) { opacity: .14; }
.stage[data-state="lis"] .widget[data-w="lis"],
.stage[data-state="pvc"] .widget[data-w="pvc"],
.stage[data-state="cct"] .widget[data-w="cct"] {
  border-top-color: transparent;
  transform: scale(1.03); transform-origin: left center;
}

/* the stock meter, revealed only on the LIS step */
.widget .bar {
  display: block;
  width: 100%;
  height: 4px;
  border-radius: 3px;
  margin-top: 0;
  background: var(--line);
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  transition: max-height .4s ease, opacity .4s ease, margin-top .4s ease;
  flex: 0 0 100%;
}
.widget .bar i { display: block; height: 100%; background: var(--accent); width: 22%; transition: width .8s ease; }
.stage[data-state="lis"] .widget[data-w="lis"] .bar,
.stage[data-state="all"] .widget[data-w="lis"] .bar { max-height: 4px; opacity: 1; margin-top: 9px; }

/* ---- cart panel ---- */

.cart { padding: 40px 44px; display: flex; flex-direction: column; gap: 16px; }
.cart-h { font-size: 20px; font-weight: 600; letter-spacing: -.02em; }
.cart-row { display: flex; align-items: center; gap: 16px; padding: 14px 0; border-top: 1px solid var(--line); }
.cart-row .thumb {
  width: 56px; height: 56px; flex: 0 0 56px; border-radius: 8px; border: 1px solid var(--line);
  background: linear-gradient(155deg, var(--sunk), var(--bg));
}
.cart-row .cart-item { flex: 1; display: flex; flex-direction: column; gap: 8px; }
.cart-row .cart-item b { font-weight: 600; }
.cart-row.totals { color: var(--ink-2); font-size: 15.5px; }
.cart .cta { margin-top: 6px; }

/* ---- mobile: sticky pins to the top, frame goes single column ---- */

@media (max-width: 900px) {
  /* minmax(0,…) and min-width:0, because a grid item defaults to min-width:auto
     - the checkout's min-content width blew the single column past the viewport
     and the whole page scrolled sideways. */
  .scroll-grid { grid-template-columns: minmax(0, 1fr); gap: 24px; }
  .scroll-stage { top: 12px; min-width: 0; }
  .scroll-stage .pdp { grid-template-columns: 1fr; gap: 20px; padding: 20px; }
  .scroll-stage .photo { display: none; }
  .cart { padding: 22px; }
  .step { min-height: 62vh; }
  .step .steph { font-size: 23px; }
}

/* ---- no JS, or reduced motion: everything visible and static ---- */

@media (prefers-reduced-motion: reduce) {
  .scroller.js-on .step { opacity: 1; }
  .panel, .widget, .rail i, .widget .bar { transition: none; }
}

/* ---- scroller layout corrections ---- */

/* The meter was a flex sibling of the label, so it stole horizontal space and
   wrapped "Only 4 left in stock" onto three lines. Give it its own row. */

/* The frame shares its row with the copy, so it needs more width than the
   1080 wrapper allows, and tighter internal padding than the hero version. */
.scroll-stage .pdp {
  padding: 34px;
  gap: 32px;
  grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr);
  align-items: stretch;
}

/* The 900px block far above already sets one column, but this rule is
   unconditional and sits later in the file, so it was quietly winning and the
   frame kept two tracks on a phone. With .photo display:none the copy then
   became the FIRST grid item and got the narrow .92fr track - 120px of the
   309px frame - while 141px sat empty beside it. Invisible until the timer grew
   an hours digit and started being clipped. */
@media (max-width: 900px) {
  .scroll-stage .pdp { grid-template-columns: minmax(0, 1fr); }
}
.scroll-stage .name { font-size: 21px; }
.scroll-stage .cta { padding: 15px; font-size: 16.5px; }
.scroll-stage .widget { font-size: 17px; padding: 14px 0; }
.scroll-stage .cart { padding: 30px; }

@media (max-width: 1180px) {
  .scroller { max-width: 100%; }
  .scroll-grid { gap: 40px; }
}

/* the cart panel is shorter than the product page; centre it so the frame
   keeps one height and nothing jumps during the cross-fade */

/* the photo was vertically centred and looked adrift - let it fill its column */
.scroll-stage .photo { aspect-ratio: auto; align-self: stretch; }

/* ---- mobile: the frame must be shorter than the viewport or sticky fails ----
   At 390×844 the full product panel came to 944px, so the stage scrolled off
   the top on the last step. Strip it to the essentials instead. */

@media (max-width: 900px) {
  .scroll-stage .rail { margin-bottom: 14px; }
  .scroll-stage .pdp { padding: 18px; gap: 12px; }
  .scroll-stage .detail { gap: 11px; }
  .scroll-stage .line { display: none; }
  .scroll-stage .name { font-size: 17px; }
  .scroll-stage .widget { font-size: 14px; padding: 9px 0; }
  .scroll-stage .cta { padding: 11px; font-size: 14.5px; }
  .scroll-stage .cart { padding: 18px; gap: 11px; }
  .scroll-stage .cart-row { padding: 11px 0; }
  .scroll-stage .cart-row .thumb { width: 42px; height: 42px; flex: 0 0 42px; }
  .scroll-stage .cart-h { font-size: 17px; }
}

/* ---------- hero proof ----------
   Every competitor checked leads the hero with numbers rather than a product
   visual; two of five have no hero visual at all. The mockup lives in the
   scroller below, so it does not need to appear twice. */

.proof {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px; background: var(--line);
  border: 1px solid var(--line); border-radius: 14px; overflow: hidden;
  margin: 56px auto 0; max-width: 860px;
}
.proof > div { background: var(--card); padding: 26px 20px; }
.proof .pnum {
  display: block; font-family: var(--mono); font-weight: 600;
  font-size: clamp(30px, 3.4vw, 40px); line-height: 1;
  letter-spacing: -.045em; font-variant-numeric: tabular-nums;
}
.proof .pnum .star { color: var(--accent-text); font-size: .6em; margin-left: 2px; }
.proof .plabel { display: block; font-size: 14px; color: var(--ink-2); margin-top: 10px; }

/* ---------- hero quotes ---------- */

.quotes {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 20px; margin: 26px auto 0; max-width: 1000px; text-align: left;
}
.quote {
  margin: 0; background: var(--card); border: 1px solid var(--line);
  border-radius: 14px; padding: 22px;
  display: flex; flex-direction: column; gap: 12px;
}
.quote .stars { color: var(--accent-text); font-size: 14px; letter-spacing: .1em; }
.quote blockquote {
  margin: 0; font-size: 15px; line-height: 1.55; color: var(--mute);
  border: 1px dashed var(--line); border-radius: 10px; padding: 14px;
}
.quote figcaption { font-size: 13px; color: var(--mute); }

/* ============================================================
   Scroller animation

   All compositor-only (opacity / transform). The showpiece is the
   CCT step: a cursor travels to Add to cart, clicks, and the click
   is what flips the frame to the cart - so the transition reads as
   a demonstration rather than a slideshow.
   ============================================================ */

/* ---- the cursor ---- */

.cursor {
  position: absolute; z-index: 5; left: 52%; top: 34%;
  width: 22px; height: 22px; pointer-events: none;
  opacity: 0; transform: translate(-2px, -2px) scale(.9);
  transition: opacity .3s ease, left .85s cubic-bezier(.4,.05,.2,1),
              top .85s cubic-bezier(.4,.05,.2,1), transform .18s ease;
}
.cursor::before {
  content: ""; position: absolute; inset: 0;
  background: #FFFFFF;
  clip-path: polygon(0 0, 0 74%, 26% 56%, 44% 96%, 60% 88%, 42% 48%, 74% 44%);
  filter:
    drop-shadow(0 0 1px rgba(0,0,0,.9))
    drop-shadow(0 0 1px rgba(0,0,0,.55))
    drop-shadow(0 2px 4px rgba(0,0,0,.3));
}
.cursor::after {
  content: ""; position: absolute; left: -13px; top: -13px;
  width: 48px; height: 48px; border-radius: 50%;
  border: 2px solid var(--accent); opacity: 0; transform: scale(.3);
}
.stage[data-state="cct"] .cursor { opacity: 1; left: 78%; top: 84%; }
.stage.clicking .cursor { transform: translate(-2px, -2px) scale(.74); }
.stage.clicking .cursor::after { animation: ring .5s ease-out forwards; }
@keyframes ring {
  from { opacity: .8; transform: scale(.3); }
  to   { opacity: 0;  transform: scale(1.15); }
}

/* the button reacts to being clicked */
.stage.clicking .panel-pdp .cta { transform: scale(.96); filter: brightness(1.25); }

/* the panel flip is now driven by the click, not by the state alone */
.stage.cart-open .panel-pdp { opacity: 0; transform: translateY(-10px); pointer-events: none; }
.stage.cart-open .panel-cart { opacity: 1; transform: none; pointer-events: auto; }

/* ---- staggered widget entry ---- */

.scroller.js-on .widget { transition: opacity .4s ease, transform .45s cubic-bezier(.2,.7,.3,1); }
.stage.entering .widget { opacity: 0; transform: translateY(8px); }
.stage.entering .widget[data-w="lis"] { transition-delay: 0s; }
.stage.entering .widget[data-w="pvc"] { transition-delay: .06s; }
.stage.entering .widget[data-w="cct"] { transition-delay: .12s; }

/* ---- the frame lifts while its section owns the viewport ---- */
.scroller.js-on .scroll-stage { transform: translateY(10px) scale(.985); }
.scroller.js-on.in-view .scroll-stage { transform: none; }
.scroller.js-on.in-view .window.stage { box-shadow: 0 2px 6px rgba(22,24,26,.05), 0 30px 60px -30px rgba(22,24,26,.32); }

/* ---- the counting number gets a beat ---- */

.num.counting { transition: none; }

@media (prefers-reduced-motion: reduce) {
  .cursor { display: none; }
  .scroller.js-on .scroll-stage,
  .scroller.js-on.in-view .scroll-stage { transform: none; transition: none; }
  .stage.entering .widget { opacity: 1; transform: none; }
  .stage[data-state="cct"] .panel-pdp { opacity: 0; pointer-events: none; }
  .stage[data-state="cct"] .panel-cart { opacity: 1; }
}

/* ---------- hero badge + trust row ----------
   Both borrowed from Vitals, which does this better than a stats grid:
   a real trust badge above the headline, then one compact line of
   qualifiers under the subhead rather than four heavy cells. */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--sunk);
  border: 1px solid var(--line);
  color: var(--ink-2);
  font-size: 14px;
  font-weight: 500;
  padding: 8px 15px;
  border-radius: 999px;
  margin-bottom: 20px;
  margin-top: 4px;
}
.badge::first-letter { color: var(--accent-text); }

.trust {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px 30px;
  margin: 26px 0 0;
  font-size: 15px;
  color: var(--ink-2);
  margin-top: 22px;
}
.trust span { display: inline-flex; align-items: center; gap: 8px; }
.trust b { color: var(--accent-text); font-weight: 700; }
/* gold stars, not accent-coloured - a rating should read as a rating */
.trust .gold, .quote .stars.gold { color: #B57C00; font-style: normal; letter-spacing: .06em; }

/* the frame below peeks above the fold, so the scroll is invited */

/* ---------- step figures ----------
   Verified numbers render normally. Slots awaiting Effectify's own measured
   data render as a dash and are visibly pending, so an empty metric can never
   be mistaken for a claim. */

.stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 12px;
  overflow: hidden;
  margin: 22px 0 0;
}
.stats > div { background: var(--card); padding: 16px 14px; }
.stats .sv {
  display: block; font-family: var(--mono); font-weight: 600; font-size: 22px;
  letter-spacing: -.03em; font-variant-numeric: tabular-nums; line-height: 1;
}
.stats .sv i { color: #B57C00; font-style: normal; font-size: 1.4em; margin-left: 1px; }
.stats .sl { display: block; font-size: 12.5px; color: var(--ink-2); margin-top: 8px; line-height: 1.35; }
.stats .pending .sv { color: var(--mute); }
.stats .pending .sl { color: var(--mute); }
.stats .pending { background: var(--sunk); }
/* The grid draws its dividers with a 1px gap over a coloured background, so an
   odd number of figures left a filled cell with nothing in it. An unpaired last
   figure takes the whole row instead of leaving a hole. */
.stats > div:nth-child(3):last-child { grid-column: 1 / -1; }

@media (max-width: 900px) {
  .stats { grid-template-columns: 1fr 1fr; }
}

/* ---------- step-demo: before/after widget preview ----------
   Sits where the stat grid used to on the AI Stock Counter card. Loops
   between two states: no widget shown at all, and the widget fully
   switched on. The meter bar on the right is the only thing that argues
   the point - short and grey with no widget, filling to full and coloured
   once it appears - deliberately with no "lower/higher conversion" copy
   attached; the bar is the claim. Reuses pulse-red (defined above) on the
   dot rather than declaring a second copy of the same keyframes.

   Mobile only: on desktop and tall/short-height views the "how" section
   stays a pinned scroller with the real, functioning widget mock next to
   this text the whole time (see the scroll-stage rules), so a second,
   decorative copy here would be redundant. Below the same breakpoint that
   unpins the scroller (phones: max-width 760px, or short viewports), that
   pinned mock is shown once up front rather than staying in view, so this
   inline demo is what reinforces the message as the step scrolls by. */

.step-demo { display: none; }

@media (max-width: 760px), (max-height: 620px) {
  .step-demo {
    display: block;
    margin: 22px 0 0;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 16px 18px;
  }
}
.step-demo-mock { display: flex; flex-direction: column; gap: 8px; }
.step-demo-name, .step-demo-price {
  display: block; height: 10px; border-radius: 4px; background: var(--line);
}
.step-demo-name { width: 58%; height: 12px; }
.step-demo-price { width: 30%; }

/* The card embeds the real .widget markup (same dot/eye, .num.tick-* odometer
   reels, and .bar) rather than a decorative copy, so site.js's live-figure
   loop drives this exactly like the desktop mock - same digits, same roll,
   same clock. Only the stock bar needs a nudge: it only reveals itself inside
   the scrollytelling stage's data-state, which this card isn't part of. */
.step-demo .widget { margin-top: 2px; }
.step-demo .widget[data-w="lis"] .bar { max-height: 4px; opacity: 1; margin-top: 9px; }

.step-demo-tag {
  display: inline-block; margin-top: 12px;
  padding: 3px 10px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; white-space: nowrap;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
}

@media (max-width: 480px) {
  .step-demo { padding: 14px 16px; }
}

/* ---------- reviews ----------
   Laid out as finished cards rather than dashed placeholders - the previous
   version looked broken. A small amber chip marks each one as a sample so it
   cannot be shipped by accident. */

/* ---- reviews marquee ----

   Two rows travelling in opposite directions. The loop is seamless because each
   row holds the same set of cards twice and slides exactly one set's width: the
   gap lives inside .rev-set as a trailing padding rather than as a flex gap
   between the two sets, so -50% lands on the duplicate's first card to the
   pixel instead of half a gap short of it.

   Deliberately not gated on prefers-reduced-motion, same call as the deck and
   the jump links. It pauses on hover and on keyboard focus, so a reader who
   wants to stop and read a quote can.

   The duplicate set is aria-hidden, so a screen reader hears each review once. */

/* full-bleed: the section's own padding no longer applies to these, so the
   loop reads as continuous rather than as a strip clipped to 1016px with two
   half-cards hanging off the ends */
.reviews-sec { padding-inline: 0; padding-top: 40px; }
.reviews-sec > .wrap { margin-bottom: 0; }

.rev-marquee {
  position: relative;
  margin-top: 44px;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.rev-marquee + .rev-marquee { margin-top: 20px; }
.rev-track {
  display: flex;
  width: max-content;
  animation: rev-slide 78s linear infinite;
}
.rev-track.rev-back { animation-direction: reverse; }
.rev-marquee:hover .rev-track,
.rev-marquee:focus-within .rev-track { animation-play-state: paused; }
.rev-set { display: flex; gap: 20px; padding-right: 20px; }
.rev-set .review { flex: 0 0 320px; width: 320px; }

@keyframes rev-slide {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (max-width: 760px) {
  .rev-set .review { flex-basis: 272px; width: 272px; }
  .rev-track { animation-duration: 56s; }
  /* A row is as tall as its longest quote, and at 272px wide the longest one
     ran off the bottom of the screen and dragged the other five up with it.
     Clamped with an ellipsis so it reads as an excerpt of a longer review -
     nothing is reworded, and the full text is on the App Store. */
  .rev-set .review blockquote {
    display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 9; overflow: hidden;
  }
}

.review {
  position: relative; margin: 0;
  background: var(--card); border: 1px solid var(--line); border-radius: 14px;
  padding: 26px 24px 24px;
  display: flex; flex-direction: column; gap: 16px;
}
.review .sample {
  position: absolute; top: 14px; right: 14px;
  font-family: var(--mono); font-size: 9.5px; letter-spacing: .14em; text-transform: uppercase;
  color: #8A5A00; background: #FDF3DC; border: 1px solid #F0DFB0;
  padding: 3px 8px; border-radius: 999px;
}
.review .rstars { color: #B57C00; font-size: 15px; letter-spacing: .1em; }
/* which app each quote is about - same [data-app] accent tokens the deck and
   the scroller already key off, so the ring colour matches without a new
   palette. Sits over .rstars rather than beside it: the row was already the
   card's only fixed-height line, and a second element in it wrapped at 272px. */
.review .rev-ico {
  position: absolute; top: 20px; right: 22px;
  width: 22px; height: 22px; border-radius: 7px;
  border: 1.5px solid var(--app-accent, var(--line));
  box-shadow: 0 2px 6px rgba(0,0,0,.08);
  background: var(--card);
}
.review blockquote {
  margin: 0; font-size: 16.5px; line-height: 1.55; color: var(--ink);
}
.review blockquote::before { content: "“"; }
.review blockquote::after { content: "”"; }
.review figcaption {
  margin-top: auto; display: flex; flex-direction: column; gap: 4px;
  padding-top: 14px; border-top: 1px solid var(--line);
}
.review .who { font-weight: 600; font-size: 15px; }
.review .what { font-family: var(--mono); font-size: 11.5px; color: var(--ink-2); letter-spacing: .02em; }
.note b { color: #8A5A00; font-weight: 600; }

/* the verification line under the marquees.
   Deliberately NOT a flex container: it holds a sentence, and flex would make
   every text run and every <b> inside it a separate item, so "Twelve of 136
   reviews" came apart into three chunks separated by the gap. The pill row is
   the only thing that needs to lay out, so the flex lives there. */
.rev-verify {
  margin: 30px 0 0; font-size: 14.5px; line-height: 1.65; color: var(--ink-2);
  max-width: 70ch;
}
.rev-verify b { color: var(--ink); font-weight: 600; }
.rev-links { display: flex; flex-wrap: wrap; gap: 9px 10px; margin-top: 14px; }
.rev-links a {
  display: inline-flex; align-items: center; gap: 8px;
  text-decoration: none; color: var(--ink); font-weight: 600;
  padding: 7px 13px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--card);
  transition: border-color .2s ease, background .2s ease;
}
.rev-links a:hover { border-color: var(--ink-2); background: var(--bg); }
.rev-links i {
  font-style: normal; font-family: var(--mono); font-size: 12.5px;
  color: #B57C00; letter-spacing: .01em;
}

/* ---------- the fold ----------
   Measured: 135px of dead space sat between the trust row and the frame, with
   a rule at the fold reading as an ending, so nothing invited the scroll. The
   frame now breaks the fold deliberately - the same move Vitals makes with its
   screenshots peeking from the bottom of the first screen. */
.scroll-stage .rail { margin-bottom: 18px; }

/* a quiet cue, in case the peek is missed */
.cue {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin: 30px 0 0;
  font-size: 13.5px;
  color: var(--mute);
  margin-top: 22px;
}
.cue i {
  width: 18px; height: 18px; border-radius: 50%; border: 1px solid var(--line);
  display: grid; place-items: center; font-style: normal; font-size: 10px; color: var(--ink-2);
}
@media (prefers-reduced-motion: no-preference) {
  .cue i { animation: nudge 2.4s ease-in-out infinite; }
}
@keyframes nudge { 0%,100% { transform: translateY(0); } 50% { transform: translateY(2px); } }

/* Trimming the stack above the frame so it breaks the fold on a short laptop
   viewport too, not only on tall displays. Measured before: frame top 827,
   which cleared an 800px viewport entirely. */

/* ---------- sticky nav ----------
   The scroller's stage is also sticky, so its offset has to clear the nav or
   the frame slides underneath it. Both are pinned off one variable. */   /* measured, not assumed */

header.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  /* Transparent at rest. A backdrop-filter here blurred the hero wash behind
     it and left a hard seam along the bar's bottom edge - the gradient looked
     cut in half. The blur only arrives once the page has actually scrolled. */
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background .3s ease, border-color .25s ease, backdrop-filter .3s ease;
  width: 100%;
}
header.topbar.stuck {
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(150%) blur(14px);
  -webkit-backdrop-filter: saturate(150%) blur(14px);
  border-bottom-color: var(--line);
}
/* a hairline appears only once the page has moved */

/* ---------- the other two display preferences ----------
   Reduced motion was already handled. These two were not: a frosted bar is
   exactly what someone who has asked for less transparency does not want, and
   a hairline border is exactly what someone asking for more contrast cannot
   see. Both are one-line answers. */

@media (prefers-reduced-transparency: reduce) {
  header.topbar.stuck {
    background: var(--bg);
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .pb-figs > div { background: rgb(255 255 255 / .12); backdrop-filter: none; }
}

@media (prefers-contrast: more) {
  header.topbar.stuck { background: var(--bg); border-bottom-color: var(--ink-2); }
  .btn-line, .appcard, .review, .integ, .mini { border-color: var(--ink-2); }
  .sep { background: var(--ink-2); opacity: 1; }
  :root { --mute: #4A5155; --ink-2: #3C4245; }
}

/* browsers without color-mix get a solid bar rather than a transparent one */
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  header.topbar.stuck { background: var(--bg); }
}
main { scroll-margin-top: var(--navh); }
#how, #reviews, #effectify { scroll-margin-top: calc(var(--navh) + 16px); }

/* Whole-page smooth scroll for every in-page link - "How it works"/"Reviews"
   ride on this directly; the three app links get their own centred, JS-driven
   scroll on top of it (see site.js). Deliberately NOT gated on
   prefers-reduced-motion, same call as the deck and the jump links: a nav
   click is a direct request to travel across the page, and the animation is
   the answer to that request, not incidental motion layered over it. */
html { scroll-behavior: smooth; }

/* the nav link for whichever app is currently live in the preview below */
.nav .links a.jump.on { color: var(--app-accent, var(--ink)); font-weight: 600; }
.nav-cta { background: var(--accent); color: #fff !important; padding: 8px 18px; border-radius: 999px; font-weight: 600; font-size: 14px; white-space: nowrap; }
.nav-cta:hover { filter: brightness(1.06); }

@media (max-width: 900px) {
  :root { --navh: 96px; }
  .scroll-stage { top: calc(var(--navh) + 10px); }
}

/* At 390px the nav wrapped onto three rows and stood 227px tall - a quarter of
   the viewport, with every anchor landing underneath it. One row: brand left,
   Install right. The two section links are anchors you scroll past anyway. */

@media (max-width: 760px) {
  :root { --navh: 68px; }
  .nav { gap: 14px; padding-block: 15px; flex-wrap: nowrap; }
  /* Hide the four section jump links but keep the Install CTA - it's also
     inside .links in the markup, so it's targeted separately rather than
     hidden along with its siblings by a blanket `.links { display: none }`. */
  .nav .links { margin-left: auto; gap: 0; }
  .nav .links a.jump { display: none; }
  .brand { font-size: 17px; }
  .brand .mark { width: 28px; height: 28px; flex: 0 0 28px; border-radius: 8px; }
  .nav .btn { padding: 10px 16px; font-size: 15px; }
  .scroll-stage { top: calc(var(--navh) + 10px); }
}

/* Brand wordmark + Install button together are wider than the smallest
   phones (measured overflow at 320px). Trim the CTA's own padding/size
   rather than touch its tap target's height. */
@media (max-width: 360px) {
  .nav { gap: 10px; }
  .nav .btn { padding: 10px 12px; font-size: 14px; }
  .brand { font-size: 16px; }
}

/* The URL was the one line in the frame that could not shrink: at 320px it
   wrapped to two rows and pushed the whole mock down. */
@media (max-width: 900px) {
  .scroll-stage .bar-top .url {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
  }
}

/* ---- phones: unpin the frame ----

   The scrollytelling model needs two things on screen at once - the pinned mock
   and the step you are reading. A phone has room for one. On a 568px viewport,
   once the nav and the frame have taken their share there is nothing left, and
   the frame ran past the fold whatever size its contents were shrunk to.

   So on a phone the section stops being a scroller: the frame is shown once, up
   front, in its full state with all three widgets, and the four steps read as a
   plain list underneath it. Nothing is pinned, nothing is dimmed, nothing is
   competing for the same pixels. The desktop behaviour is untouched.

   Height is in the query as well as width, because a phone held sideways is
   wide but just as short. */

@media (max-width: 760px), (max-height: 620px) {
  /* The per-step demo cards below now show the same before/after story for
     each app individually, so the big combined mock up here is dropped on
     phones rather than shown once, unpinned, ahead of the steps. */
  .scroll-stage { display: none; }
  .scroll-grid { gap: 20px; }

  /* The state machine is switched off in the JS for these viewports, so the
     frame keeps its data-state="all" - every widget on, nothing dimmed, no
     .js-on to fade the steps. These three just remove the furniture that only
     meant something while it was running. */
  .panel-cart { display: none; }
  .stagewrap .cursor { display: none; }
  .rail { display: none; }

  /* 62vh per step was scroll distance for the pinned frame to work against.
     As a plain list it is just half a screen of blank between paragraphs. */
  .step { min-height: 0; padding: 26px 0; }
  .steps { gap: 4px; }

  /* Flat does not have to mean inert. Losing the pinned frame lost the sense of
     the section unfolding as you move through it, so each step arrives on its
     own instead of the whole list simply being there.

     The class is added by JS, so with JS off nothing is ever hidden. It is a
     separate class from .reveal above precisely because that one is switched
     off under prefers-reduced-motion - same call as the deck and the jump
     links: this is the section revealing itself as you travel through it, which
     is the content, not decoration layered over it. */
  .flat-reveal {
    opacity: 0;
    transform: translateY(22px);
    transition: opacity .6s cubic-bezier(.2,.8,.25,1),
                transform .7s cubic-bezier(.2,.8,.25,1);
  }
  .flat-reveal.in { opacity: 1; transform: none; }
}

/* ---------- closing block ----------
   The mascot art is full-body with a transparent background, so it belongs
   where there is room - not in a 46px icon slot. */
.closing {
  display: grid; grid-template-columns: 280px 1fr; gap: 48px; align-items: center;
}
.closing .fox { width: 280px; height: auto; }
.closing h2 { max-width: 22ch; }
.closing .lede { max-width: 44ch; }

@media (max-width: 820px) {
  .close { text-align: center; }
  .closing { grid-template-columns: 1fr; gap: 24px; justify-items: center; }
  .closing .fox { width: 200px; }
  .closing h2, .closing .lede { margin-inline: auto; }
  .closing .btnrow { justify-content: center; }
}

/* ============================================================
   Checkout panel - CCT

   The timer's real home is checkout, so the frame becomes one.
   Laid out like Shopify's: form left, sticky order summary right,
   and the urgency banner in Shopify's critical-notice styling.
   ============================================================ */

.checkout { display: grid; grid-template-columns: 1.18fr 1fr; min-height: 100%; }
.co-main { padding: 26px 28px; display: flex; flex-direction: column; gap: 12px; }
.co-brand { font-weight: 600; font-size: 17px; letter-spacing: -.02em; margin-bottom: 4px; }

/* Shopify's critical banner: #FFF0F0-ish fill, #E0B3B2 border, filled round
   icon in their critical red, dismiss control on the right. */
.co-banner {
  display: grid;
  grid-template-columns: 20px 1fr 14px;
  gap: 10px;
  align-items: start;
  background: #FDF1F1;
  border: 1px solid #E7B7B5;
  border-radius: 8px;
  padding: 13px 14px 14px;
  font-size: 13px;
  line-height: 1.45;
  color: #1A1A1A;
}
.co-ico {
  width: 20px; height: 20px; border-radius: 50%; background: #D72C0D; color: #fff;
  display: grid; place-items: center; font-size: 13px; font-weight: 700; line-height: 1;
}
.co-msg b { font-weight: 600; background: #F1F1F1; padding: 0 3px; border-radius: 3px; }
.co-clock {
  display: block; margin-top: 7px; font-family: var(--mono); font-size: 12px; color: #5A6165;
}
.co-clock b { background: none; padding: 0; }
.co-x { color: #6B7177; font-size: 15px; line-height: 1; justify-self: end; }

.co-h { font-size: 14px; font-weight: 600; margin-top: 8px; }
.co-field {
  border: 1px solid #C9CCCF; border-radius: 8px; padding: 11px 12px;
  font-size: 13px; color: var(--mute); background: #fff;
}
.co-row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.co-pay {
  margin-top: 8px; background: #1A1A1A; color: #fff; border-radius: 8px;
  padding: 13px; text-align: center; font-weight: 600; font-size: 15px;
}

.co-side {
  background: #F1F1F1;
  border-left: 1px solid #E1E1E1;
  padding: 22px 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.co-item { display: grid; grid-template-columns: 36px 1fr auto; gap: 10px; align-items: center; }
.co-thumb {
  width: 40px; height: 40px; border-radius: 7px; border: 1px solid #DADADA;
  background: linear-gradient(155deg, #E7E7E7, #F6F6F6); position: relative;
}
.co-thumb b {
  position: absolute; top: -6px; right: -6px; width: 17px; height: 17px; border-radius: 50%;
  background: #6B7177; color: #fff; font-size: 10px; display: grid; place-items: center;
}
.co-item-t { display: flex; flex-direction: column; font-size: 13px; }
.co-item-t b { font-weight: 600; font-size: 12.5px; line-height: 1.3; }
.co-item-t i { font-style: normal; color: var(--mute); font-size: 12px; }
.co-price { font-size: 13px; }
.co-line {
  display: flex; justify-content: space-between; font-size: 13px; color: #4A5054;
  padding-top: 10px; border-top: 1px solid #E1E1E1;
}
.co-total { font-size: 16px; font-weight: 600; color: var(--ink); }

@media (max-width: 900px) {
  .checkout { grid-template-columns: 1fr; }
  .co-side { border-left: 0; border-top: 1px solid #E1E1E1; padding: 18px; }
  .co-main { padding: 18px; }
}

/* On a phone the frame is pinned near the top and has maybe 500px to work with,
   so the checkout mock is trimmed to what actually carries the point: the
   urgency banner and enough form to read as checkout. Real Shopify collapses
   the order summary on mobile for the same reason. */

/* ============================================================
   Soft washes, fox, and the official App Store badge
   ============================================================ */

/* Vitals breaks the white with a soft radial wash behind each mockup rather
   than a solid section fill. Hue follows the app on screen. */
.stagewrap { position: relative; }
.wash {
  position: absolute; z-index: 0; inset: -14% -18% -10%;
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(60% 55% at 45% 40%, var(--wash, #FFE8D6), transparent 72%);
  filter: blur(22px); opacity: .85;
  transition: background .7s ease;
}
.stagewrap .window { position: relative; z-index: 1; }

/* ---- the fox travels with the scroller ---- */

.foxes {
  position: absolute;
  z-index: 2;
  right: auto;
  bottom: -46px;
  width: 170px;
  height: 228px;
  left: -108px;
}
.fx {
  position: absolute; inset: 0; width: 100%; height: auto;
  opacity: 0; transform: translateY(10px) scale(.97);
  transition: opacity .45s ease, transform .55s cubic-bezier(.2,.7,.3,1);
  filter: drop-shadow(0 18px 22px rgba(22,24,26,.16));
}
.stage-all   ~ .foxes .fx[data-for="all"],
.foxes .fx.on { opacity: 1; transform: none; }

@media (max-width: 1100px) { .foxes { right: -20px; width: 132px; } }
@media (max-width: 900px)  { .foxes { display: none; } }

/* ---- welcoming fox in the hero ---- */

.hero-fox {
  position: absolute;
  display: inline-block;
  margin: 0 0 6px;
  margin-bottom: 20px;
  right: -30px;
  bottom: 4px;
  width: 232px;
  pointer-events: none;
}
.hero-fox img { width: 100%; height: auto; filter: drop-shadow(0 26px 30px rgba(120,60,20,.18)); }
@keyframes bob { 0%,100% { transform: translateY(0) } 50% { transform: translateY(-5px) } }

/* the hero wash sits behind the fox and headline */
.hero::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: 50%;
  top: -90px;
  width: min(1180px, calc(100vw - 16px));
  height: 640px;
  transform: translateX(-50%);
  background:
    radial-gradient(38% 44% at 28% 34%, #FFD9C2, transparent 66%),
    radial-gradient(34% 40% at 74% 30%, #FFE6F0, transparent 66%),
    radial-gradient(46% 46% at 50% 62%, #FFEFE2, transparent 70%);
  filter: blur(30px);
  opacity: .95;
  pointer-events: none;
}

/* ---- official Shopify App Store badge ----
   Shopify's brand assets ship this badge in black or white only, at a minimum
   30px height with half-height clear space, and forbid recolouring, cropping,
   stretching or animating it. It must link to the listing. There is no
   downloadable "Built for Shopify" badge, so that stays plain text. */

.appstore {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  background: #000;
  color: #fff;
  text-decoration: none;
  height: 44px;
  padding: 0 18px;
  border-radius: 8px;
  margin: 22px auto 0;
  width: fit-content;
  font-size: 14px;
}
.appstore svg { width: 20px; height: 22px; flex: 0 0 20px; }
.appstore span { display: flex; flex-direction: column; line-height: 1.15; text-align: left; }
.appstore i { font-style: normal; font-size: 10.5px; opacity: .82; }
.appstore b { font-size: 14px; font-weight: 600; letter-spacing: -.01em; }
.appstore:hover { opacity: .9; }

/* ---- fixes after looking at the CCT state ---- */

/* 2. The cursor's target (Add to cart) no longer exists once checkout is open,
      so leaving it on screen was nonsense. */
.stage.cart-open .cursor { opacity: 0; }

/* 3. The fox sat over the order summary. Push it clear of the frame. */
@media (max-width: 1240px) { .foxes { right: -70px; width: 148px; } }
@media (max-width: 1100px) { .foxes { right: -44px; width: 128px; } }

/* 4. "Merino Wool Runner" wrapped to three lines in a cramped summary column. */
.co-msg { font-size: 13px; }

/* ---- hero spacing + App Store link ---- */

/* the badge pill was landing on the fox */
.appstore .arrow { opacity: .7; }

/* a figure flashes as it settles on a new reading */
@media (prefers-reduced-motion: no-preference) {
  .bump { animation: bump .5s cubic-bezier(.2,1.4,.4,1); }
}
@keyframes bump {
  0%   { transform: translateY(0)     scale(1); }
  30%  { transform: translateY(-2px)  scale(1.07); }
  100% { transform: translateY(0)     scale(1); }
}
.num, .sv, h1 .num { display: inline-block; }

/* ---- Shopify's own assets, used unaltered ----
   built-for-shopify.svg and the logo come straight from Shopify's brand assets
   library. Their rules forbid recolouring, cropping, stretching, rotating or
   animating them, so these rules only set a size and leave the art alone. */

/* Shopify's own badge, drawn on a 258.5-wide viewBox. It was rendered at 129px
   - exactly half - which put its wordmark at around 9px and made the one piece
   of third-party endorsement on the page the hardest thing on it to read. 168px
   is still under the artwork's native size, so it stays crisp. */
/* A soft bloom in the badge's own blue, not an arbitrary accent colour -
   stacked drop-shadows so it reads as light coming off the card rather than a
   generic box-shadow rectangle sitting under it. Static, not pulsing: this is
   the one piece of third-party endorsement on the page, and it should read as
   settled credibility, not as something competing for attention the way the
   live figures above it do. */
.bfs {
  display: block; margin: 0 auto 24px; width: 168px; height: auto;
  filter:
    drop-shadow(0 0 1px rgba(13,121,167,.22))
    drop-shadow(0 4px 16px rgba(13,121,167,.24))
    drop-shadow(0 0 34px rgba(73,163,201,.30));
}
.appstore img { height: 24px; width: auto; }

/* ---- the fox moves to the far side of the frame ---- */
@media (max-width: 1240px) { .foxes { left: -74px; width: 148px; } }
@media (max-width: 1100px) { .foxes { left: -46px; width: 128px; } }

/* ============================================================
   Effectify tie - their brand, not ours

   effectify.io is black with a crimson glow, a pill nav and a
   </Effectify> wordmark. Wearing that here makes the parentage
   read as a brand relationship rather than a footnote, and gives
   the light page one deliberate dark band.
   ============================================================ */

.parent-block { padding-top: 48px; }
.pb {
  position: relative; overflow: hidden;
  background: #08070A; color: #F3EFF1; border-radius: 24px;
  display: grid; grid-template-columns: 1.12fr .88fr; gap: 52px; align-items: center;
  padding: 56px 52px 58px;
}
/* their crimson horizon glow */
.pb-glow {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(120% 70% at 50% -18%, rgba(196,49,91,.42), transparent 58%),
    radial-gradient(70% 46% at 12% 106%, rgba(148,19,58,.34), transparent 62%),
    radial-gradient(60% 40% at 92% 88%, rgba(196,49,91,.20), transparent 64%);
}
.pb > *:not(.pb-glow) { position: relative; z-index: 1; }

.pb-mark {
  display: block; height: 28px; width: auto; opacity: .85; margin-bottom: 18px;
}

.pb h2 { color: #fff; margin-bottom: 14px; text-transform: none; }
.pb p { color: rgba(243,239,241,.72); max-width: 52ch; margin: 0; }
.pb .btnrow { margin-top: 26px; }
.pb-cta { background: #C4315B; color: #fff; border-color: #C4315B; border-radius: 999px; }
.pb-cta:hover { filter: brightness(1.1); }
.pb-ghost { background: transparent; color: #F3EFF1; border-color: rgba(243,239,241,.26); border-radius: 999px; }
.pb-ghost:hover { background: rgba(255,255,255,.08); }

/* The four cells sat on a 1px hairline grid with no border of their own, so on
   the near-black background they read as one flat plate instead of four
   distinct figures. Each now gets its own edge, a lift on hover, and a crimson
   top rule that ties them to the glow behind the block rather than sitting on
   top of it as plain grey boxes. */
.pb-figs {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
}
.pb-figs > div {
  position: relative; overflow: hidden;
  background: rgba(255,255,255,.045);
  border: 1px solid rgba(255,255,255,.10);
  border-radius: 14px;
  padding: 20px 20px 22px;
  backdrop-filter: blur(4px);
  transition: background .25s ease, border-color .25s ease, transform .25s ease;
}
.pb-figs > div::before {
  content: ""; position: absolute; top: 0; left: 18px; right: 18px; height: 2px;
  background: linear-gradient(90deg, #C4315B, rgba(196,49,91,0));
  border-radius: 0 0 2px 2px;
}
.pb-figs > div:hover {
  background: rgba(255,255,255,.075);
  border-color: rgba(196,49,91,.34);
}
@media (hover: hover) {
  .pb-figs > div:hover { transform: translateY(-2px); }
}
.pb-n {
  display: block; font-family: var(--mono); font-weight: 600; font-size: 34px;
  letter-spacing: -.04em; line-height: 1; color: #fff; font-variant-numeric: tabular-nums;
}
.pb-l { display: block; font-size: 13px; color: rgba(243,239,241,.62); margin-top: 10px; }

@media (max-width: 900px) {
  .pb { grid-template-columns: 1fr; gap: 30px; padding: 34px 26px 36px; }
}

/* ============================================================
   Hero - centred, the headline doing the work
   Loox gives its h1 88px/800 at line-height 1.0 and lets the rest
   of the first screen be almost empty. Same discipline here, with
   the fox greeting from the edge instead of taking a column.
   ============================================================ */
.hero-grid { display: block; }
.hero-badges { display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; margin: 0 auto 26px; }
.hero-badges .bfs { margin: 0; }
.hero-badges .badge-wix { margin: 0; }
.hero .trust { justify-content: center; margin-top: 26px; }
.hero .appstore { margin: 20px auto 0; }
.hero .cue { justify-content: center; margin-top: 24px; }

/* three stacked washes instead of one, so the tint has depth */
.hero-fox .livechip {
  bottom: 30px;
  left: -28px;
  position: absolute;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 6px 12px 6px 10px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--ink-2);
  box-shadow: var(--shadow);
  text-transform: uppercase;
  letter-spacing: .1em;
  white-space: nowrap;
}

@media (max-width: 1180px) { .hero-fox { right: -10px; width: 210px; } }
@media (max-width: 980px)  { .hero-fox { display: none; } }

/* ---- restored: chips lost when the side-by-side hero block was removed ---- */
.hero-fox .livechip i { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
@media (prefers-reduced-motion: no-preference) {
  .hero-fox img { animation: bob 5.5s 1.4s ease-in-out infinite; }
  .hero-fox .livechip i { animation: blip 1.8s ease-in-out infinite; }
}

/* the fox was colliding with the headline - drop it beside the buttons instead */

@media (max-width: 1240px) { .hero-fox { right: -12px; width: 196px; } }
@media (max-width: 1080px) { .hero-fox { display: none; } }

/* a change on a headline this size needs to be unmissable */
@keyframes roll {
  0%   { transform: translateY(72%) scale(.86); opacity: 0; filter: blur(3px); }
  55%  { filter: blur(0); }
  75%  { transform: translateY(-6%) scale(1.05); }
  100% { transform: translateY(0) scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: no-preference) {
  .roll { animation: roll .58s cubic-bezier(.16,.9,.28,1); }
}
h1 .num { padding: 0 .04em; }

/* ---- full-width bar, constrained content ---- */
header.topbar .nav { margin: 0 auto; }

/* ============================================================
   Section tiers

   Every section used to sit on the same cream at the same elevation, with a
   ruled line drawn between them. That reads as one long surface with marks on
   it - scrolling never feels like arriving anywhere. Material does the work the
   hairline was standing in for: each section is a full-bleed surface, and no two
   neighbours share one, so a boundary is a change of room.

   Three tiers only; more would be noise.
     · page   - the cream the site is built on
     · sunk   - recessed, for the sections you look *into* (demos, proof)
     · raised - a white plane above the page, for the ones you act on
   ============================================================ */

.tier { position: relative; padding-block: 40px; }
.tier > .wrap { position: relative; z-index: 1; }

.tier-sunk {
  background: var(--sunk);
  /* a bright top edge over a soft inner shadow: light catching the lip of a
     recess, which is what says "below the page" rather than "above it" */
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .7),
              inset 0 14px 26px -18px rgb(120 60 20 / .10);
}

.tier-raised {
  background: var(--card);
  border-block: 1px solid var(--line);
}

/* Every tier gets the same band of its own material around the content, so the
   distance from a boundary to the nearest thing you can read is identical at
   every boundary on the page. Putting this on the sunk and raised tiers only
   made the rhythm 0 / 22 / 23 / 0 - a separation you can measure a difference
   in is a separation that reads as a mistake. */

/* the eyebrow labels the room you have just walked into */
.tier > .wrap > section > .kicker,
/* the reviews section inverts the nesting - .wrap sits inside it, so the
   marquees can escape the measured column and run full width */
.tier > section > .wrap > .kicker { display: block; margin-bottom: 14px; }

/* Dim to focus: the section nearest the middle of the viewport carries its wash
   at full strength while its neighbours let theirs go, so attention lands on one
   room at a time. Text opacity is deliberately untouched - dimming copy would
   break contrast, and the goal is emphasis, not obscurity. */
.tier .band::before, .tier .wash, .tier .pc-wash { opacity: var(--focus, .5); }
@media (prefers-reduced-motion: no-preference) {
  .tier .band::before, .tier .wash, .tier .pc-wash { transition: opacity .9s ease; }
}
/* no JS: every wash sits at a sensible middle rather than half-off */
.no-focus .tier .band::before, .no-focus .tier .wash, .no-focus .tier .pc-wash { opacity: .85; }

/* ---- radial washes between every section, so the tint has depth ---- */

.band { position: relative; }
.band::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  left: 50%; transform: translateX(-50%);
  /* a fixed 1240px pushed the document 425px sideways on a 390px phone -
     the washes are decorative, so they cap to the viewport */
  width: min(1240px, calc(100vw - 16px)); height: 620px; top: -120px;
  filter: blur(34px); opacity: .8;
}
.band-a::before { background: radial-gradient(40% 44% at 22% 40%, #FFE3D0, transparent 66%),
                              radial-gradient(36% 40% at 78% 56%, #E9EEFF, transparent 68%); }
.band-b::before { background: radial-gradient(40% 44% at 78% 34%, #FFE9F2, transparent 66%),
                              radial-gradient(34% 38% at 18% 62%, #FFF0DC, transparent 68%); }
.band-c::before { background: radial-gradient(44% 46% at 50% 40%, #E7F5EC, transparent 68%),
                              radial-gradient(32% 34% at 84% 70%, #FFE6D8, transparent 66%); }

/* ---- the measured chapter ----

   The only section on the page carrying telemetry rather than copy, and as a
   pale band between two other pale bands it was the easiest thing here to
   scroll straight past. It is now a dark chapter in Scarcity Timer's blue -
   the same device the Shopify and Effectify chapters use - so the page's one
   piece of hard evidence stops the scroll instead of blending into it.

   Built to read as a readout: mono figures, a real distribution rather than a
   single cherry-picked percentage, and the scope printed underneath at a size
   you can actually read. */

.tier-measured {
  background:
    radial-gradient(56% 60% at 16% 6%, #1E4BC4 0%, transparent 58%),
    radial-gradient(48% 54% at 90% 94%, #14225E 0%, transparent 60%),
    #070C1C;
  overflow: hidden;
  --ink: #FFFFFF;
  --ink-2: #BFCDF2;
  --mute: #93A2CC;
  --card: rgb(255 255 255 / .07);
  --line: rgb(255 255 255 / .16);
  --sunk: rgb(0 0 0 / .34);
  color: var(--ink);
}
/* same film grain the other two dark chapters carry, so they read as a set */
.tier-measured::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .22;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}
.tier-measured::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  left: -6%; top: -30%; width: 42%; aspect-ratio: 1;
  border-radius: 50%; background: radial-gradient(circle, rgb(92 140 255 / .20), transparent 62%);
}
.tier-measured .kicker { color: #8FB0FF; }
.tier-measured h2 { color: #fff; }
.tier-measured .body b { color: #fff; }

.measured { --ms: #6E9BFF; }
.measured h2 {
  font-size: clamp(34px, 4.4vw, 54px); letter-spacing: -.035em; line-height: 1.04;
}
.measured .body { font-size: 18px; max-width: 60ch; }

.measured .ms-grid {
  display: grid; grid-template-columns: minmax(0, .58fr) minmax(0, 1fr);
  gap: 48px; align-items: center;
  margin-top: 38px; padding: 36px 40px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 20px;
  backdrop-filter: blur(4px);
}

.ms-figure { display: flex; flex-direction: column; gap: 12px; }
.ms-n {
  font-family: var(--mono); font-weight: 600; font-size: clamp(64px, 8vw, 104px);
  line-height: .88; letter-spacing: -.055em; color: #fff;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 34px rgb(110 155 255 / .55);
}
.ms-n i {
  font-style: normal; font-size: .38em; letter-spacing: -.02em; margin-left: 3px;
  color: var(--ms);
}
.ms-l { font-size: 15px; color: var(--ink-2); line-height: 1.4; max-width: 22ch; }

.ms-bars { list-style: none; margin: 0; padding: 0; display: grid; gap: 17px; }
.ms-bars li {
  display: grid; grid-template-columns: 15ch minmax(0, 1fr) 4.5ch;
  align-items: center; gap: 16px;
}
.ms-bar-l { font-size: 15px; color: var(--ink-2); }
.ms-track {
  display: block; height: 12px; border-radius: 999px;
  background: var(--sunk); overflow: hidden;
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .07);
}
/* width is set inline from the measured share, so the bar cannot drift from
   the number printed beside it */
.ms-track i {
  display: block; height: 100%; border-radius: inherit;
  background: linear-gradient(90deg, #4C7DF0, #8FB0FF);
  box-shadow: 0 0 16px rgb(110 155 255 / .55);
}
.ms-pct {
  font-family: var(--mono); font-size: 16.5px; font-weight: 600;
  color: #fff; text-align: right; font-variant-numeric: tabular-nums;
}

.ms-note {
  margin: 20px 0 0; font-size: 13px; line-height: 1.55;
  color: var(--mute); max-width: 74ch;
}

@media (max-width: 820px) {
  .measured .ms-grid { grid-template-columns: 1fr; gap: 28px; padding: 26px 22px; }
  .ms-bars li { grid-template-columns: 13ch minmax(0, 1fr) 4.5ch; gap: 12px; }
  .ms-bar-l { font-size: 14px; }
  .ms-pct { font-size: 15px; }
}

@media (prefers-reduced-transparency: reduce) {
  .measured .ms-grid { background: rgb(255 255 255 / .12); backdrop-filter: none; }
}

/* ---- app icons ---- */

.appsrow { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin: 0 0 44px; }
.appcard {
  display: flex; align-items: center; gap: 14px; text-decoration: none;
  background: var(--card); border: 1px solid var(--line); border-radius: 16px;
  padding: 16px 18px; box-shadow: var(--shadow);
}
.appcard:hover { border-color: var(--ink-2); }
.appcard img { width: 52px; height: 52px; border-radius: 13px; flex: 0 0 52px; }
.appcard .ac-t { display: flex; flex-direction: column; min-width: 0; }
.appcard .ac-n { font-weight: 600; font-size: 16px; letter-spacing: -.02em; }
.appcard .ac-m { font-family: var(--mono); font-size: 11.5px; color: var(--ink-2); }
@media (max-width: 760px) { .appsrow { grid-template-columns: 1fr; } }

/* the scroller kicker carries the app's real icon */
.kicker.with-icon { display: inline-flex; align-items: center; gap: 10px; }
.kicker.with-icon img { width: 26px; height: 26px; border-radius: 7px; }

/* ============================================================
   Audience tabs
   ============================================================ */

.tabs { display: flex; flex-wrap: wrap; gap: 8px; margin: 40px 0 26px; }
.tab {
  font: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
  background: transparent; color: var(--ink-2); border: 1px solid transparent;
  padding: 10px 18px; border-radius: 999px;
}
.tab:hover { color: var(--ink); }
.tab.on { background: var(--card); color: var(--ink); border-color: var(--line); box-shadow: var(--shadow); }

.panes { position: relative; }
.pane { display: none; }
.pane.on { display: block; }
.pane h3 { font-size: 24px; margin: 0 0 12px; }
.pane p { color: var(--ink-2); max-width: 58ch; margin: 0 0 20px; }
.pane .checks { grid-template-columns: repeat(3, 1fr); }
.pane .checks svg { color: var(--accent-text); }
@media (max-width: 820px) { .pane .checks { grid-template-columns: 1fr; } }

/* ============================================================
   Integrations
   ============================================================ */

.integrations {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px;
  background: var(--line); border: 1px solid var(--line);
  border-radius: 18px; overflow: hidden; margin-top: 42px;
}
.integ { background: var(--card); padding: 24px 22px; }
.ig-n { display: block; font-weight: 600; font-size: 16.5px; letter-spacing: -.02em; }
.ig-m { display: block; font-size: 14px; color: var(--ink-2); margin-top: 6px; }
/* Same fix as .stats above: an unpaired last tile starting a new row leaves
   empty cells beside it, so it takes the whole row instead. */
.integrations > .integ:last-child:nth-child(3n+1) { grid-column: 1 / -1; }
@media (max-width: 860px) {
  .integrations { grid-template-columns: 1fr 1fr; }
  .integrations > .integ:last-child:nth-child(3n+1) { grid-column: auto; }
  .integrations > .integ:last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@media (max-width: 560px) { .integrations { grid-template-columns: 1fr; } }

/* ============================================================
   Ask an assistant
   ============================================================ */

.askblock { padding-top: 0; }
.ask {
  background: var(--card); border: 1px solid var(--line); border-radius: 22px;
  padding: 44px 46px 46px; box-shadow: var(--shadow); text-align: center;
}
.ask h2 { max-width: 26ch; margin-inline: auto; }
.ask > p { color: var(--ink-2); max-width: 54ch; margin: 0 auto 24px; }
.askq {
  margin: 0 auto 26px; max-width: 62ch; text-align: left;
  background: var(--sunk); border-radius: 14px; padding: 20px 22px;
  font-size: 16px; line-height: 1.55; color: var(--ink);
}
.askq::before { content: "“"; } .askq::after { content: "”"; }
.ask .btnrow { justify-content: center; }

/* ---- scroll previews, made clearer ----
   The frame shared its row with the copy and rendered small. Give it more of
   the width and scale the UI inside it up so the widget is legible. */
.scroll-stage .tag { font-size: 18px; }
.scroll-stage .widget .num { font-size: 20px; }
.scroll-stage .bar-top { padding: 15px 20px; }
.scroll-stage .bar-top .url { font-size: 13px; }
.scroll-stage .co-main { padding: 30px 32px; }
.scroll-stage .co-banner { font-size: 14px; }
.scroll-stage .co-side { padding: 28px 24px; }

@media (max-width: 1180px) {
  .scroller { max-width: 100%; }
  .scroll-grid { grid-template-columns: minmax(0,1.3fr) minmax(0,.7fr); gap: 34px; }
}

/* These widths came after the mobile collapse at 900px and quietly re-imposed
   two columns on phones - the copy column ended up 102px wide and the checkout
   mockup pushed the whole document 425px sideways. Re-assert the collapse. */
@media (max-width: 900px) {
  .scroll-grid { grid-template-columns: minmax(0, 1fr); gap: 24px; }
  /* same story: a later unconditional rule put the checkout back to two columns */
  .checkout { grid-template-columns: 1fr; }
  .co-side { border-left: 0; border-top: 1px solid #E1E1E1; }
  /* the glow is deliberately wider than its parent, which is fine on desktop
     and a sideways scrollbar on a phone */
  .stagewrap .wash { display: none; }
}

/* auto-fit gave 3 columns then a lone 4th cell; 2×2 always fills */

/* ============================================================
   Feature panels

   Loox's pattern: big full-bleed rounded blocks in solid saturated
   colour, one idea each, copy one side and a real preview the
   other, alternating and revealing as you scroll. Every colour is
   set per panel from the data, so adding a fourth is a dict entry.
   ============================================================ */


.panel-big {
  background: var(--pbg); color: var(--pfg);
  border-radius: 28px; padding: 38px 44px 38px;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, .9fr);
  gap: 52px; align-items: center; overflow: hidden;
  position: relative;
  /* every card the same height, or the tallest one leaves a stray strip poking
     out below whichever card is covering it. Matches .js .deck-stage's height
     below - measured against the tallest real card content, not guessed. */
  min-height: 540px;
  box-shadow: 0 -2px 0 rgb(255 255 255 / .06), 0 -30px 60px -30px rgb(30 12 4 / .5);
}

/* a soft light bloom inside the panel so the flat colour has depth */
.panel-big::before {
  content: ""; position: absolute; inset: -30% -10% auto -20%; height: 130%;
  background: radial-gradient(46% 44% at 26% 34%, rgba(255,255,255,.16), transparent 66%);
  pointer-events: none;
}
.panel-big > * { position: relative; }

.pb-kick {
  display: inline-flex; align-items: center; gap: 10px;
  font-size: 14px; font-weight: 600; color: var(--pdim); margin-bottom: 10px;
}
/* the real app icons carry their own dark artwork, so on a dark panel they need
   a light plate under them to read at all */
.pb-kick img {
  width: 28px; height: 28px; border-radius: 8px;
  background: #fff; box-shadow: 0 0 0 1px rgb(0 0 0 / .06);
}

.panel-big h3 {
  font-size: clamp(24px, 2.8vw, 34px); line-height: 1.1; letter-spacing: -.032em;
  font-weight: 700; margin: 0 0 10px; color: var(--pfg); max-width: 20ch;
}
.panel-big p { color: var(--pdim); font-size: 16px; line-height: 1.5; max-width: 46ch; margin: 0 0 14px; }

.pchecks { list-style: none; margin: 0 0 16px; padding: 0; display: grid; gap: 7px; }
.pchecks li { display: flex; align-items: flex-start; gap: 11px; font-size: 14.5px; color: var(--pfg); }
.pchecks svg { width: 17px; height: 17px; flex: 0 0 17px; opacity: .9; margin-top: .32em; }

/* Was an underline in --pfg - each panel's own body-text colour, so the one
   link that actually installs something read as the same weight as the copy
   above it.

   A plain orange outline does not survive contact with these panels: measured
   against the honest-green card it is 2:1, nowhere near readable, and against
   the peach card it is 3:1 - both below the 4.5:1 body text needs. Plain
   --accent on white is only 4.04:1, just under, too. So the border sits on a
   white chip rather than straight on the panel colour, which is worth doing
   whether a panel is dark, light, or (as here) both at once: the button reads
   identically everywhere instead of degrading on whichever card it lands on. */
.pb-link {
  display: inline-flex; align-items: center; gap: 9px; text-decoration: none;
  color: var(--accent-text); font-weight: 600; font-size: 15.5px;
  padding: 12px 22px; border-radius: 999px;
  background: #fff; border: 1.5px solid var(--accent-text);
  box-shadow: 0 6px 18px -8px rgb(0 0 0 / .28);
  transition: background .16s ease, color .16s ease, gap .16s ease, transform .12s ease-out;
}
.pb-link:hover { background: var(--accent); border-color: var(--accent); color: #fff; gap: 13px; }
.pb-link:active { transform: scale(.975); }

/* These install buttons only ever show up on the app cards, at a comfortable
   card width - never squeezed into a 320px nav or hero button - so, unlike
   the site's other CTAs, the full "Install X free" label always fits and
   always shows, matching desktop instead of falling back to the short
   "Boost your sales" label below 420px. */
.pb-link .cta-lg { display: inline; }
.pb-link .cta-sm { display: none; }

/* The full label is what wins the width fight on a card this size normally,
   but the deck cards themselves shrink well below 400px (32px of .wrap side
   padding plus the card's own padding eats fast), and "Install Low Stock
   Counter free" is the longest of the three - so it needs a smaller button
   to keep fitting once the short fallback above is off the table. */
@media (max-width: 400px) {
  .panel-big .pb-link { padding: 9px 12px; font-size: 12px; gap: 5px; }
  .panel-big .pb-link .cta-arw { display: none; }
}

/* ---- the visual side ---- */

.pb-visual {
  position: relative;
  display: flex;
  justify-content: center;
  padding-bottom: 0;
  align-items: flex-end;
  gap: 4px;
}

.mini {
  width: 100%;
  max-width: 300px;
  background: #fff;
  color: #16181A;
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 3px 8px rgba(0,0,0,.10), 0 28px 50px -22px rgba(0,0,0,.45);
  flex: 1 1 300px;
}
.mini-bar { display: flex; gap: 6px; padding: 12px 14px; border-bottom: 1px solid #ECECEC; }
.mini-bar i { width: 8px; height: 8px; border-radius: 50%; background: #E2E2E2; }
.mini-body { padding: 18px; display: flex; flex-direction: column; gap: 11px; }
.mini-shot {
  display: block; aspect-ratio: 16/10; border-radius: 10px;
  background: linear-gradient(150deg, #F2F2F0, #FAFAF8);
  border: 1px solid #EEEEEC;
}
.mini-name { font-weight: 600; font-size: 16px; letter-spacing: -.02em; }
.mini-tag { font-family: var(--mono); font-size: 14px; color: #5A6165; }
.mini .widget { border-top: 1px solid #EFEFED; padding: 11px 0; font-size: 14.5px; color: #5A6165; }
.mini .widget .num { font-size: 16px; }
.mini-cta {
  margin-top: 3px; border-radius: 9px;
  background: transparent;
  color: var(--app-accent, #16181A);
  border: 1.5px solid currentColor;
  padding: 11px; text-align: center; font-weight: 600; font-size: 14.5px;
}

.pb-fox {
  position: static;
  bottom: -14px;
  right: -56px;
  width: 124px;
  height: auto;
  filter: drop-shadow(0 14px 18px rgba(0,0,0,.26));
  flex: 0 0 124px;
  margin-bottom: -18px;
  transform: none;
}

/* ---- reveal on scroll ---- */

.reveal { opacity: 0; transform: translateY(26px) scale(.985); }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: no-preference) {
  .reveal { transition: opacity .7s cubic-bezier(.2,.8,.25,1), transform .8s cubic-bezier(.2,.8,.25,1); }
}
@media (prefers-reduced-motion: reduce) { .reveal { opacity: 1; transform: none; } }
.no-js-reveal .reveal { opacity: 1; transform: none; }

@media (max-width: 900px) {
  .panel-big { grid-template-columns: 1fr; gap: 34px; padding: 34px 26px 40px; border-radius: 22px; }
  .pb-fox { width: 112px; bottom: -20px; right: -6px; }

  /* On a phone the live widget mock reads small and easy to miss - the
     install button is the clearer, more actionable thing to leave on
     screen, so the mock (and the fox beside it) drop here instead. */
  .pb-visual { display: none; }
}

/* the fox was sitting over the Add to cart button - push it clear of the card */

/* ---- fox beside the card, not over it ----
   Absolute positioning against a centred card kept clashing with the CTA at
   every width. As a flex sibling the overlap is structurally impossible. */

@media (max-width: 1240px) { .pb-fox { width: 104px; flex-basis: 104px; } }
@media (max-width: 900px)  { .pb-fox { width: 92px; flex-basis: 92px; } }
@media (max-width: 560px)  { .pb-fox { display: none; } }

/* ---- restored: .checks lost with the old .feat block ----
   Without a size the check SVG expanded to 976×976. */

.checks { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 10px 26px; }
.checks li { display: flex; align-items: flex-start; gap: 10px; font-size: 15.5px; }
.checks svg { width: 16px; height: 16px; flex: 0 0 16px; margin-top: .3em; }
@media (max-width: 760px) { .checks { grid-template-columns: 1fr; } }

/* ============================================================
   The card deck

   Built on the same mechanic as the app previews: the stage is
   pinned so the surrounding page holds still, and zero-content
   markers scrolling past it drive which card is live. Only the
   cards move - the live one slides off to the left and vanishes,
   and the card waiting below rises into the space it left.

   With no JS (or reduced motion) the cards simply unstack.
   ============================================================ */

/* ---- the deck's cover slide ----
   Not a coloured card: the section heading and the three app cards, which then
   slide away exactly like the rest instead of being left behind above a pinned
   stage. Transparent, centred, and carrying its own wash. */

.panel-cover {
  /* An opaque surface, not a transparent block. Transparent, the cards waiting
     behind it showed straight through, and fading them out instead meant the
     incoming card was see-through for the length of its own fade. A real card
     face solves both and keeps the deck consistent. */
  background: var(--card); color: var(--ink);
  text-align: center;
  /* .panel-big is a two-column grid; the cover is one full-width column, or the
     copy crams into 58% and the app names wrap */
  grid-template-columns: 1fr; gap: 0;
}
.panel-cover .pc-in { width: 100%; margin: 0 auto; position: relative; z-index: 1; }
.panel-cover h2 { margin-top: 0; }
.panel-cover .body { margin-inline: auto; }
.panel-cover .appsrow { margin-bottom: 0; text-align: left; }
/* the wash lives inside the card now, clipped by its own rounded corners */
.pc-wash {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(42% 46% at 22% 24%, #FFE3D0, transparent 68%),
              radial-gradient(38% 42% at 80% 76%, #E9EEFF, transparent 70%);
  opacity: .85;
}
.pc-cue {
  display: none; margin-top: 30px; font-size: 14px; color: var(--mute);
  letter-spacing: .01em;
}
.pc-cue i { font-style: normal; margin-left: 6px; }

/* ---- the base state is no deck at all ----
   Three cards in a plain column. That is what a phone gets, what reduced
   motion gets, and what anyone without JS gets - no piled-up sticky stage
   waiting for a script that never ran. */

.deck { position: relative; margin: 22px 0 40px; }
.deck-stage { display: flex; flex-direction: column; gap: 22px; }
.deck-mark, .deck-dots, .pb-shade { display: none; }

/* The section title. It pins together with the stage so it holds for all four
   slides - previously it lived inside the first card and scrolled away with it,
   which left three cards with nothing saying what section you were in. */
.deck-head { margin-bottom: 14px; }
.deck-head h2 { margin: 0; }
.deck-head .kicker { display: block; margin-bottom: 8px; }

.pb-num {
  position: absolute; top: 26px; right: 30px; z-index: 2;
  font-family: var(--mono); font-size: 13px; letter-spacing: .06em;
  color: var(--pfg); opacity: .5;
}
.pb-num i { font-style: normal; margin: 0 3px; opacity: .6; }

/* ---- pinned deck ----
   Only where there is room for it, motion is welcome, and JS confirmed it can
   drive the thing (html.js is set inline in <head>, and site.js drops it again
   if it cannot wire up). */

/* The height gate matters as much as the width one: the pinned block is the
   title plus a 540px stage under an ~83px nav, needing a window taller than
   ~760px or the last card is cut off by the fold. That number used to be 880,
   sized for a roomy monitor - which meant a maximized browser on an actual
   14" MacBook (long since fully within "desktop", not the phone/tablet split
   this gate exists for) failed it too, since a real browser window's chrome
   eats 80-100px before any page content is counted. Tightened the deck's own
   padding and type scale to fit a shorter stage, rather than just lowering the
   number and letting the last card run past the fold again. Shorter windows
   still get the plain column, which stays the honest fallback for anything
   this doesn't fit - a stage that doesn't fit isn't a stage.

   Deliberately NOT gated on prefers-reduced-motion. macOS Reduce Motion is on
   for plenty of people (it was on here), and gating the whole mechanic on it
   meant they never saw the cards change at all. The deck still works; the
   travel is dropped further down instead. */
@media (min-width: 901px) and (min-height: 760px) {
  /* title and stage pin as one block */
  .js .deck-pin {
    position: sticky; top: calc(var(--navh) + 16px);
  }
  .js .deck-stage {
    display: block;
    position: relative;
    height: 540px;
  }

  /* one marker's worth of scroll per card, plus a beat before the first */
  .js .deck-mark { display: block; height: 86vh; }
  .js .deck-mark:first-of-type { height: 46vh; }

  /* Position and compositing only. Every transform, opacity and z-index below
     is written per frame from the scroll position in site.js - there is no
     transition here on purpose, because a transition would fight the scroll
     and make the motion uninterruptible. */
  .js .panel-big {
    position: absolute; inset: 0;
    transform-origin: center;
    will-change: transform, opacity;
    backface-visibility: hidden;
    pointer-events: none;
  }
  .js .panel-big.is-live { pointer-events: auto; }

  /* guard for the frame before the first paint: without it the last card in the
     source order would flash on top of the stack */
  .js .deck-stage .panel-big + .panel-big { opacity: 0; }

  /* fog toward the page colour, not black - black over the pink card turned it
     to mud, and receding into the background reads as distance */
  .js .pb-shade {
    display: block; position: absolute; inset: 0; border-radius: inherit;
    pointer-events: none; background: var(--bg); opacity: 0; z-index: 3;
  }

  .js .deck-dots {
    display: flex; gap: 8px; z-index: 50;
    position: absolute; bottom: -32px; left: 50%; transform: translateX(-50%);
  }
  .js .deck-dots i {
    width: 26px; height: 4px; border-radius: 3px; background: var(--line);
    transition: background .35s ease, width .35s ease;
  }
  .js .deck-dots i.on { background: var(--accent); width: 40px; }

  /* the cover is centred in the stage rather than filling it, and only shows
     its wash and its scroll cue once it is actually a slide */
  .js .panel-cover { display: grid; align-content: center; }
  .js .pc-cue { display: block; }
}

/* the fixed 620px only exists to keep the pinned cards identical in size */
@media (max-width: 900px) { .panel-big { min-height: 0; } }

/* ---- the same pin, on a phone ----

   A side-swiping carousel lived here first - a different gesture from the
   desktop version, on the theory that there was no room to scrub vertically
   on a phone. There wasn't, at full size: at 375px wide, one card's text plus
   its full product mock ran to over 1000px. But the actual ask was the exact
   desktop animation - the page scrolling normally while the deck holds still
   and the cards change underneath - and a carousel doesn't do that at any
   size, it does something else. Wrong fix for the constraint.

   The constraint itself only demanded a stage shorter than one phone screen,
   not a small stage. Dropping the per-card button (redundant here - the
   sticky "Boost your store" pill already does that job), shrinking the
   product mock to a labelled thumbnail, and trimming the checklist to three
   items (see the comment further down on measuring this in place rather than
   on a clone) brings the tallest of the four cards to 557px. That is the
   same mechanism as the desktop block above - same .js .deck-pin /
   .js .deck-stage / .js .panel-big, same site.js paint() loop, nothing new to
   write - just a shorter stage and lighter cards under it.

   Gated on min-height too, same as the desktop block - but 700px, and every
   number below it, was calibrated against a phone's FULL screen height, which
   is not what a phone actually gives a page. A real iPhone 14 Pro (852px
   tall, full screen) reported window.innerHeight as 695px with Safari's
   address bar and its own bottom toolbar both on screen - the ordinary,
   default state, not an edge case. Every browser-automation tool used to
   build this reports the full 852px with zero chrome deducted, which is why
   every measurement up to this point was wrong by ~150px on a real device
   and passed anyway: nothing in this codebase's own testing tools could see
   the gap. Rebuilt against the measured 695px, with the gate lowered to
   match. */
@media (max-width: 900px) and (min-height: 640px) {
  .js .deck-pin { position: sticky; top: calc(var(--navh) + 2px); }
  .js .deck-stage { display: block; position: relative; height: 360px; }

  /* The section heading sits inside the same pinned block and was never cut
     when the cards were - at a real 695px it was ~14% of the entire budget on
     its own. Same content, smaller. */
  .js .deck-head { margin-bottom: 6px; }
  .js .deck-head .kicker { margin-bottom: 4px; }
  .js .deck-head h2 { font-size: 19px; line-height: 1.15; }

  .js .deck-mark { display: block; height: 78vh; }
  .js .deck-mark:first-of-type { height: 42vh; }

  .js .panel-big {
    position: absolute; inset: 0;
    transform-origin: center;
    will-change: transform, opacity;
    backface-visibility: hidden;
    pointer-events: none;
  }
  .js .panel-big.is-live { pointer-events: auto; }
  .js .deck-stage .panel-big + .panel-big { opacity: 0; }

  .js .pb-shade {
    display: block; position: absolute; inset: 0; border-radius: inherit;
    pointer-events: none; background: var(--bg); opacity: 0; z-index: 3;
  }

  .js .deck-dots {
    display: flex; gap: 6px; z-index: 50;
    position: absolute; bottom: -24px; left: 50%; transform: translateX(-50%);
  }
  .js .deck-dots i {
    width: 18px; height: 4px; border-radius: 3px; background: var(--line);
    transition: background .35s ease, width .35s ease;
  }
  .js .deck-dots i.on { background: var(--accent); width: 26px; }

  .js .panel-cover { display: grid; align-content: center; }
  .js .pc-cue { display: block; }

  /* ---- a summary, not the whole card ----
     370px was the real number: 695px measured, minus ~165px for nav and the
     (now-shrunk) heading above, minus ~136px for Crisp and Sienna's reserved
     zone at the bottom, minus a margin of error. Three checks and a full
     paragraph fit 557px - genuinely does not fit 370px. But the first pass
     through this budget landed at ~245px of real content against a 320px
     stage, leaving a chunk of the actual 370px ceiling unused out of
     over-caution. Reopened to use closer to the real number: three lines of
     the paragraph instead of two, two checks instead of one, the price back
     on the mock. Measured in place again below, not assumed.

     align-items:start still matters here for the same reason noted on the
     desktop rule above - grid centers each stacked row inside whatever extra
     space text-vs-space math leaves, and this box still doesn't have much
     to spare even at the larger size. */
  .js .panel-big { align-items: start; gap: 10px; padding: 14px 18px 16px; }
  .js .panel-big .pb-kick { margin-bottom: 6px; }
  .js .panel-big h3 { font-size: 19px; line-height: 1.2; margin-bottom: 6px; }
  .js .panel-big p {
    font-size: 13.5px; line-height: 1.35; margin-bottom: 8px;
    display: -webkit-box; -webkit-box-orient: vertical;
    -webkit-line-clamp: 3; overflow: hidden;
  }
  .js .panel-big .pchecks { gap: 4px; margin-bottom: 0; }
  .js .panel-big .pchecks li { font-size: 12.5px; }
  .js .panel-big .pchecks li:nth-child(n+3) { display: none; }

  /* The live widget mock is small and easy to miss at a glance on a phone -
     the install button is the clearer, more actionable thing to show in its
     place, so the mock (and the fox beside it) are dropped here instead of
     shrunk to a thumbnail. .pchecks lost its bottom margin above for the same
     budget reasons noted there, which left the button crowding the last
     checklist line now that it is visible again. */
  .js .panel-big .pb-visual { display: none; }
  .js .panel-big .pb-link { margin-top: 12px; }

  /* The cover slide is the fourth card in this stage and was still running
     434px against the same 320px box - its .appsrow switches to one column
     per app below 760px (a rule for the flat fallback further up this file,
     which this section no longer uses once pinned), stacking three cards that
     read fine as a full-width column but not inside a box this short. Forced
     back to a row of three, much smaller, so it is proof-of-three-apps at a
     glance rather than a fourth thing to read. */
  .js .panel-cover h3 { font-size: 17px; margin-bottom: 6px; }
  .js .panel-cover .body { font-size: 12.5px; line-height: 1.3; margin-bottom: 10px; }
  .js .panel-cover .appsrow { grid-template-columns: repeat(3, 1fr); gap: 6px; margin-bottom: 0; }
  .js .panel-cover .appcard {
    flex-direction: column; text-align: center; gap: 4px; padding: 8px 4px;
  }
  .js .panel-cover .appcard img { width: 26px; height: 26px; flex-basis: 26px; border-radius: 8px; }
  .js .panel-cover .ac-n { font-size: 10px; line-height: 1.15; }
  .js .panel-cover .ac-m { display: none; }
  .js .panel-cover .pc-cue { display: none; }
}

/* The slide is the section - it does not read the OS motion setting at all.
   Deliberate call by the owner: no prefers-reduced-motion rule may touch
   .panel-big, so don't add one back. */

/* ---- which way the fox faces ----
   Direction is data now, not an assumption. `data-facing` carries which way the
   drawing points and each slot mirrors only when its position disagrees:

     .pb-fox            sits RIGHT of the mini card  -> must point LEFT
     .foxes .fx         sits LEFT, inside the frame  -> must point RIGHT
     .cta-banner .fox   sits bottom-RIGHT            -> must point LEFT

   The stand-in art faces left and the redrawn art faces right, so the two sets
   mirror in opposite slots and the art can be swapped with no code change.

   Mirroring uses the independent `scale` property rather than `transform`,
   because the scroller's foxes animate `transform` on entry and a flip written
   there would be overwritten by it. */
.pb-fox[data-facing="right"] { scale: -1 1; }
.cta-banner .fox[data-facing="right"] { scale: -1 1; }
.foxes .fx[data-facing="left"] { scale: -1 1; }

/* ---- clearer separation between sections ---- */
/* A full-bleed hairline cut straight across the page and sliced every section's
   radial wash in half. Inset it to the content width and fade both ends, so it
   reads as a break in the copy rather than a line ruled over the artwork. */
.sep {
  border: 0; height: 1px; opacity: .85;
  width: min(1180px, calc(100% - 48px)); margin: 0 auto;
  background: linear-gradient(90deg,
    transparent, var(--line) 14%, var(--line) 86%, transparent);
}

/* ============================================================
   Chapters - two deliberate interruptions in the rhythm

   Eight sections of cream, however well spaced, still read as one voice. These
   two sections are the ones that earn a change of voice: the Shopify chapter,
   because the platform is the credential, and the Effectify chapter, because it
   is genuinely a different company's story.

   Both work by overriding the tokens *inside the tier* rather than restyling
   components. Tabs, checks, cards and headings then invert on their own, which
   is why these are ~40 lines rather than 400.
   ============================================================ */

/* film grain, so a large flat field reads as a surface rather than a fill */
.tier-shopify::after, .tier-ink::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .22;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}

/* ---- the Shopify chapter ---- */

.tier-shopify {
  /* Shopify's own dark green, with their brighter green as the light source */
  background:
    radial-gradient(58% 62% at 18% 12%, #00A87E 0%, transparent 58%),
    radial-gradient(50% 55% at 88% 88%, #00694F 0%, transparent 60%),
    #004C3F;
  overflow: hidden;
  --ink: #FFFFFF;
  --ink-2: #C6E5DA;
  --mute: #AFD8CB;
  --card: rgb(255 255 255 / .09);
  --line: rgb(255 255 255 / .22);
  --sunk: rgb(0 0 0 / .18);
  --accent-text: #8FE3C4;
  color: var(--ink);
}
.tier-shopify .kicker { color: #8FE3C4; }
.tier-shopify h2, .tier-shopify h3 { color: #fff; }
.tier-shopify .tab { color: #DCEFE8; }
.tier-shopify .tab.on { background: #fff; color: #00352C; border-color: #fff; }
.tier-shopify .checks svg { color: #8FE3C4; }

/* Their glyph is deliberately NOT used as a watermark here. Shopify's guidelines
   allow the Built for Shopify badge as a credential and discourage decorative use
   of the logo - so the credential is the badge, and the depth comes from light. */
.tier-shopify::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  right: -8%; bottom: -34%; width: 46%; aspect-ratio: 1;
  border-radius: 50%; background: radial-gradient(circle, rgb(143 227 196 / .18), transparent 62%);
}

/* The cream section washes belong to the page, not to a chapter - over green
   they read as smudges, and one of them sat behind the lede and ate its contrast. */
.tier-shopify .band::before, .tier-ink .band::before { display: none; }
.shopify-note {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  margin-top: 38px; padding-top: 28px; border-top: 1px solid rgb(255 255 255 / .18);
  color: var(--ink-2); font-size: 15px;
}
.shopify-note img { height: 34px; width: auto; }

/* ---- the Effectify chapter ---- */

.tier-ink {
  background:
    radial-gradient(52% 58% at 14% 0%, #3A0A1B 0%, transparent 62%),
    radial-gradient(46% 52% at 92% 100%, #2A0713 0%, transparent 58%),
    #0B0C0D;
  overflow: hidden;
  --ink: #F4F2F1;
  --ink-2: #B7B2AF;
  --mute: #A39E9B;
  --card: rgb(255 255 255 / .06);
  --line: rgb(255 255 255 / .14);
  color: var(--ink);
}
/* the tier is the surface now, so the block inside stops being its own card */
.tier-ink .pb { background: none; box-shadow: none; border: 0; padding: 0; }
.tier-ink .pb-glow { display: none; }
.tier-ink .pb-figs > div { background: rgb(255 255 255 / .05); border-color: rgb(255 255 255 / .10); }

/* ---- the closing banner ----
   Deliberately overlaps the boundary below it: a block that crosses a seam ties
   the two surfaces together and stops the page ending on a whimper. */

.cta-banner {
  position: relative; z-index: 2;
  margin: 0 auto -84px; max-width: 1016px;
  padding: 62px 48px 66px; border-radius: 30px; text-align: center;
  background: radial-gradient(64% 120% at 50% -20%, #FF6A3D 0%, transparent 62%), var(--accent);
  color: var(--accent-ink);
  box-shadow: 0 30px 60px -28px rgb(200 60 20 / .55);
  overflow: hidden;
}
.cta-banner h2 { color: var(--accent-ink); margin: 0 0 12px; max-width: 22ch; margin-inline: auto; }
.cta-banner p { color: rgb(26 10 4 / .78); margin: 0 auto 28px; max-width: 46ch; font-size: 17.5px; }
.cta-banner .btn-fill { background: #fff; color: #16181A; }
.cta-banner .btn-line { border-color: rgb(26 10 4 / .3); background: transparent; color: var(--accent-ink); }
.cta-banner .btn-line:hover { background: rgb(255 255 255 / .18); }
.cta-banner .btnrow { justify-content: center; }
.cta-banner .fox {
  position: absolute; right: -18px; bottom: -26px; width: 168px; height: auto;
  filter: drop-shadow(0 12px 18px rgb(90 20 0 / .3)); pointer-events: none;
}
@media (max-width: 760px) {
  .cta-banner { padding: 44px 24px 48px; margin-bottom: -60px; border-radius: 24px; }
  .cta-banner .fox { display: none; }
}

/* ---- sticky micro-CTA ----
   The page is ~11,000px tall and the only install button was at the two ends.
   This is the one persistent element; it stays out of the way until the hero has
   gone and steps aside again once the closing banner arrives. */

/* right: 92px, not 22px - Crisp's launcher is a 54px circle pinned at
   right:24/bottom:20 with z-index 1000002, so at 22px this pill sat directly
   underneath it and lost the overlap. Sitting it to the left of the launcher
   keeps both tappable on one baseline. Sienna's button takes the bottom-left
   corner, so nothing competes over there. */
.minicta {
  position: fixed; right: 92px; bottom: 22px; z-index: 60;
  display: inline-flex; align-items: center; gap: 10px;
  padding: 13px 20px; border-radius: 999px; text-decoration: none;
  background: var(--accent); color: #fff;
  font-weight: 600; font-size: 15.5px; letter-spacing: -.01em;
  box-shadow: 0 10px 28px -10px rgb(200 60 20 / .55), 0 2px 6px rgb(0 0 0 / .12);
  opacity: 0; transform: translateY(14px) scale(.96); pointer-events: none;
}
.minicta b { font-weight: 600; }
.minicta .rate { font-family: var(--mono); font-size: 13px; opacity: .8; }
.minicta.show { opacity: 1; transform: none; pointer-events: auto; }
.minicta:active { transform: scale(.97); }
@media (prefers-reduced-motion: no-preference) {
  .minicta { transition: opacity .35s ease, transform .45s cubic-bezier(.2,.8,.25,1); }
}
@media (max-width: 620px) {
  /* The bar takes the true bottom edge, full width; Crisp and Sienna move up
     to their own row above it rather than sharing its row. They used to sit
     at the same bottom:20px as this bar and rely on z-index to render over
     its corners - technically fine, nothing was ever hidden, but it read as
     three controls fighting for the same strip rather than two rows. This
     costs the pinned mobile deck a few sections up some of the room the
     previous layout gave back (its own comment, and the CSS below, has the
     current numbers) - that trade was made deliberately, in the other
     direction, this time. */
  .minicta { left: 16px; right: 16px; bottom: 16px; justify-content: center; }
}

/* ---- Crisp and Sienna, pushed above the CTA bar ----

   Neither widget exposes a bottom-offset config option, so this overrides
   their own CSS directly rather than through an API - the standard way sites
   reposition either one. .asw-menu-btn is what Sienna's own script renders it
   as here - confirmed moving.

   Crisp did not move: #crisp-chatbox is a wrapper Crisp inserts in the page,
   but the thing that actually carries position:fixed is the <iframe> Crisp
   renders inside it - overriding the wrapper's own (already static, unpositioned)
   box does nothing to where the iframe sits. This is Crisp's own documented
   customization path (their help center gives the same #crisp-chatbox iframe
   selector for exactly this). Still could not confirm on screen - the chat
   launcher never rendered in local/localhost testing, #crisp-chatbox exists in
   the DOM but stays a zero-size shell, almost certainly gated to configured
   domains the same way CookieYes's banner is elsewhere on this page - so this
   is the documented fix, not an observed one. The JS below is the fallback for
   the one way that can still fail silently: if Crisp sets its own bottom
   inline with !important after this stylesheet loads, inline beats external
   regardless of !important on the external side, and it re-asserts ours
   whenever Crisp's iframe changes. */
@media (max-width: 620px) {
  .asw-menu-btn { bottom: 78px !important; }
  #crisp-chatbox iframe { bottom: 78px !important; }
}

/* ---- install picker modal ----
   The generic install CTAs (hero, closing banner, sticky pill) all pointed at
   the all-17-apps listing, asking a merchant to work out which of the three
   they actually want once they land there. This asks the question up front
   and sends them straight to the right App Store page. */
.install-modal { position: fixed; inset: 0; z-index: 200; display: none; }
.install-modal[aria-hidden="false"] { display: flex; align-items: center; justify-content: center; padding: 24px; }
body.modal-lock {
  overflow: hidden;
  position: fixed;
  left: 0;
  right: 0;
  width: 100%;
}

.install-overlay {
  position: absolute; inset: 0;
  background: rgba(16,14,12,.56);
  backdrop-filter: blur(3px);
}

.install-dialog {
  position: relative; z-index: 1;
  background: var(--card); border-radius: 22px;
  padding: 40px 40px 34px;
  max-width: 700px; width: 100%; max-height: calc(100vh - 48px);
  overflow-y: auto; overflow-x: hidden;
  box-shadow: 0 40px 80px -30px rgba(0,0,0,.45);
  text-align: center; isolation: isolate;
}
.install-glow {
  position: absolute; top: -120px; left: 50%; translate: -50% 0;
  width: 420px; height: 240px; border-radius: 50%;
  background: radial-gradient(closest-side, rgb(232 65 15 / .30), transparent);
  z-index: -1; pointer-events: none;
}
.install-dialog h2 { margin-bottom: 8px; }
.install-sub { color: var(--ink-2); max-width: 46ch; margin: 0 auto 30px; }

.install-close {
  position: absolute; top: 16px; right: 16px;
  width: 34px; height: 34px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--card); color: var(--ink-2); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background .15s ease, color .15s ease;
}
.install-close svg { width: 15px; height: 15px; }
.install-close:hover { background: var(--sunk); color: var(--ink); }

.install-dialog .badge-row { justify-content: center; margin: 0 auto 26px; }
.install-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; text-align: left; }
/* The platform step only ever has two cards (Shopify, Wix) - its own
   column count, not a fourth card squeezed into the app-picker's grid. */
.install-grid-2 { grid-template-columns: repeat(2, 1fr); max-width: 480px; margin: 0 auto; }

/* Three (or two) cards side by side needs real width per card - below 700px
   the dialog itself is already down to its own padding's worth of room,
   so the row was squeezing until it forced the dialog to scroll sideways
   rather than wrap. Stacked cards read fine at this width; a cramped row
   of three does not. */
@media (max-width: 700px) {
  .install-modal[aria-hidden="false"] { padding: 16px; }
  .install-dialog { padding: 28px 22px 24px; }
  .install-grid, .install-grid-2 { grid-template-columns: 1fr; max-width: none; }
  /* The bloom behind the title is decorative and fixed at 420px - harmless
     once the dialog clips it, but capping it here means nothing on the
     page ever has to fight a phantom 420px-wide box for room. */
  .install-glow { width: min(420px, 100%); }
}
.install-card {
  display: flex; flex-direction: column; gap: 8px; width: 100%;
  padding: 20px 16px 18px; border-radius: 16px;
  border: 1.5px solid var(--line); background: var(--bg);
  text-decoration: none; color: var(--ink);
  font: inherit; text-align: left; cursor: pointer;
  transition: border-color .18s ease, transform .18s ease, box-shadow .18s ease, background .18s ease;
}
.install-card img { border-radius: 10px; }
.install-card .ic-name { font-weight: 700; font-size: 16px; }
.install-card .ic-name i { font-style: normal; font-weight: 600; color: var(--ink-2); }
.install-card .ic-desc { color: var(--ink-2); font-size: 13.5px; line-height: 1.4; flex-grow: 1; }
.install-card .ic-rate { font-family: var(--mono); font-size: 12.5px; color: #B57C00; }
.install-card .ic-rate i { font-style: normal; color: var(--ink-2); }
.install-card .ic-go {
  margin-top: 4px; font-weight: 600; font-size: 13.5px;
  color: var(--app-accent, var(--accent-text));
}
/* Back control for the platform step - same weight as a footnote link, not
   a second close button. */
.install-back {
  display: inline-flex; align-items: center; gap: 6px;
  margin-bottom: 16px; padding: 0; border: none; background: none;
  font: inherit; font-weight: 600; font-size: 14px; color: var(--ink-2);
  cursor: pointer;
}
.install-back svg { width: 14px; height: 14px; }
.install-back:hover { color: var(--ink); }
.install-card:hover {
  border-color: var(--app-accent, var(--accent)); background: var(--card);
  transform: translateY(-3px);
  box-shadow: 0 16px 30px -18px rgb(0 0 0 / .25);
}

.install-all {
  display: inline-block; margin-top: 24px;
  color: var(--ink-2); font-size: 14px; text-decoration: none; border-bottom: 1px solid var(--line);
}
.install-all:hover { color: var(--ink); border-color: var(--ink-2); }

@media (prefers-reduced-motion: no-preference) {
  .install-dialog { animation: install-in .22s cubic-bezier(.2,.8,.25,1); }
}
@keyframes install-in {
  from { opacity: 0; transform: translateY(10px) scale(.97); }
  to   { opacity: 1; transform: none; }
}

@media (max-width: 700px) {
  .install-dialog { padding: 32px 22px 28px; border-radius: 18px; }
  .install-grid { grid-template-columns: 1fr; }
  .install-card { flex-direction: row; align-items: center; flex-wrap: wrap; }
  .install-card .ic-desc { flex-basis: 100%; order: 3; }
  .install-card .ic-go { flex-basis: 100%; order: 4; }
}

/* the footer's second row of legal links - same treatment as .links, kept as
   its own row so "Privacy / Terms / Accessibility" reads as one group rather
   than blending into the nav-style links above it */
.footer-tier footer .legal { display: flex; flex-wrap: wrap; gap: 22px; font-size: 14.5px; color: var(--mute); width: 100%; order: 3; }
.footer-tier footer .legal a { color: var(--mute); }
.footer-tier footer .legal a:hover { color: var(--ink); }

/* the footer is the last chapter, and the banner lands on it */
.footer-tier { padding-top: 0; }
.footer-tier footer { border-top: 0; color: var(--ink-2); }
.footer-tier footer a { color: var(--ink); }
.footer-tier footer b { color: var(--ink); }

/* the overlap only reads if the banner actually crosses the seam - the section's
   own bottom padding was holding it clear of it */
.close.last { padding-bottom: 0; }
.tier:has(.cta-banner) { padding-bottom: 0; }
.cta-banner { margin-bottom: -118px; }
@media (max-width: 760px) { .cta-banner { margin-bottom: -88px; } }

/* ============================================================
   Odometer reels

   A digit is a strip of 0-9 inside a 1em window. Only `transform` moves, so a
   headline-sized counter costs the compositor and nothing else. Sized in `em`
   throughout, which is what lets the same markup work at 86px in the hero and
   at 14px inside a mock.
   ============================================================ */

.odo { display: inline-flex; align-items: flex-start; vertical-align: baseline; }
.odo-col {
  display: inline-block; overflow: hidden;
  height: 1em; line-height: 1em;
  /* tabular figures are all one width, so a reel is exactly one character wide
     and 9 -> 10 is the only thing that ever changes the layout */
  width: 1ch;
}
/* ---- flip-clock digits ----
   The timer already rolls, so the reels only need dressing: each digit gets its
   own tile with a hairline across the middle, which is what makes a split-flap
   board read as one. The separator stays outside the tiles so it does not get a
   box of its own. */
/* Vertical breathing room lives on the row, not the tile: .odo-col is the
   reel's own clip window, and its overflow:hidden (set on the base rule,
   line ~2276) clips at whatever height:1em resolves to. `height: auto` here
   used to let the tile grow to fit vertical padding - but overflow:hidden
   clips CHILDREN to the parent's own box, and a box sized by `auto` around a
   1em child (the .odo-strip, itself pinned to height:1em two lines down) grows
   to fit that 1em box, not to the 20-line-tall stack of digits inside it. The
   clip window ended up ~1.4 digits tall, so two digits' edges always showed
   in the same tile - worse at some font sizes than others, which is why it
   only became obvious at one width. Padding stays horizontal only, so the
   clip stays locked to exactly one line at every size. */
.num.flip { display: inline-flex; align-items: center; gap: 6px; }
.num.flip .odo { gap: 2px; }
.num.flip .odo-col {
  width: auto; min-width: 1ch; padding: 0 5px; border-radius: 5px;
  background: var(--app-surface, #E3ECFF);
  border: 1px solid color-mix(in srgb, var(--app-accent, #2B61D9) 26%, transparent);
  box-shadow: inset 0 -1px 0 rgb(0 0 0 / .06);
  position: relative;
  line-height: 1em;
  color: var(--app-accent, #2B61D9);
}
/* the hinge */
.num.flip .odo-col::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  border-top: 1px solid color-mix(in srgb, var(--app-accent, #2B61D9) 20%, transparent);
  pointer-events: none;
}
.num.flip .odo-sep { padding: 0 1px; color: var(--app-accent, #2B61D9); opacity: .7; }

.odo-strip { display: block; will-change: transform; }
.odo-strip i { display: block; height: 1em; line-height: 1em; font-style: normal; }
.odo-sep { font-style: normal; line-height: 1em; }
/* generated content, so the reels stay out of the document's text */
.odo-strip i::before, .odo-sep::before { content: attr(data-d); }

/* the value in text, for screen readers and for anything scraping the page */
.odo-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* ============================================================
   The previews take the screen

   This section is the product demo - it is the one place a merchant sees the
   apps actually running - and it was living inside the same 1080px column as a
   paragraph of copy, throwing away 332px a side on a 1680px display.

   So this section, and only this section, breaks the page's column. The rest of
   the site staying narrow is what makes the break read as deliberate: the demo
   is wide because it matters, not because everything is wide.
   ============================================================ */

@media (min-width: 1200px) {
  .tier:has(.scroller) > .wrap { max-width: min(1580px, 94vw); }

  .scroller { max-width: none; }

  /* the frame takes roughly two thirds, the copy becomes a caption column
     beside it rather than a second body of text competing with it */
  .scroll-grid {
    grid-template-columns: minmax(0, 1.78fr) minmax(0, .74fr);
    gap: 76px;
    /* NOT center: the copy column is four full-height steps, so centring the
       row put the sticky frame ~1,200px down the track and it never appeared
       at the top of the section at all */
    align-items: start;
  }
  .step p { max-width: 34ch; }

  /* the frame is bigger, so everything inside it steps up with it - otherwise a
     larger window just means more white space around the same small widget */
  .scroll-stage .pdp { padding: 42px; gap: 40px; }
  .scroll-stage .name { font-size: 24px; }
  .scroll-stage .tag { font-size: 20px; }
  .scroll-stage .widget { font-size: 19px; padding: 17px 0; }
  .scroll-stage .widget .num { font-size: 23px; }
  .scroll-stage .cta { padding: 18px; font-size: 18px; }
  .scroll-stage .bar-top { padding: 17px 24px; }
  .scroll-stage .bar-top .url { font-size: 14px; }
  .scroll-stage .co-main { padding: 38px 40px; }
  .scroll-stage .co-banner { font-size: 15px; }
  .scroll-stage .co-side { padding: 34px 30px; }

  /* a bigger object needs to sit on more light and cast a longer shadow, or it
     reads as a screenshot pasted on the page */
  .scroller.js-on.in-view .window.stage {
    box-shadow: 0 3px 10px rgb(22 24 26 / .06), 0 54px 90px -44px rgb(22 24 26 / .40);
  }
  .stagewrap .wash { transform: scale(1.18); }
  /* the frame's left edge is now near the viewport edge, so the fox moves to the
     inside of the frame's bottom-left rather than hanging off it */
  .foxes { left: 26px; bottom: -58px; width: 190px; height: 255px; }
}

/* very wide screens: keep the frame from stretching past a believable browser */
@media (min-width: 1700px) {
  .scroll-grid { grid-template-columns: minmax(0, 1.62fr) minmax(0, .8fr); gap: 88px; }
}

/* between the wide layout and the mobile stack the fox hung 9px off the viewport */
@media (min-width: 901px) and (max-width: 1199px) { .foxes { left: 8px; } }

/* ============================================================
   Per-app rooms in the previews

   The section takes the live app's colour as you scroll: neutral for the
   overview, then stock, live, timer. Every value comes from the generated
   [data-app] rules, which come from the icon art - so the colour a merchant
   sees here is the colour of the icon they will click on the App Store.

   Only the surface, the glow and the rail change. Type colours stay put: the
   surfaces were solved so the palest body colour still clears 4.55:1 on all
   three, which is what lets the room change without the copy following it.
   ============================================================ */

.tier:has(.scroller) {
  background-color: var(--app-surface, var(--sunk));
}
@media (prefers-reduced-motion: no-preference) {
  /* slow, because it is a room changing rather than an element animating */
  .tier:has(.scroller) { transition: background-color .9s ease; }
}

.stagewrap .wash {
  background: radial-gradient(60% 55% at 45% 40%, var(--app-wash, #FFE4D2), transparent 72%);
}

/* the progress rail picks up the live app's accent */
.rail i.on { background: var(--app-accent, var(--accent)); }

/* ---- the stock bar reads as a traffic light ----
   A non-text graphic carrying meaning needs 3:1 against what is next to it
   (WCAG 1.4.11), so these are the darker ends of each hue, not the bright ones. */
.widget .bar i.is-high { background: #157F4F; }   /* room on the shelf */
.widget .bar i.is-mid  { background: #A86A00; }   /* thinning */
.widget .bar i.is-low  { background: #CE2C0C; }   /* nearly gone */
@media (prefers-reduced-motion: no-preference) {
  .widget .bar i { transition: width .8s ease, background-color .5s ease; }
}

/* generated from FEATURES / APPS in scarcity.py - do not hand-edit */
.panel-big[data-panel="honest"]{--pbg:#1C5A45;--pfg:#FFFFFF;--pdim:rgba(255,255,255,.74);--ppanel:rgba(255,255,255,.10)}
.panel-big[data-panel="surfaces"]{--pbg:#FBD8C8;--pfg:#2A1206;--pdim:rgba(42,18,6,.72);--ppanel:rgba(255,255,255,.55)}
.panel-big[data-panel="setup"]{--pbg:#16181A;--pfg:#FFFFFF;--pdim:rgba(255,255,255,.70);--ppanel:rgba(255,255,255,.08)}
[data-app="lis"]{--app-accent:#AA4F10;--app-surface:#FFE7D6;--app-wash:#FFD7BC}
[data-app="pvc"]{--app-accent:#C92350;--app-surface:#FFE5EC;--app-wash:#FFC9D8}
[data-app="cct"]{--app-accent:#2B61D9;--app-surface:#E3ECFF;--app-wash:#C9D9FF}
