KnowSugar
aadmin2026-09-27 发布Site模板0 阅读

KnowSugar

类型:Site模板 · 收录:2026-09-27 11:09:41 · 源码:会员可见

[hidecontent type="vip1"]
Build a single, standalone, production-ready `index.html` file — no build step, no
external JS, no frameworks, no CSS files. Everything inline. It is a scroll-driven
landing page for a fictional blood-sugar awareness brand called "KnowSugar".

═══════════════════════════════════════════════════════════════════════
1. THE CORE IDEA
═══════════════════════════════════════════════════════════════════════
A single 8-second background video is scrubbed by scroll position. The page has
340vh of scroll track; everything visible is `position: fixed`. Scrolling from top
to bottom plays the video from 0s to its end, and two overlaid "scenes" of copy
cross-fade over it.

The video shows: a glowing sugar cube → it shatters into crystals → the crystals
reassemble into a human bust.

Scene 1 (hero) sits inside an INSET ROUNDED CARD. As you scroll, that card EXPANDS
to full-bleed, and the navbar's ink inverts from dark to white. Scene 2 then
staggers in over the now-full-bleed video.

═══════════════════════════════════════════════════════════════════════
2. VIDEO ASSET
═══════════════════════════════════════════════════════════════════════
Use exactly this URL as the <video> src:

https://d2ol7oe51mr4n9.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/88cfbf35-fe4d-4461-a21b-1e581c69ba18.mp4

Properties (rely on these): H.264 High profile, 8-bit yuv420p, 1920x1080, 24 fps,
8.04 s, ~9.2 MB, ALL-INTRA (every one of its 193 frames is a keyframe), faststart,
no audio track.

All-intra matters: any seek decodes exactly one frame (~4-5 ms), which is what makes
scroll scrubbing smooth. Do not assume sparse keyframes.

<video> attributes: id="bgv" muted playsinline webkit-playsinline preload="auto"
disablepictureinpicture. No `loop`, no `autoplay`, no `controls`.

═══════════════════════════════════════════════════════════════════════
3. FONTS
═══════════════════════════════════════════════════════════════════════
Google Fonts, with preconnect to fonts.googleapis.com and fonts.gstatic.com
(crossorigin):

https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Playfair+Display:ital,wght@1,400;1,500&display=swap

--sans:  "Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif
--serif: "Playfair Display",Georgia,"Times New Roman",serif

Inter is the UI/body face. Playfair Display ITALIC is used ONLY for: the second
headline line of each section, and the "i" glyph in the info badge.

═══════════════════════════════════════════════════════════════════════
4. DESIGN TOKENS (:root)
═══════════════════════════════════════════════════════════════════════
--ink:#1A1A1E;  --slate:#7C819B;  --slate-soft:#8B90A8;
--nav-h:88px;  --card-gap:24px;  --card-r:16px;  --pad:24px;
--r-btn:8px;   --r-card:12px;

Driven per-frame from JS (declare with these initial values):
--inset-x:24px; --inset-t:96px; --radius:16px;
--nav-ink:#1A1A1E; --nav-accent:#8B90A8; --nav-icon:#7C819B;
--pill-bg:#7C819B; --pill-ink:#FFFFFF;

body: margin 0, background #FFFFFF, color var(--ink), font-family var(--sans),
-webkit-font-smoothing:antialiased, -moz-osx-font-smoothing:grayscale,
overscroll-behavior-y:none. Universal `box-sizing:border-box`.

═══════════════════════════════════════════════════════════════════════
5. DOM STRUCTURE (exact order)
═══════════════════════════════════════════════════════════════════════
<html lang="en" class="intro">
  head → meta charset, viewport (width=device-width, initial-scale=1,
         viewport-fit=cover), title, description, then a small inline <script>
         (see §9), then font links, then <style>
  body →
    div.stage#stage         → video#bgv, div.scrim.top, div.scrim.bottom
    header.nav.rv#nav       → a.logo, nav.nav-right(button.pill, button.menu)
    div.scenes              → section.scene#scene1, section.scene#scene2
    div.track
    <script>

