类型:区块 · 收录:2026-09-23 19:21:52 · 源码:会员可见
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]