@import url('./fonts.css');

/* =========================================================================
   David's Master Pot — 首页 (mobile)
   1:1 restoration of 首页2.psd  ·  design canvas 1284 × 14286 px
   All layout values below are RAW PSD PIXELS. The page is scaled by
   --px (1 design px), so every number matches the PSD exactly.
   ========================================================================= */

:root {
  /* 1rem = 100 design px.  --px = 1 design px */
  --px: .01rem;

  --red: #ef0b2d;
  --red2: #f00d2b;
  --gold: #d2b374;
  --gold2: #f0c977;
  --ink: #9f1410;          /* deep red page ground */
  --cream: #f3e9d2;

  /* per-family baseline constant  k = (descender - ascender) / 2
     used by .t to place text on the PSD baseline, metric-exact */
  --k: -.3475;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html {
  /* the whole design scales with the viewport, capped at a phone width */
  font-size: calc(min(100vw, 500px) / 12.84);
  -webkit-text-size-adjust: 100%;
  /* keep the gutter while the loader locks scrolling, so releasing it does not
     shift the layout on desktop */
  scrollbar-gutter: stable;
}

body {
  margin: 0 auto;
  width: 12.84rem;
  background: var(--ink);
  color: #fff;
  overflow-x: hidden;
  font-family: 'OPPO Sans', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  -webkit-font-smoothing: antialiased;
}

img { display: block; border: 0; }
a { color: inherit; text-decoration: none; }

/* --- html/body backdrop outside the phone column (desktop preview) ------ */
/* the PSD ground is a single flat red from the founder block down to the footer */
html { background: var(--ink); }

/* =========================  positioning primitives  ====================== */

/* absolute box, PSD coordinates */
.b {
  position: absolute;
  left: calc(var(--x) * var(--px));
  top: calc(var(--y) * var(--px));
  width: calc(var(--w) * var(--px));
  height: calc(var(--h) * var(--px));
}

/* absolute text, anchored on the PSD baseline of its first line */
.t {
  position: absolute;
  margin: 0;
  left: calc(var(--x) * var(--px));
  /* --fsx optically rescales a substitute face so its cap-height matches the
     PSD; it has to appear in the baseline term too or the text drifts */
  top: calc(var(--y) * var(--px) - var(--lh) * var(--px) / 2 + var(--fs) * var(--fsx, 1) * var(--px) * var(--k));
  font-size: calc(var(--fs) * var(--fsx, 1) * var(--px));
  line-height: calc(var(--lh) * var(--px));
  letter-spacing: calc(var(--tr) * .001em);
  white-space: pre;
  font-weight: 400;
}
/* centred text: --x is the PSD centre anchor */
.t.tc {
  left: calc((var(--x) - 200) * var(--px));
  width: calc(400 * var(--px));
  text-align: center;
}

/* ------------------------------  type roles  ----------------------------
   Client font kit (字体/) drives the page:
     大标题字体/            ->  League Spartan   (.f-ls)  — display headlines
     字体/OPPOSans-L/R/M/B/H ->  OPPO Sans       (.f-op)  — everything else,
                                subset to the 155 glyphs this page renders

   PSD face                  ->  face used here
   FuturaBT-* / FuturaPT-*   ->  League Spartan  (.f-ls display, .f-fu rest)
                                 both are geometric sans, so the kit maps by
                                 category and the PSD's box widths still hold
   PingFang-SC-*             ->  OPPO Sans       (.f-pf)
   OPPOSans-R/M/B/H          ->  OPPO Sans       (.f-op)  — exact, the real face
   DIN-Bold                  ->  Archivo 700 @ font-stretch 84%  (.f-din)
   Century751BT-SemiBold     ->  Noto Serif 600  (.f-se)
   (the kit has no condensed-numeral face and no serif, so those two keep a
    substitute — flagged in the handover note)

   --k  per-family baseline constant (descender - ascender) / 2, from hhea.
   --fsx optical size compensation: League Spartan's cap-height is .66 em vs
        Futura's .738, so display type is set 1.118x the PSD size to occupy
        the same optical space. Measured against the PSD it then lands within
        ~3% on width (DAVID'S 746 vs 746, Group Footprint 542 vs 553).
   -------------------------------------------------------------------------- */
.f-ls,
.f-fu  { font-family: 'League Spartan', 'Futura', sans-serif; --k: -.24; --fsx: 1.118; }
.f-op,
.f-pf  { font-family: 'OPPO Sans', 'PingFang SC', 'HarmonyOS Sans SC', 'Microsoft YaHei', sans-serif; --k: -.3405; }
.f-din { font-family: 'Archivo', 'DIN Alternate', 'DIN Next', sans-serif; font-stretch: 84%; --k: -.334; }
.f-se  { font-family: 'NotoSerifSub', 'Century Schoolbook', Georgia, 'Times New Roman', serif; --k: -.388; font-weight: 600; }

.w300 { font-weight: 300 } .w400 { font-weight: 400 } .w500 { font-weight: 500 }
.w600 { font-weight: 600 } .w700 { font-weight: 700 } .w800 { font-weight: 800 } .w900 { font-weight: 900 }
.caps { text-transform: uppercase }
.ital { font-style: italic }

/* ============================  page skeleton  ============================ */
.stage { position: relative; width: 12.84rem; overflow: hidden; }
.sec   { position: relative; width: 12.84rem; scroll-margin-top: calc(240 * var(--px)); }

/* =============================  LOADING  ================================
   Background artwork is 1284 x 2778 with the word Loading… baked in, so the
   stage keeps that ratio and is scaled to *cover* the viewport — the mascot
   then stays locked to the artwork at any screen shape.
   ----------------------------------------------------------------------- */
.loading, .loading body { overflow: hidden; }

.loader {
  position: fixed; inset: 0; z-index: 200;
  background: #f5d89d;               /* artwork base gold — the fallback if the
                                        webp ever fails, screen is never blank */
  overflow: hidden;
  transition: opacity .55s ease, visibility .55s;
}
.loader.done { opacity: 0; visibility: hidden; pointer-events: none; }

.loader-stage {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: max(100vw, calc(100vh * 1284 / 2778));
  aspect-ratio: 1284 / 2778;
}
.loader-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* The mascot runs as a CSS sprite, not a <video>.
   Muted inline autoplay is refused often enough in the wild (iOS in Low Power Mode)
   and hijacked into a native player with visible chrome often enough (WeChat's X5,
   several vendor browsers on Android) that a loading screen cannot depend on it.
   A background-image stepped by a CSS animation has no autoplay policy, no player
   chrome to suppress, and loops forever by construction.
   48 frames on an 8 x 6 sheet, 5.04s — one frame per 105 ms.
   The sheet's flat field is repainted to the backdrop's exact gold, so the sprite's
   box melts into the artwork and needs no mask. Every tile carries a 2px gutter so
   lossy compression cannot bleed the neighbouring frame in at a seam; the box is
   scaled up by 320/316 below to cancel that gutter out again. */
.loader-anim {
  position: absolute;
  left: 18.0566%;                    /* 236/1280 of the stage, less half a gutter */
  top: 24.3235%;
  width: 60.918%;                    /* 770/1280, plus the gutter */
  height: 26.356%;                   /* 720/1280 of the stage width */
  background: url(../assets/loading-sprite.webp) 0% 0% / 800% 600% no-repeat;
  animation: mascot 5.04s step-end infinite;
}
@keyframes mascot {
  0% { background-position: 0% 0%; }
  2.0833% { background-position: 14.2857% 0%; }
  4.1667% { background-position: 28.5714% 0%; }
  6.25% { background-position: 42.8571% 0%; }
  8.3333% { background-position: 57.1429% 0%; }
  10.4167% { background-position: 71.4286% 0%; }
  12.5% { background-position: 85.7143% 0%; }
  14.5833% { background-position: 100% 0%; }
  16.6667% { background-position: 0% 20%; }
  18.75% { background-position: 14.2857% 20%; }
  20.8333% { background-position: 28.5714% 20%; }
  22.9167% { background-position: 42.8571% 20%; }
  25% { background-position: 57.1429% 20%; }
  27.0833% { background-position: 71.4286% 20%; }
  29.1667% { background-position: 85.7143% 20%; }
  31.25% { background-position: 100% 20%; }
  33.3333% { background-position: 0% 40%; }
  35.4167% { background-position: 14.2857% 40%; }
  37.5% { background-position: 28.5714% 40%; }
  39.5833% { background-position: 42.8571% 40%; }
  41.6667% { background-position: 57.1429% 40%; }
  43.75% { background-position: 71.4286% 40%; }
  45.8333% { background-position: 85.7143% 40%; }
  47.9167% { background-position: 100% 40%; }
  50% { background-position: 0% 60%; }
  52.0833% { background-position: 14.2857% 60%; }
  54.1667% { background-position: 28.5714% 60%; }
  56.25% { background-position: 42.8571% 60%; }
  58.3333% { background-position: 57.1429% 60%; }
  60.4167% { background-position: 71.4286% 60%; }
  62.5% { background-position: 85.7143% 60%; }
  64.5833% { background-position: 100% 60%; }
  66.6667% { background-position: 0% 80%; }
  68.75% { background-position: 14.2857% 80%; }
  70.8333% { background-position: 28.5714% 80%; }
  72.9167% { background-position: 42.8571% 80%; }
  75% { background-position: 57.1429% 80%; }
  77.0833% { background-position: 71.4286% 80%; }
  79.1667% { background-position: 85.7143% 80%; }
  81.25% { background-position: 100% 80%; }
  83.3333% { background-position: 0% 100%; }
  85.4167% { background-position: 14.2857% 100%; }
  87.5% { background-position: 28.5714% 100%; }
  89.5833% { background-position: 42.8571% 100%; }
  91.6667% { background-position: 57.1429% 100%; }
  93.75% { background-position: 71.4286% 100%; }
  95.8333% { background-position: 85.7143% 100%; }
  97.9167% { background-position: 100% 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .loader-anim { animation: none; }
}

/* =============================  TOP BAR  ================================ */
.topbar {
  position: fixed; top: 0; left: 50%; transform: translateX(-50%);
  width: 12.84rem; height: 2.3rem; z-index: 60;
  transition: background .45s ease, backdrop-filter .45s ease;
}
.topbar.solid {
  background: rgba(8, 8, 8, .72);
  backdrop-filter: saturate(160%) blur(.14rem);
  -webkit-backdrop-filter: saturate(160%) blur(.14rem);
}
.topbar .bar { position: absolute; background: #fff; }
.menu-btn {
  position: absolute; left: calc(950 * var(--px)); top: calc(60 * var(--px));
  width: calc(280 * var(--px)); height: calc(140 * var(--px));
  background: none; border: 0; padding: 0; cursor: pointer;
}
.menu-btn .bar { transition: transform .4s cubic-bezier(.22,.61,.36,1), opacity .3s ease, width .4s ease; }

/* burger -> close */
.nav-open .menu-btn .b1 { transform: translateY(calc(18 * var(--px))) rotate(45deg); }
.nav-open .menu-btn .b2 { opacity: 0; transform: scaleX(.2); }
.nav-open .menu-btn .b3 { width: calc(68 * var(--px)); transform: translateY(calc(-18 * var(--px))) rotate(-45deg); }

/* ------------------------------  drawer  -------------------------------- */
.drawer {
  position: fixed; inset: 0; left: 50%; transform: translateX(-50%);
  width: 12.84rem; z-index: 55;
  background: rgba(6, 6, 6, .93);
  backdrop-filter: blur(.2rem); -webkit-backdrop-filter: blur(.2rem);
  padding: 2.9rem .9rem 0;
  opacity: 0; visibility: hidden; transition: opacity .4s ease, visibility .4s;
}
.nav-open .drawer { opacity: 1; visibility: visible; }
.drawer a {
  display: block; font-family: 'League Spartan', sans-serif; font-weight: 400;
  font-size: calc(56 * var(--px)); line-height: calc(120 * var(--px));
  letter-spacing: -.05em; color: #fff;
  border-bottom: 1px solid rgba(255,255,255,.09);
  opacity: 0; transform: translateY(calc(30 * var(--px)));
  transition: opacity .5s ease, transform .5s cubic-bezier(.22,.61,.36,1);
}
.nav-open .drawer a { opacity: 1; transform: none; transition-delay: calc(var(--i) * 32ms + 80ms); }
.drawer a:last-child { border: 0; }

/* ==============================  HERO  ================================== */
.s-hero { height: calc(2722 * var(--px)); background: var(--ink); }
.s-hero::before {
  content: ''; position: absolute; left: 0; top: 0;
  width: 12.84rem; height: calc(1470 * var(--px)); background: #000100;
}

/* subscribe card */
.subscribe { border-radius: calc(80 * var(--px)); overflow: hidden; border: calc(2 * var(--px)) solid rgba(162,162,162,.5); }
.subscribe .shade { position: absolute; inset: 0; background: rgba(0,0,0,.5); }
.field   { border-radius: calc(80.5 * var(--px)); background: #000; border: calc(2 * var(--px)) solid #454545; }
.pill    { border-radius: calc(80.5 * var(--px)); background: var(--red); border: 0; cursor: pointer; }

/* real inputs sit invisibly over the designed field */
.inp {
  position: absolute; border: 0; background: transparent; outline: none; color: #fff;
  font-family: 'OPPO Sans', sans-serif; padding: 0;
}
.inp::placeholder { color: transparent; }
.inp:not(:placeholder-shown) + .ph { opacity: 0; }
.ph { transition: opacity .2s ease; pointer-events: none; }

/* ==============================  FOUNDER  =============================== */
.s-founder { height: calc(1152 * var(--px)); }
.founder-card { border-radius: calc(80 * var(--px)); background: #d0411a; }

.rule { background: var(--gold); }
.btn-dark  { border-radius: calc(25 * var(--px)); background: #000; }

/* ==============================  RECIPE  ================================ */
.s-recipe { height: calc(1392 * var(--px)); }
/* the PSD darkens this card towards its foot (图层 矩形 5), it is not a flat fill */
.card-recipe {
  border-radius: calc(80 * var(--px)); overflow: hidden;
  background: linear-gradient(#272727 0, #272727 63%, #1e1e1e 78%, #141414 90%, #0b0b0b 100%);
}
.dot { border-radius: 50%; background: var(--red2); }
.btn-red { border-radius: calc(35 * var(--px)); background: var(--red); }

/* ---- rising steam (requirement 5) ---- */
.smoke { position: absolute; pointer-events: none; overflow: hidden; }
.smoke i {
  position: absolute; display: block;
  width: calc(var(--sw) * var(--px)); height: calc(var(--sw) * var(--px));
  left: calc(var(--sx) * var(--px)); bottom: calc(var(--sy) * var(--px));
  background: radial-gradient(closest-side, rgba(255,255,255,.55), rgba(255,255,255,.14) 55%, transparent 72%);
  filter: blur(calc(14 * var(--px)));
  mix-blend-mode: screen; opacity: 0;
  animation: smokeRise var(--sd) linear infinite; animation-delay: var(--sl);
  will-change: transform, opacity;
}
@keyframes smokeRise {
  0%   { transform: translate3d(0,0,0) scale(.55) rotate(0deg);      opacity: 0; }
  12%  { opacity: .5; }
  55%  { opacity: .38; }
  100% { transform: translate3d(calc(var(--sdx) * var(--px)), calc(-1 * var(--srise) * var(--px)), 0) scale(1.9) rotate(14deg); opacity: 0; }
}
.paused .smoke i { animation-play-state: paused; }

/* =============================  FOOTPRINT  ============================== */
.s-footprint { height: calc(2138 * var(--px)); }
.card-light { border-radius: calc(80 * var(--px)); background: #fff; overflow: hidden; }
.card-cream { border-radius: calc(80 * var(--px)); background: var(--cream); overflow: hidden; }
.entry      { border-radius: calc(20 * var(--px)); background: var(--cream); }
.footer-card{ border-radius: calc(80 * var(--px)); background: #680e0b; }
.qr-card    { border-radius: calc(10 * var(--px)); background: #fff; }

.pin-label { border-radius: calc(10 * var(--px)); background: #f6efe1; }
.pin-caret { }
.pin img, .pin-caret img { width: 100%; height: 100%; }

/* ---------------------------------------------------------------------
   Infinite auto-scrollers (App-Store style, slow + seamless loop)

   The PSD marks each of these regions with a gradient layer mask — a right
   edge fade on the store rail, a bottom fade under the stat tiles and under
   the system cards. Those masks are the viewport edge of a scrolling area,
   which is why the PSD draws more rows than the container can hold. The
   masks below reproduce the PSD gradients 1:1 (design px):

     store rail   x 1108 opaque -> 1186 clear   (rel. to card: 1027 -> 1105)
     stat tiles   y 8634 opaque -> 8932 clear   (rel. to viewport: 460 -> 758)
     system cards y 9778 opaque -> 10090 clear  (rel. to viewport: 352 -> 664)

   A matching fade is added on the opposite edge, which the PSD has no reason
   to draw (nothing scrolled in from there in a static comp) but the loop does.
   --------------------------------------------------------------------- */
.scroll {
  position: absolute;
  left: calc(var(--x) * var(--px));
  top: calc(var(--y) * var(--px));
  width: calc(var(--w) * var(--px));
  height: calc(var(--h) * var(--px));
  overflow: hidden;
}
.scroll .track { position: absolute; left: 0; top: calc(var(--padTop, 0) * var(--px)); width: 100%; height: 100%; }
.scroll .unit  { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }

/* vertical loop: tiles / system cards */
.scroll-y {
  -webkit-mask-image: linear-gradient(to bottom,
      transparent 0,
      #000 calc(var(--fadeIn, 70) * var(--px)),
      #000 calc(var(--fadeStart) * var(--px)),
      transparent calc(var(--fadeEnd) * var(--px)));
  mask-image: linear-gradient(to bottom,
      transparent 0,
      #000 calc(var(--fadeIn, 70) * var(--px)),
      #000 calc(var(--fadeStart) * var(--px)),
      transparent calc(var(--fadeEnd) * var(--px)));
}
.scroll-y .track { animation: loopY var(--dur, 40s) linear infinite; }
.scroll-y .unit + .unit { top: calc(var(--loop) * var(--px)); }
@keyframes loopY { to { transform: translate3d(0, calc(var(--loop) * var(--px) * -1), 0); } }

/* horizontal loop: store rail */
.scroll-x {
  -webkit-mask-image: linear-gradient(to right,
      transparent 0, #000 calc(var(--fadeIn, 33) * var(--px)),
      #000 calc(var(--fadeStart) * var(--px)),
      transparent calc(var(--fadeEnd) * var(--px)));
  mask-image: linear-gradient(to right,
      transparent 0, #000 calc(var(--fadeIn, 33) * var(--px)),
      #000 calc(var(--fadeStart) * var(--px)),
      transparent calc(var(--fadeEnd) * var(--px)));
}
.scroll-x .track { display: flex; width: max-content; height: 100%;
  padding-left: calc(33 * var(--px));
  animation: loopX var(--dur, 46s) linear infinite; }
.scroll-x .unit { position: static; display: flex; width: auto; gap: calc(24 * var(--px)); }
.scroll-x .unit + .unit { margin-left: calc(24 * var(--px)); }
@keyframes loopX { to { transform: translate3d(calc(var(--loop) * var(--px) * -1), 0, 0); } }

/* pause while off screen, on hover and while touched */
.scroll.paused .track,
.scroll:hover .track,
.scroll:active .track { animation-play-state: paused; }

.store {
  flex: 0 0 calc(405 * var(--px)); height: calc(532 * var(--px));
  border-radius: calc(20 * var(--px)); background: #fff; overflow: hidden;
  position: relative;
}
.store > img { width: 100%; height: calc(341 * var(--px)); object-fit: cover; }

/* =============================  NUMBERS  =============================== */
.s-numbers { height: calc(1576 * var(--px)); }
.card-white { border-radius: calc(80 * var(--px)); background: #fff; overflow: hidden; }
.tile { border-radius: calc(60 * var(--px)); background: #f2f2f2; }

/* =============================  SYSTEMS  =============================== */
.s-systems { height: calc(1188 * var(--px)); }
.card-brown { border-radius: calc(80 * var(--px)); background: #241a16; overflow: hidden; }
.sys {
  border-radius: calc(20 * var(--px)); background: #291f1b;
  border: calc(2 * var(--px)) solid #473929;
}

/* ============================  TRAJECTORY  ============================= */
.s-trajectory { height: calc(1262 * var(--px)); }
.card-dark {
  border-radius: calc(80 * var(--px)); background: #111214; overflow: hidden;
}

/* ===============================  NEWS  ================================ */
.s-news { height: calc(1487 * var(--px)); }
.thumb { border-radius: calc(40 * var(--px)); overflow: hidden; background: #000; }
.thumb img { width: 100%; height: 100%; object-fit: cover; }
.hr { background: #a4a4a4; }

/* ==============================  FOOTER  =============================== */
.s-footer { height: calc(1369 * var(--px)); overflow: hidden; }
.cb {
  border-radius: calc(5 * var(--px)); background: #340908;
  border: calc(2 * var(--px)) solid #e2e2e2; cursor: pointer;
}
.cb .ico { position: absolute; left: calc(11 * var(--px)); top: calc(14 * var(--px));
          width: calc(35 * var(--px)); height: calc(26 * var(--px)); color: #fff;
          opacity: 0; transform: scale(.4); transition: opacity .18s ease, transform .28s cubic-bezier(.34,1.56,.64,1); }
.cb.on .ico { opacity: 1; transform: none; }
.field-dark { border-radius: calc(76.6 * var(--px)); background: #280908; }
.op5 { opacity: .5 }

/* mask-tinted icons (PSD shapes carry layer effects we recolour in CSS) */
.ico {
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
  background: currentColor;
}
.ico-arrow { -webkit-mask-image: url(../assets/icon-arrow.png); mask-image: url(../assets/icon-arrow.png); }
.ico-check { -webkit-mask-image: url(../assets/icon-check.png); mask-image: url(../assets/icon-check.png); }

.form-note {
  position: absolute; left: calc(200 * var(--px)); top: calc(600 * var(--px));
  font-family: 'OPPO Sans', sans-serif; font-size: calc(24 * var(--px));
  letter-spacing: -.03em; color: #ffd9d4; opacity: 0; transition: opacity .3s ease;
}
.form-note.show { opacity: .95; }

/* =======================================================================
   MOTION
   ======================================================================= */

/* --- 1. Apple-style type reveal (per word / per char) ------------------ */
.rt { }
.js .rt .w, .js .rt .c {
  display: inline-block; white-space: pre;
  opacity: 0; transform: translateY(.42em); filter: blur(calc(9 * var(--px)));
  transition:
    opacity .56s cubic-bezier(.22,.61,.36,1),
    transform .66s cubic-bezier(.22,.61,.36,1),
    filter .56s cubic-bezier(.22,.61,.36,1);
  transition-delay: var(--d, 0ms);
}
.rt.in .w, .rt.in .c { opacity: 1; transform: none; filter: blur(0); }
/* drop the blur filter once revealed so we do not keep hundreds of layers */
.rt.done .w, .rt.done .c { filter: none; transition: none; }

/* --- 2. App-Store style card reveal ------------------------------------ */
.js .rv {
  opacity: 0;
  transform: translateY(calc(46 * var(--px))) scale(.94);
  transition:
    opacity .48s cubic-bezier(.22,.61,.36,1),
    transform .58s cubic-bezier(.19,.82,.28,1);
  transition-delay: var(--d, 0ms);
}
.rv.in { opacity: 1; transform: none; }

/* soft fade-only variant (images, plates) */
.js .rf { opacity: 0; transition: opacity .6s ease var(--d, 0ms); }
.rf.in { opacity: 1; }

/* --- 3. pin drop -------------------------------------------------------- */
.js .pin {
  opacity: 0;
  transform: translateY(calc(var(--drop) * var(--px) * -1)) scale(.86);
}
.pin.in { animation: pinDrop .56s cubic-bezier(.45,.03,.52,.96) forwards; animation-delay: var(--d, 0ms); }
@keyframes pinDrop {
  0%   { opacity: 0; transform: translateY(calc(var(--drop) * var(--px) * -1)) scale(.86); }
  8%   { opacity: 1; }
  62%  { transform: translateY(0) scale(1.14, .82); }
  76%  { transform: translateY(calc(-26 * var(--px))) scale(.94, 1.08); }
  88%  { transform: translateY(0) scale(1.04, .96); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
.js .pin-label, .js .pin-caret {
  opacity: 0; transform: translateY(calc(14 * var(--px))) scale(.9);
  transform-origin: 50% 100%;
  transition: opacity .32s ease var(--d, 0ms), transform .4s cubic-bezier(.34,1.4,.64,1) var(--d, 0ms);
}
.pin-label.in, .pin-caret.in { opacity: 1; transform: none; }

/* --- 4. rising trajectory (节节高升) --------------------------------------
   The curve just fades in. The story is told by the arrow: it is drawn from
   its tail up to its head, and each of the four milestone nodes pulses as the
   arrow sweeps past it.

   Node x in chart-curve.png (1119 wide): 44 / 293 / 555 / 776, scaled 1.04 and
   offset -16 -> card x 29.8 / 288.7 / 561.2 / 791. The arrow box spans card x
   100..1081, so each node sits at 0 / .192 / .470 / .704 along the sweep; the
   draw is linear so those fractions convert straight into delays (x 1.05s).
   ------------------------------------------------------------------------- */
.chart-wrap, .arrow-wrap { overflow: hidden; }
.js .growth .chart-wrap { opacity: 0; }
.growth.in .chart-wrap { animation: chartIn .5s ease .06s forwards; }
@keyframes chartIn { to { opacity: 1; } }

.node {
  border-radius: 50%;
  border: calc(3 * var(--px)) solid #ff8a3a;
  box-shadow: 0 0 calc(18 * var(--px)) rgba(255, 138, 58, .6);
  opacity: 0; pointer-events: none;
}
.growth.in .node { animation: nodePulse .62s ease-out var(--nd) both; }
@keyframes nodePulse {
  0%   { opacity: 0;   transform: scale(.35); }
  14%  { opacity: .95; transform: scale(1); }
  100% { opacity: 0;   transform: scale(2.15); }
}

.js .growth .arrow-wrap { clip-path: inset(0 100% 0 0); }
.growth.in .arrow-wrap { animation: drawArrow 1.05s linear .35s forwards; }
@keyframes drawArrow { to { clip-path: inset(0 0 0 0); } }

.js .growth .arrow-wrap img { transform: translate3d(-4%, 7%, 0); }
.growth.in .arrow-wrap img {
  animation: arrowRise 1.15s cubic-bezier(.2,.85,.3,1) .35s forwards,
             arrowFloat 3.2s ease-in-out 1.5s infinite;
}
@keyframes arrowRise { to { transform: translate3d(0, 0, 0); } }
@keyframes arrowFloat {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(calc(9 * var(--px)), calc(-16 * var(--px)), 0); }
}

/* --- 6. number roll ----------------------------------------------------- */
.num { display: inline-block; font-variant-numeric: tabular-nums; }
.num.pop { animation: numPop .5s cubic-bezier(.34,1.56,.64,1); }
@keyframes numPop { 0% { transform: scale(1) } 38% { transform: scale(1.14) } 100% { transform: scale(1) } }

/* KV headline: red glow sweep once revealed */
.kv-title { }

@media (prefers-reduced-motion: reduce) {
  .rt .w, .rt .c, .rv, .rf, .pin, .pin-label, .pin-caret {
    opacity: 1 !important; transform: none !important; filter: none !important;
    animation: none !important; transition: none !important;
  }
  .growth .chart-wrap { opacity: 1 !important; }
  .growth .arrow-wrap { clip-path: none !important; }
  .growth .node { opacity: 0 !important; }
  .growth .arrow-wrap img { transform: none !important; }
  .chart-wrap, .arrow-wrap, .arrow-wrap img, .node,
  .smoke i, .scroll .track { animation: none !important; }
}