═══════════════════════════════════════════════════════════════════════
6. THE HERO CARD (.stage)
═══════════════════════════════════════════════════════════════════════
.stage{ position:fixed; top:var(--inset-t); left:var(--inset-x);
  right:var(--inset-x); bottom:0; overflow:hidden; z-index:0;
  border-radius:var(--radius) var(--radius) 0 0; background:#8F96AD; }

Note bottom:0 — the card is inset on left/right/top only and BLEEDS OFF the bottom
edge of the viewport. Only the top two corners are rounded.

#bgv{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  object-position:center; display:block; background:#8F96AD; }

Two legibility scrims, both position:absolute; inset:0; pointer-events:none; z-index:1
.scrim.top    background:linear-gradient(to bottom,rgba(32,36,50,.30) 0%,rgba(32,36,50,.08) 26%,rgba(32,36,50,0) 46%)
.scrim.bottom background:linear-gradient(to top,  rgba(32,36,50,.40) 0%,rgba(32,36,50,.12) 30%,rgba(32,36,50,0) 56%)

═══════════════════════════════════════════════════════════════════════
7. NAVBAR — no background, inverting ink
═══════════════════════════════════════════════════════════════════════
The navbar NEVER paints a background of its own. In section 1 the white behind it
is simply the page showing around the inset card. Do not add a white bar, a blur,
or a border.

.nav{ position:fixed; top:0; left:0; right:0; min-height:var(--nav-h); z-index:40;
  display:flex; align-items:center; justify-content:space-between;
  padding:0 clamp(18px,2vw,26px); padding-top:env(safe-area-inset-top);
  background:transparent; transition:none; }

.logo → `Know` + <span>Sugar</span>. font-size:clamp(19px,1.6vw,23px); weight 400;
  letter-spacing:-.018em; color:var(--nav-ink); text-decoration:none; white-space:nowrap.
  .logo span{ color:var(--nav-accent) }

.nav-right{ display:flex; align-items:center; gap:clamp(14px,1.8vw,26px) }

.pill (text "Free risk test"): font-size 14px; weight 400; letter-spacing:-.005em;
  color:var(--pill-ink); background:var(--pill-bg); border:0;
  border-radius:var(--r-btn) /* 8px — lightly rounded, NOT a pill */;
  padding:11px 21px; cursor:pointer; white-space:nowrap;
  transition:filter .25s ease,transform .25s ease
  :hover{ filter:brightness(.94); transform:translateY(-1px) }

.menu button: display:flex; align-items:center; gap:11px; background:none; border:0;
  cursor:pointer; padding:6px 2px; color:var(--nav-ink); font-size:14px; weight 400.
  aria-label="Menu". Contains .menu-grid (a 2x2 grid of four <i>, gap 3px, each
  10x10px, background:var(--nav-icon), border-radius:2.5px, display:block) then
  a <span>Menu</span>.

═══════════════════════════════════════════════════════════════════════
8. SCENES
═══════════════════════════════════════════════════════════════════════
.scenes{ position:fixed; inset:0; z-index:20; pointer-events:none }
.scene{ position:absolute; display:flex; flex-direction:column }
.scene > *{ pointer-events:auto }

⚠ CRITICAL: do NOT put `will-change` on .scene. `will-change` naming
opacity/transform/filter makes the element a BACKDROP ROOT, which would cut
section 2's cards off from the video behind them and silently kill their
backdrop-filter.

Scene 1 lives INSIDE the card, so its box tracks the card as it expands:
#scene1{ top:calc(var(--inset-t) + var(--pad) + 6px);
         left:calc(var(--inset-x) + var(--pad));
         right:calc(var(--inset-x) + var(--pad));
         bottom:var(--pad); }
Scene 2 is always full-bleed (it only appears after expansion):
#scene2{ top:calc(var(--nav-h) + var(--pad)); left:var(--pad);
         right:var(--pad); bottom:var(--pad); }

--- shared type ---
.micro-row{ display:flex; justify-content:space-between; gap:28px; align-items:flex-start }
.micro{ font-size:clamp(12px,.95vw,14px); line-height:1.48; font-weight:400;
        color:rgba(255,255,255,.90); max-width:26ch }
