motionsites中文站 / Axle Journey
Axle Journey
aadmin2026-09-23 发布区块0 阅读

Axle Journey

类型:区块 · 收录:2026-09-23 19:21:52 · 源码:会员可见

[hidecontent type="vip1"]
Build a single-file, production-ready HTML landing page called "Axle". Output ONE complete
.html file — no build step, no frameworks, no CSS/JS libraries, no bundler, no Tailwind.
All CSS in one <style> in <head>. All JS in plain <script> tags at the end of <body>.
Vanilla ES5-compatible JS only (var, function, no arrow functions in the shipped code).

=====================================================================
0. WHAT THIS PAGE IS
=====================================================================
A full-viewport, non-scrolling "app shell" landing page for a city-mobility brand.
A looping video of a tilt-shift miniature model city fills the entire viewport edge to
edge. Floating on top of it: a left sidebar of three frosted-glass stat cards, a top row
of glass navigation pills, and a large rounded glass hero panel containing a headline,
a CTA button, a riders badge, a paragraph, and map controls. Four small orange map pins
sit on the video itself. The whole thing is one fixed, non-scrolling screen at every size.

HARD RULE: the page NEVER scrolls. body{overflow:hidden;height:100%}. The layout always
fits the viewport exactly at every breakpoint.

=====================================================================
1. DOCUMENT HEAD
=====================================================================
<!DOCTYPE html>, <html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Axle — Less friction. More possibility.</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">

=====================================================================
2. FONTS
=====================================================================
TWO families.

(a) Inter — from Google Fonts as above. This is the UI/body font.
    body font-family: 'Inter','Helvetica Neue',Helvetica,Arial,sans-serif;
    body also sets -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;

(b) "Parsi" — a custom display face loaded from local woff2 files. Declare three faces
    at the very top of the <style>, all font-style:normal, font-display:swap:
      @font-face{font-family:'Parsi';font-weight:300;src:url(assets/fonts/parsi-300.woff2) format('woff2')}
      @font-face{font-family:'Parsi';font-weight:400;src:url(assets/fonts/parsi-400.woff2) format('woff2')}
      @font-face{font-family:'Parsi';font-weight:700;src:url(assets/fonts/parsi-700.woff2) format('woff2')}
    Parsi is a wide geometric grotesque with a cap-height ratio of 0.564 (this is why the
    display font-sizes below look oversized — they are cap-height ÷ 0.564). Always write it
    as font-family:'Parsi',sans-serif so it degrades gracefully if the files are absent.
    Parsi is used ONLY for: the "Axle" wordmark, the "4K" stat, the "-42" gauge value,
    the "88%" stat, and the hero <h1>. Everything else is Inter.

=====================================================================
3. THE SCALE SYSTEM — read this before writing any CSS
=====================================================================
The entire design is a pixel reproduction of a 1280 x 853 reference. EVERY dimension is
expressed as a multiple of one custom property, --k, which equals exactly 1px at 1280x853
and scales the whole composition proportionally at any other size.

:root{
  --k: max(0.5px, min(100vw / 1280, 100vh / 853));
  --k: max(0.5px, min(100vw / 1280, 100dvh / 853));   /* second line overrides where dvh is supported */
}
Write sizes as calc(N * var(--k)). Use percentages only where a value is stated as a % below.

TOKENS (put these in :root):
  --accent:#F2702E;
  --accent-hot:#F66E2C;
  --ink-dark:#232428;
  --ink-light:#F0F1F3;
  --glass-dark:rgba(7,8,10,.77);
  --glass-pill:rgba(28,28,29,.93);
  --glass-light:rgba(241,243,246,.60);
  --glass-outline:rgba(180,180,178,.55);
  --hairline:rgba(255,255,255,.62);
  --r-card:calc(33 * var(--k));
  --r-hero:calc(34 * var(--k));


=====================================================================
4. LAYER 1 — THE FULL-BLEED VIDEO SCENE (z-index 0)
=====================================================================
<div class="scene"> is position:fixed;inset:0;overflow:hidden;z-index:0.

Inside it, a looping muted video — USE THESE EXACT REMOTE URLS, do not substitute a local
file, a placeholder, or a different CDN:

  <video class="scene__img" autoplay muted loop playsinline preload="auto"
         poster="https://d2ol7oe51mr4n9.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/0d8b856d-942e-429d-85de-1ae21c89b0e1.jpg"
         aria-label="Model city at dusk: tram lines converging on a glass transit hub">
    <source src="https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260912_105913_a30a8f2a-3118-4b26-9ccf-67b1869edf89.mp4" type="video/mp4">
  </video>

(The clip is 1920x1080, 16:9, ~10s, silent, and loops. The poster is its first frame, so
the composition is correct on first paint and if the video never loads.)

.scene__img geometry — these odd numbers are solved so the media is drawn 1284 x 858 at
(0,-2) in reference space; reproduce them literally:
  position:absolute; left:0; top:-0.2345%; width:100.3125%; height:100.5807%;
  object-fit:cover; object-position:50% 50%;

Then a scrim on top of the video:
  .scene__scrim{position:absolute;inset:0;pointer-events:none;
    background:linear-gradient(180deg,rgba(255,255,255,0) 60%,rgba(230,232,235,.20) 100%)}

The four orange map overlays also live inside .scene (see section 9).

=====================================================================
5. LAYER 2 — THE STAGE GRID (z-index 1)
=====================================================================
<div class="stage"> is position:fixed;inset:0;z-index:1;display:grid.
Columns (reference px 5.5 | 265.5 | 5 | 1002 | 2 = 1280):
  grid-template-columns:55fr 2655fr 50fr 10020fr 20fr;
Rows (5.3 | 829.2 | 18.5 = 853):
  grid-template-rows:53fr 8292fr 185fr;

Two children:
  .rail  -> grid-column:2; grid-row:2   (the three stat cards)
  .main  -> grid-column:4; grid-row:2   (nav + hero)

.rail{display:grid; grid-template-rows:2652fr 30fr 2645fr 45fr 2920fr; min-height:0}
  (265.2 card | 3 gap | 264.5 card | 4.5 gap | 292 card = 829.2)
.main{display:grid; grid-template-rows:52fr 705fr 90fr 7430fr 15fr; min-height:0}
  (5.2 | 70.5 nav | 9 | 743 hero | 1.5 = 829.2)