.micro p{margin:0}  .micro p + p{margin-top:14px}  .micro.right{max-width:34ch}

.h-line1{ font-size:clamp(2.4rem,5.2vw,4.75rem); font-weight:300;
          letter-spacing:-.05em; line-height:1.02; color:#fff; margin:0 }
.h-line2{ font-family:var(--serif); font-style:italic; font-weight:400;
          font-size:clamp(2.5rem,5.5vw,5rem); line-height:1.04;
          letter-spacing:-.028em; color:#fff; margin:.02em 0 0 }

--- SECTION 1 ---
.micro-row with two .micro blocks:
  LEFT  (two <p>): "Over [540 million] adults worldwide live with diabetes." /
                   "Nearly half don’t know they’re already at risk."
  RIGHT (.micro.right.hide-sm, one <p>): "Blood sugar changes silently for years.
        Understanding your numbers today can help prevent serious health problems
        tomorrow."
  (Use the ’ right-single-quote entity, not a straight apostrophe.)

Then ONE bottom row containing headline (left) and buttons (right):
.hero-foot{ margin-top:auto; display:flex; align-items:flex-end;
            justify-content:space-between; gap:clamp(24px,4vw,64px) }
  > div.headline → h1.h-line1 "Know Your Sugar."
                   p.h-line2  "Protect Your Future."
  > div.cta-row{display:flex;gap:10px;flex-wrap:wrap}
      button.btn.btn-solid "Free risk test"
      button.btn.btn-ghost "Learn the Basics"

There is NO scroll indicator / "Scroll" hint anywhere.

.btn{ font-size:14px; weight 400; letter-spacing:-.005em; padding:11px 21px;
      border-radius:var(--r-btn); cursor:pointer; white-space:nowrap;
      transition:transform .25s ease,background .25s ease,border-color .25s ease }
.btn-solid{ background:#fff; color:var(--ink); border:1px solid #fff }
  :hover{ transform:translateY(-1px); background:#F1F2F6 }
.btn-ghost{ background:rgba(255,255,255,.08); color:#fff;
            border:1px solid rgba(255,255,255,.55);
            backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px) }
  :hover{ transform:translateY(-1px); background:rgba(255,255,255,.20) }

--- SECTION 2 ---
.micro-row containing:
  div.headline → h2.h-line1 "Small Numbers." / p.h-line2 "Big Consequences."
  div.info{ display:flex; gap:13px; align-items:flex-start; max-width:36ch }
    span.info-badge{ flex:none; width:22px; height:22px; border-radius:5px;
      background:#fff; display:grid; place-items:center; font-family:var(--serif);
      font-style:italic; font-size:14px; line-height:1; color:var(--ink);
      margin-top:2px } containing the letter "i", aria-hidden
    p.micro (inline style margin:0): "Blood sugar is one of the clearest indicators
      of long-term metabolic health. Understanding how it changes can help prevent
      problems before symptoms appear."

Then .cards pinned to the bottom:
.cards{ margin-top:auto; display:grid; grid-template-columns:repeat(4,1fr);
        gap:clamp(12px,1.4vw,18px) }

.card{ position:relative; border-radius:var(--r-card);
  border:1px solid rgba(255,255,255,.14);
  background:rgba(0,0,0,.20);                       /* low-opacity BLACK glass */
  backdrop-filter:blur(16px) saturate(120%);
  -webkit-backdrop-filter:blur(16px) saturate(120%);
  padding:clamp(18px,1.7vw,24px); min-height:clamp(172px,20vh,208px);
  display:flex; flex-direction:column; overflow:hidden;
  transition:background .3s ease,transform .3s ease,border-color .3s ease }
.card::after{ content:""; position:absolute; inset:0; border-radius:inherit;
  pointer-events:none;
  background:linear-gradient(160deg,rgba(255,255,255,.07),rgba(255,255,255,0) 55%) }
.card:hover{ background:rgba(0,0,0,.30); border-color:rgba(255,255,255,.24);
  transform:translateY(-4px) }

Card internals, in this DOM order:
  span.card-num  → position:absolute; top:clamp(16px,1.5vw,22px);
                   right:clamp(16px,1.5vw,22px); font-size:13.5px;
                   color:rgba(255,255,255,.62); letter-spacing:.02em
  span.card-ico  → 27x27px, color:#fff, opacity:.95; svg fills it 100%, display:block
  div.card-rule  → height:1px; background:rgba(255,255,255,.24);
                   margin:clamp(16px,1.6vw,22px) 0 clamp(12px,1.2vw,17px); width:45%
  h3.card-title  → font-size:clamp(16px,1.35vw,21px); weight 400;
                   letter-spacing:-.018em; color:#fff; margin:0
  p.card-body    → margin:clamp(10px,1vw,15px) 0 0;
                   font-size:clamp(12px,.9vw,13.5px); line-height:1.48;
                   color:rgba(255,255,255,.80)

All four icons are inline SVG, viewBox "0 0 24 24", fill="none" stroke="currentColor"
stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round":

01 Nutrition (cloche):
  <path d="M5 16.4a7 7 0 0 1 14 0"/><path d="M3.4 16.4h17.2"/><path d="M2.6 19.3h18.8"/>
  <circle cx="12" cy="6.6" r=".85"/><path d="M12 7.9v1.5"/>
  Body: "Balanced meals slow glucose spikes and help maintain steady energy
  throughout the day."

02 Recovery (bolt in circle):
  <circle cx="12" cy="12" r="9"/>
  <path d="M12.9 6.9 9.4 12.4h2.7l-.9 4.7 3.5-5.5h-2.7l.9-4.7Z"/>
  Body: "Poor sleep can affect insulin sensitivity even after a single night."

03 Movement (X in rounded square):
  <rect x="3.4" y="3.4" width="17.2" height="17.2" rx="5"/>
  <path d="M8.6 8.6l6.8 6.8M15.4 8.6l-6.8 6.8"/>
  Body: "Physical activity naturally improves the body’s ability to regulate glucose."

04 Prevention (shield + check):
  <path d="M12 3.2l7 2.7v5.5c0 4.3-2.9 7.6-7 9.4-4.1-1.8-7-5.1-7-9.4V5.9l7-2.7Z"/>
  <path d="M8.9 12.1l2.2 2.2 4-4.3"/>
  Body: "Knowing your baseline is often the first step toward preventing serious
  complications."

--- scroll track ---
.track{ position:relative; z-index:1; height:340vh; pointer-events:none }

═══════════════════════════════════════════════════════════════════════
9. PAGE-LOAD INTRO
═══════════════════════════════════════════════════════════════════════
On load the page is blank white; the hero card opens from a VERTICAL LINE AT THE
CENTRE out to its full width, then the copy staggers in.

Use clip-path, NOT scaleX — scaling would squash the footage. Clipping reveals the
real frame.

Inline <script> in <head>, placed AFTER the meta tags so charset comes first. It runs
before first paint:
  - if matchMedia('(prefers-reduced-motion: reduce)').matches → remove the `intro`
    class from <html>, so nothing ever starts hidden for those viewers
  - if 'scrollRestoration' in history → history.scrollRestoration='manual'
  (wrap the matchMedia call in try/catch)

Keyframes:
@keyframes stageOpen{
  from{ clip-path:inset(0 50% 0 50% round var(--radius) var(--radius) 0 0) }
  to  { clip-path:inset(0 0   0 0   round var(--radius) var(--radius) 0 0) } }
@keyframes rvUp{ from{opacity:0;transform:translate3d(0,20px,0)} to{opacity:1;transform:none} }
@keyframes rvWipe{   /* left-to-right wipe; the ±30% vertical slack keeps the
                        italic's ascenders/descenders from being clipped */
  from{ opacity:0; clip-path:inset(-30% 100% -30% -4%); transform:translate3d(0,14px,0) }
  to  { opacity:1; clip-path:inset(-30% -30% -30% -4%); transform:none } }
@keyframes fadeIn{ from{opacity:0} to{opacity:1} }

HELD state (while waiting for the first decoded video frame):
.intro .stage{ clip-path:inset(0 50% 0 50% round var(--radius) var(--radius) 0 0) }
.intro #bgv{ opacity:0 }
.intro .rv, .intro .rv-wipe{ opacity:0 }

RELEASED state — add class `intro-go` to <html>:
.intro-go .stage{ clip-path:none;
  animation:stageOpen 840ms cubic-bezier(.66,0,.24,1) backwards }
.intro-go #bgv{ opacity:1; animation:fadeIn 680ms ease 60ms backwards }
.intro-go .rv{ opacity:1;
  animation:rvUp 720ms cubic-bezier(.22,.7,.2,1) backwards;
  animation-delay:var(--d,0ms) }
.intro-go .rv-wipe{ opacity:1;
  animation:rvWipe 820ms cubic-bezier(.22,.7,.2,1) backwards;
  animation-delay:var(--d,0ms) }

⚠ Use fill-mode `backwards`, never `forwards`/`both`. With `backwards` an element
holds its from-state during its delay and afterwards returns to its OWN normal
styles. A `forwards` fill would freeze a clip-path on .stage and keep rounded
corners forever after the card goes full-bleed.

Per-element delays via an inline `style="--d:…"`:
  header.nav          .rv        --d:140ms
  micro LEFT          .rv        --d:700ms
  micro RIGHT         .rv        --d:780ms
  h1.h-line1          .rv-wipe   --d:870ms
  p.h-line2           .rv-wipe   --d:1000ms
  button.btn-solid    .rv        --d:1280ms
  button.btn-ghost    .rv        --d:1360ms

RELEASE LOGIC (at the end of the main script): if <html> has class `intro`, hold
until the first frame has actually decoded so the card never opens onto an empty
placeholder — if v.readyState >= 2 release now, else release on `loadeddata` (once)
and on `error` (once), plus a setTimeout(release, 2200) fallback. Guard with a
`released` boolean. Inside release(), add `intro-go` inside a requestAnimationFrame
so the held state paints at least once first.

Resulting timeline: card is a centre line at ~70ms, ~40% open at 300ms, essentially
open by 700ms, corner copy ~700-950ms, headline lines wipe 870→1350ms, buttons
1280→1900ms.

═══════════════════════════════════════════════════════════════════════
10. THE SCROLL ENGINE (main script, IIFE at end of body)
═══════════════════════════════════════════════════════════════════════
State: duration=8, metaReady=false, smoothTime=0, lastApplied=-1,
lastFrame=performance.now(), seeking=false, smoothExp=0, lastP=-1, idle=0, FPS=24,
reduce = matchMedia('(prefers-reduced-motion: reduce)').matches.

readGeom() reads --card-gap, --card-r from getComputedStyle(documentElement) and
NAV_H from nav.offsetHeight (fallbacks 24/16/88); call it once and on `resize`, so
the CSS breakpoints stay the single source of truth.

Helpers:
  clamp01(n)
  ease(t){ t=clamp01(t); return t*t*t*(t*(t*6-15)+10) }   // smootherstep
  range(p,a,b){ return ease((p-a)/(b-a)) }
  progress(): scrollY / (documentElement.scrollHeight - innerHeight), clamped 0..1
  mix(a,b,t): lerp two [r,g,b,a] arrays → "rgba(...)" string

Video priming:
  onMeta() → if v.duration finite and > 0 set duration; metaReady=true;
             try{ v.pause(); v.currentTime=0.001 }catch{}
  bind to `loadedmetadata` and `durationchange`; call immediately if readyState>=1.
  Safari/iOS will not decode frames until the element is activated once: on the
  first of touchstart/pointerdown/wheel/keydown/click (once, passive) and on
  `canplay` (once), call v.play() and immediately pause it (handle the promise).
  Track `seeking` via the `seeking`/`seeked` events.

ONE requestAnimationFrame loop does everything:
  dt = min((now-lastFrame)/1000, 0.05); k = reduce ? 1 : (1 - Math.exp(-dt*7.5))
    → frame-rate-independent damping; this is what makes it feel liquid.
  p = progress()
  targetTime = p * max(duration-0.06, 0.06)      // LINEAR, so scrub tracks 1:1
  expTarget  = range(p, 0.10, 0.38)              // card expansion

  IDLE SHORT-CIRCUIT (do this before any style writes):
    busy = p !== lastP || |targetTime-smoothTime| > 0.003
                       || |expTarget-smoothExp|   > 0.0015
    lastP = p; idle = busy ? 0 : idle+1
    if (idle > 3) { requestAnimationFrame(frame); return }
    → once scroll stops and both springs converge, stop churning style every frame.

  smoothTime += (targetTime - smoothTime) * k
  SEEK, quantised to the frame grid (source is all-intra, so one decode per frame):
    if (metaReady && !seeking) {
      q = Math.round(smoothTime*FPS)/FPS
      if (q !== lastApplied) { try{ v.currentTime = q; lastApplied = q }catch{} } }
    Do NOT use fastSeek and do NOT seek on sub-frame deltas.

  smoothExp += (expTarget - smoothExp) * k;  e = smoothExp;  ie = 1 - e
  Write, via documentElement.style.setProperty:
    --inset-x : (CARD_GAP * ie) px
    --inset-t : ((NAV_H + CARD_GAP*0.34) * ie) px
    --radius  : (CARD_R * ie) px
  Navbar ink inversion (interpolated every frame, never threshold-switched):
    --nav-ink    [26,26,30,1]    → [255,255,255,1]
    --nav-accent [139,144,168,1] → [255,255,255,.70]
    --nav-icon   [124,129,155,1] → [255,255,255,1]
    --pill-bg    [124,129,155,1] → [255,255,255,1]
    --pill-ink   [255,255,255,1] → [26,26,30,1]
  (The navbar background itself is never touched — it stays transparent.)

  SCENE 1 out, range(p, 0.20, 0.45) → out1:
    rest1 = out1 <= 0.002
    opacity    = String(1-out1)
    transform  = rest1 ? "none" : `translate3d(0,${-52*out1}px,0)`
    filter     = rest1 ? "none" : `blur(${out1*7}px)`
    visibility = out1 >= 0.999 ? "hidden" : "visible"

  SCENE 2 in, range(p, 0.62, 0.88) → in2:
    scene2.style.visibility = in2 <= 0.001 ? "hidden" : "visible"
    The CONTAINER is never faded, blurred or transformed — each child animates,
    so the container never becomes a backdrop root and the cards keep the video
    behind their backdrop-filter.
    Collect s2Items = scene2.querySelectorAll("[data-s2]") — mark with a bare
    `data-s2` attribute, in DOM order: h-line1, h-line2, .info, then the 4 .card
    elements (7 items).
    S2_STEP = 0.07;  S2_SPAN = 1 - S2_STEP*(s2Items.length-1)
    for each item i:
      t = ease((in2 - i*S2_STEP) / S2_SPAN)
      if (t >= 0.998) → set style.opacity = "" and style.transform = "" (clear them
        to empty strings, NOT to "1"/"none"), guarded by a cached s2Applied[i]
      else → opacity = String(t); transform = `translate3d(0,${30*(1-t)}px,0)`
    Mid-reveal the four cards should cascade roughly 0.98 / 0.88 / 0.71 / 0.50.

  Initial state before the first rAF:
    scene2.style.visibility = "hidden"
    each s2Item.style.opacity = "0"
    ⚠ Do NOT set opacity on the scene2 container — nothing would ever clear it and
    section 2 would stay invisible forever.

⚠ BACKDROP-ROOT RULE (applies throughout): an ancestor with `filter`, `opacity < 1`,
`clip-path`, or `will-change` naming any of those becomes a backdrop root and
silently disables descendants' backdrop-filter. At rest, every ancestor of a .card
must have transform `none`, filter `none`, opacity exactly 1, and no will-change.
Always clear to the keyword `none` / an empty string, never to identity values like
`translate3d(0,0,0)` or `blur(0px)`.

═══════════════════════════════════════════════════════════════════════
11. RESPONSIVE
═══════════════════════════════════════════════════════════════════════
@media (max-width:900px){
  :root{ --nav-h:64px; --card-gap:12px; --card-r:14px; --pad:18px }
  .micro-row{ flex-direction:column; gap:14px }
  .micro, .micro.right{ max-width:46ch }
  .hero-foot{ flex-direction:column; align-items:flex-start; gap:18px }
  .cards{ grid-template-columns:none; grid-auto-flow:column;
          grid-auto-columns:minmax(210px,72vw); gap:10px;
          overflow-x:auto; overscroll-behavior-x:contain;
          scroll-snap-type:x mandatory; -webkit-overflow-scrolling:touch;
          scrollbar-width:none;
          margin:auto calc(var(--pad) * -1) 0;      /* ⚠ see note */
          padding:0 var(--pad) 4px }
  .cards::-webkit-scrollbar{ display:none }
  .card{ scroll-snap-align:start; min-height:168px }
}
⚠ The card row is pinned to the bottom by `margin-top:auto`. Writing the bleed as
`margin:0 calc(...)` would reset margin-top to 0 and float the cards up under the
copy. It MUST be `margin:auto calc(var(--pad) * -1) 0`.

@media (max-width:560px){
  .h-line1{ font-size:clamp(1.7rem,8.4vw,2.5rem) }   /* keeps each line on one row */
  .h-line2{ font-size:clamp(1.78rem,8.8vw,2.6rem) }
  .micro{ font-size:12.5px }
  .micro.hide-sm{ display:none }
  .card-rule{ width:60% }
  .btn, .pill{ font-size:13px; padding:10px 17px }
  .menu span:last-child{ display:none }
  .menu-grid i{ width:9px; height:9px }
}

Short viewports — the card row must never be pushed off screen, so shrink the copy
above it instead of moving the row:
@media (max-height:660px){
  .cards{ gap:8px }
  .card{ min-height:clamp(132px,23vh,168px); padding:14px }
  .card-rule{ margin:11px 0 9px }
  .card-body{ font-size:11.5px; line-height:1.42 }
  .card-ico{ width:22px; height:22px }
  .micro p + p{ margin-top:9px }
}
@media (max-height:520px){
  :root{ --nav-h:52px; --pad:14px }
  .h-line1{ font-size:clamp(1.35rem,5vh,2rem) }
  .h-line2{ font-size:clamp(1.4rem,5.2vh,2.1rem) }
  .micro{ font-size:11.5px; line-height:1.4 }
  #scene2 .info{ display:none }        /* secondary copy yields first */
  .micro.hide-sm{ display:none }
  .card{ min-height:112px; padding:12px }
  .card-ico{ width:18px; height:18px }
  .card-rule{ margin:8px 0 7px }
  .card-num{ top:11px; right:12px; font-size:11.5px }
  .card-body{ margin-top:6px }
}

═══════════════════════════════════════════════════════════════════════
12. ACCEPTANCE CRITERIA
═══════════════════════════════════════════════════════════════════════
□ There are ZERO text-shadow and ZERO box-shadow declarations in the whole file.
□ At scroll 0: card inset 24px L/R, 16px top radius, bleeding off the bottom;
  navbar transparent with DARK ink and a slate #7C819B button; white page margin
  visible around the card.
□ At scroll 1: stage fills the viewport (inset 0, radius 0); navbar transparent with
  WHITE ink and a WHITE button with dark text; cards flush to the bottom edge.
□ Scroll 0→1 maps linearly to video 0→~7.98s.
□ The hero headline and the CTA buttons share ONE row, space-between, both bottom
  edges on the same line. No scroll hint exists.
□ The four cards sit flush on the bottom of the viewport (gap 0) at EVERY size,
  desktop and mobile alike — verify at 1440x900, 1280x720, 768x1024, 430x932,
  390x844, 360x640, 844x390, 667x375.
□ No vertical overflow in either scene and no horizontal page overflow at any size.
□ A card's computed backdrop-filter is `blur(16px) saturate(1.2)` AND it visibly
  blurs the footage behind it at rest.
□ After the intro finishes: zero running animations, .stage clip-path `none`, and
  every revealed element back to opacity 1 / transform none with no inline leftovers.
[/hidecontent]
← 返回首页