=====================================================================
6. THE THREE RAIL CARDS
=====================================================================
Shared: .card{position:relative;border-radius:var(--r-card);overflow:hidden;min-height:0}

.card--dark (grid-row:1) and .card--electric (grid-row:5):
  background:var(--glass-dark);
  backdrop-filter:blur(38px) (+ -webkit- prefix);
  box-shadow:0 calc(2*var(--k)) calc(18*var(--k)) rgba(12,14,18,.22);

.card--gauge (grid-row:3):
  background:var(--glass-light);
  backdrop-filter:blur(7px) saturate(1.02) (+ prefix);
  border:1px solid rgba(255,255,255,.55);
  box-shadow:0 calc(2*var(--k)) calc(18*var(--k)) rgba(12,14,18,.10);

--- CARD 1 — <section class="card card--dark" aria-label="Axle network"> ---
Every child is absolutely positioned at its measured coordinate.

.brand{position:absolute;left:17.137%;top:10.068%;display:flex;align-items:center}
  .brand svg{width:calc(47*var(--k));height:calc(32*var(--k));display:block}
  The mark is this exact SVG (viewBox "0 0 47 32", fill #F2702E), an angular "A":
    <path fill="#F2702E" d="M20 0 L27.5 0 L46.5 32 L38 32 L35 26 L27 26 L27.5 12.5 L23.5 11 L21 12.5 L8 32 L0 32 Z"/>
  .brand .word — text "Axle":
    margin-left:calc(16*var(--k)); font-family:'Parsi',sans-serif; font-weight:300;
    font-size:calc(33.7*var(--k)); letter-spacing:.02em; line-height:calc(19.1*var(--k));
    color:#F3F4F5; position:relative; top:calc(1*var(--k)); transform-origin:left center;

.dots{position:absolute;left:8.851%;top:46.267%;display:flex;gap:calc(4*var(--k))}
  Three spans: first .dot.dot--on, then two .dot.dot--off.
  .dot{width/height:calc(24*var(--k));border-radius:50%;display:grid;place-items:center}
  .dot--on{background:var(--accent)}
  .dot--off{border:calc(1.3*var(--k)) solid rgba(255,255,255,.62)}
  .dot::after{content:"";width/height:calc(5*var(--k));border-radius:50%;background:rgba(255,255,255,.92)}

.stat__num{position:absolute;left:8.851%;top:71.041%;font-family:'Parsi',sans-serif;
  font-weight:700;font-size:calc(65.5*var(--k));line-height:calc(37.2*var(--k));
  letter-spacing:.005em;color:#F1F2F4;transform-origin:left top}
  Markup: <span class="stat__num"><span class="rise">4K</span></span>

.stat__rule{position:absolute;left:51.035%;top:71.908%;width:1px;
  height:calc(33*var(--k));background:rgba(255,255,255,.34)}   (empty <span>)

.stat__label{position:absolute;left:63.842%;top:68.552%;font-size:calc(13.5*var(--k));
  font-weight:400;line-height:calc(16*var(--k));letter-spacing:.005em;color:#E7E8EA;
  white-space:pre-line}
  Text content, as three real lines inside the element (pre-line preserves them):
    Connected
    Mobility
    Points

--- CARD 2 — <section class="card card--gauge" aria-label="Emissions down 42 percent"> ---
.gauge__title{position:absolute;left:0;right:0;top:6.286%;text-align:center;
  font-size:calc(15.2*var(--k));font-weight:400;letter-spacing:.012em;color:#26272B}
  Text: "Emissions"

.gauge__ring{position:absolute;left:49.72%;top:52.00%;transform:translate(-50%,-50%);
  width:calc(200*var(--k));height:calc(200*var(--k))}
  .gauge__ring svg{width:100%;height:100%;display:block}
  The SVG is viewBox="0 0 200 200" aria-hidden="true" focusable="false" and contains
  452 <circle> elements and nothing else — see section 7 for the exact generator.

.gauge__value{position:absolute;left:0;right:0;top:53.88%;text-align:center;
  font-family:'Parsi',sans-serif;font-weight:300;font-size:calc(40.8*var(--k));
  letter-spacing:.02em;color:#232427;transform:translateY(-50%)}
  Markup: <div class="gauge__value"><span class="rise">-42</span></div>

--- CARD 3 — <section class="card card--electric" aria-label="Electric journeys 88 percent"> ---
.ej__title{position:absolute;left:17.891%;top:9.760%;font-size:calc(18.5*var(--k));
  font-weight:400;line-height:calc(19.5*var(--k));letter-spacing:.02em;
  transform:scaleX(1.08);transform-origin:left top;color:#EDEEF0}
  Markup: <div class="ej__title">Electric<br>Journeys</div>

.leaf{position:absolute;left:16.761%;top:31.336%;width/height:calc(53.5*var(--k));
  border-radius:50%;background:var(--accent);display:grid;place-items:center}
  .leaf svg{width/height:calc(25*var(--k));display:block}
  Leaf icon, viewBox "0 0 24 24", fill:none, stroke #fff, stroke-width 1.9, round caps/joins:
    <path d="M18.5 4.2c1 6.4-1.1 10.6-4.2 12.6-2.6 1.7-6 1.6-7.6-.4-1.7-2.1-1.1-5.6 1.2-7.9 2.6-2.6 6.7-3.9 10.6-4.3Z"/>
    <path d="M6.2 20.6c0-3.9 1.5-7.3 4.2-9.9"/>

.ej__num{position:absolute;left:16.761%;top:58.048%;font-family:'Parsi',sans-serif;
  font-weight:700;font-size:calc(59.2*var(--k));line-height:calc(33.6*var(--k));
  letter-spacing:.004em;color:#F1F2F4;transform-origin:left top}
  Markup: <div class="ej__num"><span class="rise">88%</span></div>

.bar{position:absolute;left:17.326%;top:79.966%;width:65.725%;height:calc(34*var(--k));
  border-radius:calc(3*var(--k));overflow:hidden;display:flex}
  Markup: <div class="bar"><span class="bar__fill"></span><span class="bar__rest"></span></div>
  .bar__fill{width:61.3%;background:linear-gradient(180deg,
      rgba(246,110,44,.02) 0%, rgba(246,110,44,.20) 26%, rgba(246,110,44,.62) 55%,
      rgba(246,110,44,.93) 82%, #F86F2A 100%)}
  .bar__rest{flex:1;background:linear-gradient(180deg,
      rgba(190,190,186,.03) 0%, rgba(190,190,186,.18) 26%, rgba(190,190,186,.46) 55%,
      rgba(196,196,192,.68) 82%, rgba(206,206,202,.80) 100%)}

=====================================================================
7. THE GAUGE DOT RING — exact generator
=====================================================================
Emit 452 static <circle> elements into the ring SVG (viewBox 0 0 200 200), centre (100,100).
FIVE concentric rings of evenly spaced dots. For ring with radius R and count N, dot index
i runs 0..N-1 and:
    theta = i * (360 / N) degrees, measured CLOCKWISE from 12 o'clock
    cx = 100 + R * sin(theta)
    cy = 100 - R * cos(theta)
Write cx/cy to 2 decimal places.

  ring 1: R=60.2  N=73   all grey
  ring 2: R=68.9  N=83   all grey
  ring 3: R=75.2  N=91   all grey
  ring 4: R=81.3  N=98   grey, EXCEPT indices 2..6 which are orange
  ring 5: R=88.0  N=107  grey, EXCEPT indices 1..36 which are orange

  grey  dot: <circle cx=".." cy=".." r="1.05" fill="#3A3B40" opacity="O"/>
             where O is 0.34, 0.44, 0.54, 0.64, 0.74 for rings 1..5 respectively
             (opacity increases outward, so the field reads as a glow)
  orange dot: <circle cx=".." cy=".." r="1.65" fill="#F2702E"/>   (no opacity attribute,
             and larger, so the arc reads as the "value" indicator)

Net effect: a soft grey halftone disc with a bold orange arc sweeping clockwise from
12 o'clock to roughly 4 o'clock on the outermost ring, plus a 5-dot orange tick just
inside it. The "-42" value sits centred in the hole.

=====================================================================
8. THE NAV ROW
=====================================================================
<nav class="nav" aria-label="Primary"> sits in .main grid-row:2.
.nav{display:flex;align-items:stretch;padding-left:calc(5*var(--k));gap:calc(3.5*var(--k));min-height:0}

Children in order:
 1. <button class="iconbtn nav__burger" type="button" aria-label="Menu" aria-expanded="false"
    aria-controls="nav-links"> — hamburger SVG, viewBox 0 0 24 24, fill none, stroke #F2F3F5,
    stroke-width 1.5, round caps: three paths "M4.5 7.5h15" / "M4.5 12h15" / "M4.5 16.5h15".
    .nav__burger{display:none} on desktop.
 2. <div class="nav__links" id="nav-links"> containing five <a class="pill"> elements:
      <a class="pill pill--solid pill--home" href="#">Home</a>
      <a class="pill pill--ghost pill--a" href="#">City Mobility</a>
      <a class="pill pill--ghost pill--b" href="#">Energy Hubs</a>
      <a class="pill pill--ghost pill--c" href="#">Our Progress</a>
      <a class="pill pill--solid pill--cta" href="#">Better Journeys
        <span class="grid4" aria-hidden="true"><i></i><i></i><i></i><i></i></span></a>
    .nav__links{display:contents} on desktop so the pills stay direct flex children of .nav.
 3. <span class="nav__gap" aria-hidden="true"></span>
 4. Three <button class="iconbtn"> tools, each viewBox 0 0 24 24, fill none, stroke #F2F3F5,
    stroke-width 1.5, round caps and joins:
      aria-label="Live navigation"  -> <path d="M12 2.6 20 21.4 12 17.1 4 21.4Z"/>
      aria-label="Riders"           -> <path d="M6.6 19.4a5.6 5.6 0 0 1 10.8 0"/>
                                       <path d="M1.4 18.6a3.4 3.4 0 0 1 3.6-3"/>
                                       <path d="M22.6 18.6a3.4 3.4 0 0 0-3.6-3"/>
      aria-label="Network layers"   -> <path d="M12 2.4 20.6 7v10L12 21.6 3.4 17V7Z"/>
                                       <path d="M3.4 7 12 11.9 20.6 7"/>
                                       <path d="M12 11.9v9.7"/>

PILL CSS — the pills are ELASTIC so the row always reaches the hero's right edge. Each pill
carries a designed width --w in reference px and grows in proportion to it:
  .pill{border-radius:999px;text-decoration:none;display:flex;align-items:center;
    justify-content:center;white-space:nowrap;font-size:calc(14*var(--k));font-weight:400;
    letter-spacing:.012em;
    flex-grow:var(--w,132); flex-shrink:0;
    flex-basis:calc(var(--w,132) * var(--k));
    max-width:calc(var(--w,132) * 1.35 * var(--k));}
  .pill--solid{background:var(--glass-pill);color:#F2F3F5;backdrop-filter:blur(30px)}
  .pill--ghost{background:transparent;border:1px solid var(--hairline);
               color:rgba(255,255,255,.93);backdrop-filter:blur(40px)}
  .pill--home{--w:132}
  .pill--a{--w:138.5}
  .pill--b{--w:131.5;margin-left:calc(-1*var(--k))}
  .pill--c{--w:135;margin-left:calc(-0.5*var(--k))}
  .pill--cta{--w:227;margin-left:calc(-2.8*var(--k));justify-content:flex-start;
    padding-left:calc(38*var(--k));font-size:calc(13.5*var(--k));letter-spacing:.012em;
    word-spacing:calc(3*var(--k));position:relative}
  .nav__gap{flex:1 0 calc(11*var(--k));align-self:stretch}
  .grid4{position:absolute;right:calc(22.7*var(--k));top:50%;transform:translateY(-50%);
    width/height:calc(16*var(--k));display:grid;grid-template-columns:1fr 1fr;
    grid-template-rows:1fr 1fr;gap:calc(4*var(--k))}
  .grid4 i{background:#F2F3F5;border-radius:50%;display:block}
  .iconbtn{flex:0 0 auto;width:calc(63.5*var(--k));height:calc(63.5*var(--k));aspect-ratio:1;
    border-radius:50%;background:var(--glass-pill);backdrop-filter:blur(30px);
    display:grid;place-items:center;align-self:center}
  .iconbtn + .iconbtn{margin-left:calc(0.5*var(--k))}
  .iconbtn svg{width/height:calc(27*var(--k));display:block}

=====================================================================
9. THE HERO PANEL
=====================================================================
<section class="hero" aria-label="Axle city network"> sits in .main grid-row:4,
position:relative;min-height:0.

The glass panel EDGE is an overlay ring so it costs no layout:
  .hero::after{content:"";position:absolute;inset:0;border-style:solid;
    border-color:rgba(255,255,255,.64);border-width:max(1px, calc(2*var(--k)));
    border-radius:var(--r-hero);pointer-events:none}
NOTE: the hero has NO background and NO backdrop-filter. It is just a ring drawn over the
video. The frosted look comes from the pills, cards, button, chip and controls only.

MAP OVERLAYS — these live inside .scene (NOT inside .hero), so they sit directly on the
video. All are <span aria-hidden="true"> with an inline SVG. Shared:
  .marker{position:absolute;filter:drop-shadow(0 calc(2*var(--k)) calc(4*var(--k)) rgba(0,0,0,.22))}
  .marker svg{display:block;width:100%;height:100%}
The % positions below are the no-JS fallback; a script re-pins them (section 12).
  .marker--bike{left:29.375%;top:39.51%;width:calc(25*var(--k));height:calc(34*var(--k))}
    SVG viewBox "0 0 25 34":
      <path fill="#F2702E" d="M12.5 0C5.6 0 0 5.5 0 12.3c0 7.1 8.4 16.9 11.4 21.1a1.3 1.3 0 0 0 2.2 0C16.6 29.2 25 19.4 25 12.3 25 5.5 19.4 0 12.5 0Z"/>
      <g fill="none" stroke="#fff" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round">
        <circle cx="7.4" cy="14.4" r="3.5"/><circle cx="17.6" cy="14.4" r="3.5"/>
        <path d="M7.4 14.4h4.2l3.1-5.2h2.9"/><path d="M11.6 14.4 14 8"/><path d="M12.6 8h3.2"/></g>
  .hero__mark{position:absolute;left:48.44%;top:45.02%;width:calc(27*var(--k));height:calc(19*var(--k))}
    viewBox "0 0 47 32", the same orange "A" path as the brand mark.
  .marker--bolt{left:83.52%;top:45.49%;width:calc(26*var(--k));height:calc(34*var(--k))}
    viewBox "0 0 26 34":
      <path fill="#F2702E" d="M13 0C5.8 0 0 5.5 0 12.3c0 7.1 8.7 16.9 11.9 21.1a1.4 1.4 0 0 0 2.2 0C17.3 29.2 26 19.4 26 12.3 26 5.5 20.2 0 13 0Z"/>
      <path fill="#fff" d="M14.9 5.4 8.2 14.1h4.1l-1.2 6.9 6.7-9h-4.1Z"/>
  .marker--pin{left:95.08%;top:46.78%;width:calc(23*var(--k));height:calc(31*var(--k))}
    viewBox "0 0 23 31":
      <path fill="#F2702E" d="M11.5 0C5.1 0 0 5 0 11.2c0 6.5 7.7 15.4 10.5 19.2a1.2 1.2 0 0 0 2 0C15.3 26.6 23 17.7 23 11.2 23 5 17.9 0 11.5 0Z"/>
      <circle cx="11.5" cy="11" r="4.1" fill="#fff"/>

HEADLINE + CTA — .pitch, anchored bottom-left:
  .pitch{position:absolute;left:7.884%;bottom:3.432%}
  .pitch h1{font-family:'Parsi',sans-serif;font-weight:400;font-size:calc(54*var(--k));
    line-height:calc(44*var(--k));letter-spacing:0;color:#1B1C1F;margin-bottom:calc(19*var(--k))}
  The h1 must be split into two mask-ready lines (required by the entrance animation):
    <h1><span class="ln"><span class="ln__i">Less friction.</span></span><span class="ln"><span class="ln__i">More possibility.</span></span></h1>
  .ln,.ln__i{display:block}

  <button class="btn" type="button"> containing
    <span class="btn__disc"> + arrow SVG + <span class="btn__label">Discover Our Network</span>
  .btn{height:calc(53*var(--k));width:calc(303*var(--k));border-radius:999px;
    border:1px solid rgba(255,255,255,.72);background:rgba(186,188,192,.20);
    backdrop-filter:blur(34px);display:flex;align-items:center;
    padding-left:calc(3.5*var(--k));cursor:pointer;color:#1D1E21;font-family:inherit}
  .btn__disc{width/height:calc(43*var(--k));border-radius:50%;background:#26272A;
    display:grid;place-items:center;flex:0 0 auto}
  .btn__disc svg{width/height:calc(16*var(--k));display:block}
    arrow: viewBox "0 0 24 24", fill none, stroke #fff, stroke-width 2.1, round caps/joins,
    <path d="M9 4.5 17 12l-8 7.5"/>
  .btn__label{margin-left:calc(43.5*var(--k));font-size:calc(14*var(--k));font-weight:400;
    letter-spacing:0;position:relative;top:calc(-3*var(--k))}

RIDERS BADGE + PARAGRAPH — .aside, anchored bottom-right-ish:
  .aside{position:absolute;left:62.076%;bottom:3.095%;width:32.136%}
  Markup:
    <div class="riders">
      <span class="avatars" aria-hidden="true"><span class="av av--a"></span><span class="av av--b"></span></span>
      <div class="riders__pill">20K+ Riders</div>
    </div>
    <p>One connected network for a city in motion. Axle brings clean transport and convenient charging closer to every neighbourhood.</p>

  .riders{position:relative;width:calc(139*var(--k));height:calc(33*var(--k));
    border-radius:999px;background:rgba(70,68,66,.24);border:1px solid rgba(255,255,255,.26);
    background-clip:padding-box;backdrop-filter:blur(8px);margin-bottom:calc(17.5*var(--k))}
  .riders__pill{position:absolute;left:calc(46.5*var(--k));top:calc(2*var(--k));
    width:calc(90*var(--k));height:calc(28*var(--k));border-radius:999px;
    background:rgba(45,43,41,.245);
    box-shadow:inset 0 0 calc(3*var(--k)) rgba(255,255,255,.30);
    display:flex;align-items:center;white-space:nowrap;padding-left:calc(11*var(--k));
    font-size:calc(11.6*var(--k));font-weight:400;letter-spacing:.012em;color:#F4F5F6}
  .avatars{position:absolute;left:calc(-1*var(--k));top:50%;transform:translateY(-50%);
    display:flex;z-index:2}
  .av{width/height:calc(32*var(--k));border-radius:50%;
    border:calc(2*var(--k)) solid rgba(255,255,255,.95);
    background:#c9ccd1 50% 40%/cover no-repeat;flex:0 0 auto}
  .av + .av{margin-left:calc(-16*var(--k))}
  Two Unsplash head crops at 160px (free licence, no attribution required):
  .av--a{background-image:url('https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=160&h=160&fit=crop&crop=faces,edges&q=80&auto=format')}
  .av--b{background-image:url('https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=160&h=160&fit=crop&crop=faces,edges&q=80&auto=format')}
  .aside p{font-size:calc(13.5*var(--k));line-height:calc(16.3*var(--k));font-weight:400;
    letter-spacing:.004em;color:#26272A}

MAP CONTROLS — .fab, a vertical glass capsule top-right of the hero:
  .fab{position:absolute;left:91.03%;top:3.59%;width:calc(71.5*var(--k));
    height:calc(153*var(--k));border-radius:999px;background:rgba(32,33,30,.66);
    backdrop-filter:blur(10px)}
  .fab__plus,.fab__layers{position:absolute;left:50%;transform:translateX(-50%);
    border-radius:50%;display:grid;place-items:center}
  .fab__plus{top:calc(17*var(--k));width/height:calc(38*var(--k));background:#FBFBFA}
    aria-label="Zoom in"; svg calc(16*var(--k)); viewBox 0 0 24 24, fill none,
    stroke #141517, stroke-width 2.6, round caps: <path d="M12 4v16M4 12h16"/>
  .fab__layers{top:calc(92*var(--k));width/height:calc(49*var(--k));background:#1B1C1E}
    aria-label="Map layers"; svg calc(21*var(--k)); viewBox 0 0 24 24, fill none,
    stroke #fff, stroke-width 1.7, round caps/joins:
      <path d="M12 3.2 21.4 8 12 12.8 2.6 8Z"/>
      <path d="M3.4 12.6 12 17l8.6-4.4"/>
      <path d="M3.4 16.6 12 21l8.6-4.4"/>

FOCUS: a:focus-visible,button:focus-visible{outline:calc(2*var(--k)) solid #F2702E;outline-offset:calc(2*var(--k))}

=====================================================================
10. THE ENTRANCE ANIMATION — architecture rules (obey these exactly)
=====================================================================
One choreographed sequence, ~1.8s, that runs ONCE and then removes itself completely.
Built on the Web Animations API. No library, no ScrollTrigger, no IntersectionObserver,
no rAF loop, no setInterval, no listeners left behind.

GATE: an inline <script> in <head>, BEFORE first paint, adds class "js-entrance" to
<html> only when BOTH are true: prefers-reduced-motion is NOT reduce, AND
'animate' in Element.prototype. Wrap in try/catch. If JS never runs, the class is never
added and the page simply renders finished — the design must never depend on JS.

THE TWO-PRIMITIVE RULE (this is the important part — a naive fade breaks the glass):
About two thirds of this design is backdrop-filter glass. Such an element CANNOT be
revealed with opacity or transform, because:
  - opacity<1 ON the element composites its filtered result against the UNfiltered
    backdrop and drops the border-radius clip while it does, so a 999px pill renders as
    a blurred rectangle for the whole fade.
  - opacity<1 on an ANCESTOR creates a backdrop root, so descendants sample an empty
    backdrop and the glass goes flat.
  - transform re-filters a 30-40px kernel at a new offset every frame.
Therefore:
  SURFACES (anything with backdrop-filter) are revealed by clip-path ONLY.
  CONTENT (no backdrop-filter) is revealed by opacity and transform.
And WHERE the clip sits matters as much as which property it is: any grouping property
(opacity, filter, mask, clip-path, transform, mix-blend-mode, isolation, contain) makes
the element carrying it a backdrop root for everything beneath. So put every wipe ON THE
GLASS ELEMENT ITSELF, never on a container of it. Consequently .hero, .rail and .nav
must NOT animate at all — each wraps glass. The video never moves either.

Two machine-checkable clauses:
  1. no grouping property on any ANCESTOR of a backdrop-filter element
  2. on the glass itself, clip-path only — never opacity, never transform

INITIAL STATES (all scoped to .js-entrance so they vanish with the class):
  .js-entrance .nav .pill, .js-entrance .nav .iconbtn, .js-entrance .rail > .card,
  .js-entrance .btn, .js-entrance .riders, .js-entrance .fab
      { clip-path: inset(-25% 100% -25% -25%) }     /* only the wiping edge is at 100% */
  .js-entrance .pitch h1 .ln__i { transform: translateY(120%) }
  .js-entrance .aside p         { opacity:0; transform:translateY(calc(10*var(--k))) }
  .js-entrance .marker          { opacity:0; transform:scale(.86) }
  .js-entrance .stat__num, .js-entrance .gauge__value, .js-entrance .ej__num { overflow:hidden }
  .js-entrance .stat__num, .js-entrance .ej__num
      { padding-bottom:calc(2*var(--k)); margin-bottom:calc(-2*var(--k)) }
  .js-entrance .rise            { transform: translateY(130%) }
  .js-entrance .brand, .js-entrance .gauge__title, .js-entrance .ej__title
      { opacity:0; transform:translateY(calc(-6*var(--k))) }
  .js-entrance .stat__label, .js-entrance .bar
      { opacity:0; transform:translateY(calc(8*var(--k))) }
  .js-entrance .dot  { opacity:0; transform:scale(.4) }
  .js-entrance .leaf { opacity:0; transform:scale(.8) }
  .js-entrance .gauge__ring { opacity:0; transform:translate(-50%,-50%) scale(.92) }
  .js-entrance .stat__rule  { opacity:0; transform:scaleY(.08); transform-origin:top center }
  .js-entrance .bar__fill   { transform:scaleX(.04); transform-origin:left center }

HEADLINE MASK FIX: the h1's line-height is tighter than its em box, so "possibility" hangs
about 2 units below the line box and the mask would shave the descenders. Add slack with
padding and take it back with a negative margin — but as plain blocks those margins would
collapse and add 9 units between the lines, so make the h1 a flex column for the duration:
  .js-entrance .pitch h1{display:flex;flex-direction:column}
  .js-entrance .pitch h1 .ln{overflow:hidden;padding-bottom:calc(4*var(--k));margin-bottom:calc(-4*var(--k))}

BELT-AND-BRACES: inside @media (prefers-reduced-motion:reduce), reset every one of the
above initial states to {opacity:1;transform:none} (clip-path:none for the surfaces,
and translate(-50%,-50%) for .gauge__ring) so nothing can ever be left hidden.
Also globally: @media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}

=====================================================================
11. THE ENTRANCE TIMELINE — exact numbers
=====================================================================
Two easings only:
  EXPO = cubic-bezier(.16, 1, .3, 1)    surfaces + typography
  SOFT = cubic-bezier(.33, 1, .68, 1)   supporting content

Every tween is a SINGLE keyframe (one "to" object) with fill:'both', composite:'replace'.
A single keyframe makes the browser read the element's current computed style as the
implicit "from", so distances stay in --k units and stay correct at every breakpoint
without being restated.

Keyframe targets:
  SHOW = {opacity:1, transform:'none'}
  RISE = {transform:'none'}
  RING = {opacity:1, transform:'translate(-50%,-50%)'}
  FILL = {transform:'scaleX(1)'}
  WIPE = {clipPath:'inset(-25% 0% -25% -25%)'}

Before wiping any glass, filter by an onScreen() predicate that reads getComputedStyle and
returns display!=='none' && visibility!=='hidden'. This covers every layout without
branching on the breakpoint (the burger is display:none on desktop; the pills inherit
visibility:hidden from the closed panel on tablet/mobile).

SURFACES
  .nav .pill, .nav .iconbtn  (document order, filtered by onScreen)
                              WIPE  delay i*45        duration 520  EXPO
  .rail > .card               WIPE  delay 80 + i*90   duration 460  EXPO
  .btn                        WIPE  delay 880         duration 560  EXPO
  .riders                     WIPE  delay 980         duration 520  EXPO
  .fab                        WIPE  delay 1020        duration 500  EXPO

CARD INTERIORS — each list is scoped to its card and offset by the card's base time,
so content starts only once the rail wipe has passed over it. The 6th value is a
per-element stagger inside the selector.
  .card--dark      base 580:
      .brand        SHOW  +0    480  SOFT
      .dot          SHOW  +55   360  SOFT   stagger 45 per dot
      .rise         RISE  +130  780  EXPO          (the "4K")
      .stat__rule   SHOW  +210  520  SOFT
      .stat__label  SHOW  +240  480  SOFT
  .card--gauge     base 660:
      .gauge__title SHOW  +0    460  SOFT
      .gauge__ring  RING  +55   720  EXPO
      .rise         RISE  +130  780  EXPO          (the "-42")
  .card--electric  base 740:
      .ej__title    SHOW  +0    460  SOFT
      .leaf         SHOW  +55   520  SOFT
      .rise         RISE  +110  780  EXPO          (the "88%")
      .bar          SHOW  +190  520  SOFT
      .bar__fill    FILL  +270  760  EXPO          (the value sweeps in)

CONTENT
  .pitch h1 .ln__i  RISE  delay 460 + i*90   duration 950  EXPO   (masked line reveal)
  .aside p          SHOW  delay 1060         duration 560  SOFT
  .marker           SHOW  delay 1180 + i*70  duration 460  SOFT   (the pins bloom last)

Last frame lands at ~1780ms. Overlaps are deliberate: the rail is still settling when the
headline starts, and the headline is still resolving when the CTA arrives, so the sequence
reads as one gesture rather than a queue.

Direction is derived from anchoring, never chosen at random: top-anchored chrome descends,
bottom-anchored copy rises, surfaces do not travel at all, and the video never moves.

TEARDOWN: push every animation into one array. Guard with root.dataset.entranceRan so it
can never fire twice. Attach .finished.then(finish, finish) to each; when the last settles,
FIRST remove the js-entrance class, THEN cancel every animation — same frame, so the
authored style is already underneath and nothing flashes. Empty the array. After ~1.8s the
document holds no animation objects at all and the page is exactly the static design.
Start on DOMContentLoaded if readyState is 'loading', otherwise start immediately.

=====================================================================
12. THE OTHER THREE SCRIPTS
=====================================================================
(a) MARKER PINNING — keeps the four orange overlays locked to the same point of the media
    at any viewport, reproducing object-fit:cover maths in JS.
      var IW = 1534, IH = 1025;                      /* source frame space */
      var BW = 1.003125, BH = 1.005807, BL = 0, BT = -0.002345;   /* the .scene__img box */
      pins, as [selector, sx, sy] in source pixels:
        ['.marker--bike',  449.2, 405.0]
        ['.hero__mark',    740.8, 461.1]
        ['.marker--bolt', 1277.2, 465.9]
        ['.marker--pin',  1454.0, 479.1]
      place():
        w=innerWidth, h=innerHeight
        bw=w*BW, bh=h*BH
        s = Math.max(bw/IW, bh/IH)                  /* cover */
        ox = w*BL + (bw - IW*s)/2                   /* object-position 50% 50% */
        oy = h*BT + (bh - IH*s)/2
        el.style.left = (ox + sx*s).toFixed(1)+'px'
        el.style.top  = (oy + sy*s).toFixed(1)+'px'
      Call place() immediately; re-run on resize debounced by 60ms with {passive:true};
      also re-run on document.fonts.ready if available.

(b) BURGER MENU — state only. CSS owns the breakpoint entirely; this script must not
    measure or listen to the viewport.
      setOpen(open): nav.classList.toggle('is-open', open) and
                     btn.setAttribute('aria-expanded', open ? 'true':'false')
      burger click -> stopPropagation, toggle
      click inside the panel on an <a> (e.target.closest('a')) -> close (link still follows)
      click anywhere outside .nav -> close
      Escape while open -> close and return focus to the burger

(c) REDUCED-MOTION VIDEO HOLD — the page's motion policy is "reduce means still", and CSS
    cannot pause a video. Query '(prefers-reduced-motion: reduce)'; when it matches,
    removeAttribute('autoplay') and pause() the .scene__img; when it does not, call
    .play().catch(noop). Apply once, then subscribe to the query's change event
    (addEventListener with an addListener fallback). Wrap in try/catch. Without scripting
    the video simply plays, which is the graceful direction to fail in.

=====================================================================
13. RESPONSIVE — four tiers, by the SHAPE of the space, not by device
=====================================================================
The desktop composition is one proportional system, so it never breaks structurally — it
goes illegible. Measured, it holds down to --k 0.875 (1120x747); at 1024x768 the smallest
label is 10.8px, too small to touch. So the tablet tier starts at 1119px, not a device width.

TIER A — BURGER (applies to both tablet arrangements and to phones):
@media (max-width:1119px), (max-height:480px){
  .nav__burger{display:grid}
  .nav__links{display:flex;flex-direction:column;gap:calc(7*var(--k));
    position:absolute;z-index:6;top:calc(100% + 9*var(--k));left:calc(5*var(--k));
    width:calc(268*var(--k));padding:calc(11*var(--k));border-radius:var(--r-card);
    background:var(--glass-dark);backdrop-filter:blur(38px);
    border:1px solid rgba(255,255,255,.14);
    box-shadow:0 calc(6*var(--k)) calc(26*var(--k)) rgba(12,14,18,.34);
    opacity:0;visibility:hidden;transform:translateY(calc(-6*var(--k)));
    transition:opacity .22s ease, transform .22s ease, visibility .22s}
  .nav.is-open .nav__links{opacity:1;visibility:visible;transform:none}
  .nav__links .pill{width:100%;max-width:none;flex:0 0 auto;height:calc(52*var(--k))}
  .nav__links .pill--b,.nav__links .pill--c,.nav__links .pill--cta{margin-left:0}
  .nav__links .pill--cta{padding-left:calc(24*var(--k))}
  .nav__links .grid4{right:calc(20*var(--k))}
  .nav{position:relative;overflow:visible}
  .main{overflow:visible}
}

TIER B — LANDSCAPE TABLET (keep the desktop architecture on a tighter frame; trimming the
outer margins and the rail from 265.5 to 250 units drops the design width 1280->1180,
buying back ~9% of --k at the same viewport):
@media (min-width:701px) and (max-width:1119px) and (min-height:481px) and (min-aspect-ratio:6/5){
  :root{--k: max(0.5px, min(100vw / 1180, 100vh / 800));
        --k: max(0.5px, min(100vw / 1180, 100dvh / 800));}
  .stage{grid-template-columns:8fr 250fr 8fr 906fr 8fr;    /* = 1180 */
         grid-template-rows:8fr 776fr 16fr}                /* = 800  */
  .rail{grid-template-rows:248fr 3fr 247fr 4fr 274fr}
}

TIER C — PORTRAIT TABLET (hero takes the height, rail becomes a row beneath it; --k is
solved so the three cards land back on their designed 265.5-unit width:
3 x 265.5 + 2 x 10 gaps + 2 x 14 margins = 844.5 units across the viewport):
@media (min-width:701px) and (max-width:1119px) and (min-height:481px) and (max-aspect-ratio:6/5){
  :root{--k: clamp(.82px, 100vw / 845, 1.34px)}
  .stage{grid-template-columns:calc(14*var(--k)) 1fr calc(14*var(--k));
         grid-template-rows:calc(14*var(--k)) auto calc(12*var(--k)) 1fr calc(12*var(--k)) auto calc(14*var(--k))}
  .main{grid-column:2;grid-row:2 / span 3;display:contents}
  .nav{grid-column:2;grid-row:2;height:calc(64*var(--k));padding-left:0}
  .nav__links{left:0}
  .hero{grid-column:2;grid-row:4;min-height:calc(320*var(--k))}
  .fab{left:auto;right:calc(20*var(--k));top:calc(20*var(--k))}
  .pitch h1{font-size:calc(46*var(--k));line-height:calc(38*var(--k))}
  .rail{grid-column:2;grid-row:6;grid-template-columns:1fr 1fr 1fr;grid-template-rows:1fr;
        gap:calc(10*var(--k));height:calc(276*var(--k))}
  .card--dark{grid-column:1;grid-row:1} .card--gauge{grid-column:2;grid-row:1}
  .card--electric{grid-column:3;grid-row:1}
}

TIER D — PHONE / SHORT LANDSCAPE. The columns re-flow instead of shrinking the type to
nothing, and the frame still fills the viewport exactly.
@media (max-width:700px), (max-height:480px){
  :root{--k: clamp(.62px, min(100vw / 430, 100vh / 900), 1.35px)}
  .stage{grid-template-columns:calc(8*var(--k)) 1fr calc(8*var(--k));
         grid-template-rows:calc(8*var(--k)) auto calc(10*var(--k)) 1fr calc(10*var(--k)) auto calc(10*var(--k))}
  .main{grid-column:2;grid-row:2 / span 3;display:contents}
  .rail{grid-column:2;grid-row:6;display:grid;grid-template-columns:1fr 1fr 1fr;
        grid-template-rows:1fr;gap:calc(8*var(--k));height:calc(186*var(--k))}
  .card--dark,.card--gauge,.card--electric{grid-row:1}
  .card--dark{grid-column:1} .card--gauge{grid-column:2} .card--electric{grid-column:3}

  .nav{grid-column:2;grid-row:2;height:calc(56*var(--k));padding-left:0}
  /* --k bottoms out at .62 on short phones, which would put menu labels under 10px —
     these px floors hold the panel legible and tappable without touching wider layouts */
  .nav__links{left:0;width:max(240px, calc(268*var(--k)));padding:max(9px, calc(11*var(--k)))}
  .nav__links .pill{height:max(42px, calc(52*var(--k)));font-size:max(14px, calc(15*var(--k)))}
  .nav__links .pill--cta{font-size:max(15px, calc(16*var(--k)))}
  .pill{font-size:calc(15*var(--k));flex:0 0 auto;flex-basis:auto;max-width:none}
  .pill--home{width:auto;padding:0 calc(26*var(--k))}
  .pill--a,.pill--b,.pill--c{width:auto;padding:0 calc(22*var(--k));margin-left:0}
  .pill--cta{width:auto;padding:0 calc(52*var(--k)) 0 calc(24*var(--k));margin-left:0;font-size:calc(16*var(--k))}
  .grid4{right:calc(20*var(--k))}
  .iconbtn{width:calc(56*var(--k));height:calc(56*var(--k))}
  .iconbtn:first-of-type{margin-left:calc(10*var(--k))}

  /* Hero: .pitch and .aside become bottom-anchored flex items that wrap only when the
     width genuinely runs out. A portrait phone stacks them; a landscape phone keeps the
     desktop's two-column bottom row. NEITHER IS EVER HIDDEN — the riders chip and the
     paragraph stay in every layout. */
  .hero{grid-column:2;grid-row:4;min-height:calc(180*var(--k));
        display:flex;flex-wrap:wrap;align-items:flex-end;align-content:flex-end;
        gap:calc(20*var(--k)) calc(24*var(--k));padding:0 calc(26*var(--k)) calc(22*var(--k))}
  .scene__img{object-position:58% 50%}
  .pitch{position:static;left:auto;right:auto;bottom:auto;flex:1 1 calc(300*var(--k))}
  .pitch h1{font-size:calc(43*var(--k));line-height:calc(37*var(--k));letter-spacing:0;
            margin-bottom:calc(15*var(--k))}
  .btn{width:calc(258*var(--k));height:calc(48*var(--k))}
  .btn__disc{width:calc(39*var(--k));height:calc(39*var(--k))}
  .btn__label{margin-left:calc(26*var(--k));font-size:calc(14*var(--k))}
  .aside{position:static;left:auto;bottom:auto;flex:1 1 calc(300*var(--k));
         width:auto;max-width:calc(400*var(--k))}
  /* right:0 re-anchors the controls to the hero padding box so they line up with the
     text column edge instead of floating 10 units past it */
  .fab{left:auto;right:0;top:calc(16*var(--k));width:calc(62*var(--k));height:calc(132*var(--k))}
  .fab__plus{top:calc(13*var(--k));width:calc(36*var(--k));height:calc(36*var(--k))}
  .fab__layers{top:calc(76*var(--k));width:calc(44*var(--k));height:calc(44*var(--k))}
  .marker--bike{left:26%;top:36%} .marker--bolt{left:80%;top:43%} .marker--pin{left:92%;top:45%}

  /* COMPACT STAT CARDS — the desktop cards place every element absolutely at its measured
     pixel, and that does not survive being shrunk into a third of a phone screen. So the
     narrow layout switches them to real centred stacks instead. */
  .card--dark,.card--electric{display:flex;flex-direction:column;align-items:center;
    justify-content:center;gap:calc(7*var(--k));padding:calc(11*var(--k));text-align:center}
  .brand,.dots,.stat__num,.stat__rule,.stat__label,.ej__title,.leaf,.ej__num,.bar{
    position:static;left:auto;right:auto;top:auto;bottom:auto;margin:0}
  /* the same hairline divider, laid ACROSS the stack rather than beside it */
  .stat__rule{width:calc(46*var(--k));height:1px;flex:0 0 auto}
  .js-entrance .stat__rule{transform:scaleX(.08);transform-origin:center}
  .brand{justify-content:center}
  .brand svg{width:calc(26*var(--k));height:calc(18*var(--k))}
  .brand .word{font-size:calc(21*var(--k));line-height:calc(11.9*var(--k));
    letter-spacing:.02em;margin-left:calc(7*var(--k));top:0;transform:none}
  .stat__num{font-size:calc(43.5*var(--k));line-height:calc(24.7*var(--k));transform:none}
  .stat__label{font-size:calc(11.5*var(--k));line-height:calc(13.5*var(--k));
    letter-spacing:.01em;text-align:center}
  /* gauge: ring and value share one centre point so they can never drift apart */
  .gauge__title{top:calc(10*var(--k));font-size:calc(13*var(--k))}
  .gauge__ring{left:50%;top:calc(50% + 7*var(--k));transform:translate(-50%,-50%);
    width:calc(92*var(--k));height:calc(92*var(--k))}
  .gauge__value{top:calc(50% + 7*var(--k));transform:translateY(-50%);
    font-size:calc(27*var(--k));letter-spacing:.02em}
  .ej__title{font-size:calc(13*var(--k));line-height:calc(14*var(--k));
    letter-spacing:.01em;transform:none}
  .leaf{width:calc(28*var(--k));height:calc(28*var(--k))}
  .leaf svg{width:calc(14*var(--k));height:calc(14*var(--k))}
  .ej__num{font-size:calc(32.5*var(--k));line-height:calc(18.7*var(--k));transform:none}
  .bar{width:calc(100% - 4*var(--k));height:calc(13*var(--k));
       border-radius:calc(3*var(--k));flex:0 0 auto}
}

=====================================================================
14. ACCEPTANCE CHECKLIST — verify all of these before returning the file
=====================================================================
[ ] One self-contained .html file. No frameworks, no libraries, no build step.
[ ] The page never scrolls, at any viewport, in any orientation.
[ ] The video uses the two CloudFront URLs above verbatim; no local media file anywhere.
[ ] Every glass panel keeps its rounded corners throughout the entrance (clip-path only —
    a pill that flashes as a blurred rectangle means the two-primitive rule was broken).
[ ] No grouping property (opacity/filter/mask/clip-path/transform/mix-blend-mode/
    isolation/contain) on .nav, .rail or .hero at any time.
[ ] After ~1.8s the js-entrance class is gone and no animation objects remain.
[ ] With prefers-reduced-motion: reduce — no entrance runs, nothing is left hidden, and
    the background video is paused on its first frame.
[ ] With JavaScript disabled the page renders in its finished state and the video plays.
[ ] On phone widths the riders chip and the paragraph are both still visible (never hidden).
[ ] The burger panel opens/closes on click, on outside click, on link click, and on Escape,
    with aria-expanded tracking the state.
[ ] The four orange map overlays stay pinned to the same point of the frame across resize.
[ ] Focus rings are visible on every link and button.
[/hidecontent]
← 返回首页