open-design/design-systems/airbnb/components.html
chaoxiaoche e62b87b62c
feat(design-systems): add structured tokens for cursor, apple, stripe, airbnb, vercel brands (#1652)
* feat(design-systems): add structured tokens for cursor brand

Introduces design-systems/cursor/tokens.css + components.html, the
third structured brand after default and kami. Brings the prose
DESIGN.md into the schema-checked form so agents picking the cursor
design system get the structured token channel (PR-D #1544) instead
of having to re-derive Cursor's warm-cream palette, three-voice
typography (CursorGothic / jjannon / berkeleyMono), oklab borders,
and depth-based focus from prose every turn.

Why now: PR-D made the structured channel default-on, but it only
materially helps the 2 brands shipping tokens.css today. To expand
batch-design-system-test (#1515) coverage beyond default + kami,
we are hand-authoring 5 high-profile brands (cursor first as
template, then apple / stripe / airbnb / vercel) before the bulk
derive script (PR-B). These five also serve as the byte-identical
oracle the future derive script must reproduce.

Brand-specific schema decisions, captured in the tokens.css header:
- Surface: 3-tier cream ladder (#f2f1ed → #ebeae5 → #e6e5e0).
  --surface-warm binds to a real intermediate tier, not aliased.
- Foreground: 4-tier ramp via rgba(38,37,30,a) at 1.0 / 0.9 / 0.55
  / 0.4 — Cursor genuinely has the richer ramp.
- Borders: oklab origins shipped as rgba fallbacks per DESIGN.md
  §Agent Prompt Guide rule 3.
- --tracking-display: -0.03em (the 72px hero value normalized to
  em). Smaller display sizes scale tracking proportionally inline.
- --elev-raised: signature 28/70px diffused atmospheric shadow.
- --focus-ring: depth-only (0 4px 12px), never the cool-blue halo.
- --ease-standard: ease (per DESIGN.md), not the schema cubic-bezier.
- --danger: warm crimson #cf2d56, used for the brand-signature
  hover-text-shift on buttons.

components.html demonstrates the full surface (hero with 72px
CursorGothic + jjannon serif lead, three-voice typography, warm-
surface CTA with crimson hover, accent-orange pill CTA used once,
oklab-fallback bordered cards, AI timeline signature element with
the warm pastel state colors, jjannon-serif input with 4-12 depth
focus). :root paste is byte-identical to tokens.css.

Verified:
- pnpm guard — 13/13 checks pass, including all 8 design-system
  sub-checks now reporting "3 brands" (default + kami + cursor):
  - token-fixture sync: 3 brand pairs aligned
  - A1/A2/B-slot required tokens: all 3 brands declare
    26 A1 + 26 A2 + 4 B-slot
  - unknown token allowlist: 185 declarations across 3 brands all
    match shared schema or brand extensions
  - flag parity: 146 prose-only brands byte-identical, 3 structured
    diverge as expected

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(design-systems): correct h2/h3 line-height in cursor components fixture

Per DESIGN.md §Hierarchy, 36px section headings use line-height 1.20
and 26px sub-headings use 1.25. The shared h1/h2/h3 rule was locking
all three to --leading-tight (1.10), baking wrong vertical rhythm into
the oracle fixture.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(design-systems): add structured tokens for apple, stripe, airbnb, vercel brands

Batch follow-up to #1652 (cursor). Adds tokens.css + components.html
for the remaining four high-profile brands, using cursor as the
format/template oracle.

Each brand encodes its specific identity:
- apple: SF Pro tight-compressed display (--leading-tight 1.05),
  system-blue accent, SF Symbols grid, frosted glass surfaces
- stripe: sohne-var weight-300 anti-convention signature, --leading-tight
  1.10 (matches all heading spec values exactly), indigo accent
- airbnb: Cereal weight-700 display with Rausch coral, --leading-tight
  1.20 with per-size overrides (h2→1.18, h3→1.25)
- vercel: Geist 600 compressed display, --leading-tight 1.10 with
  per-size overrides (h2→1.20, h3→1.33 per DESIGN.md §Typography)

pnpm guard: 13/13 checks pass; flag-parity now shows 6 structured
brands and 143 prose-only brands.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 16:53:51 +08:00

1373 lines
56 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Airbnb — reference components</title>
<meta
name="description"
content="Reference fixture for design-systems/airbnb. Every visible
value comes from tokens.css; the page itself follows Airbnb's
rules — Canvas White surfaces, a single Rausch accent capped
at two visible uses per viewport, Cereal at weight 500 for
body, the signature 14-20px soft-circle radii, the three-
layer booking-panel elevation, and the 2px Ink Black focus
ring that survives full-bleed photography."
/>
<style>
/* :root paste — sourced verbatim from design-systems/airbnb/tokens.css. */
:root {
/* ─── Surface (3 levels) ────────────────────────────────────────
* Canvas White is both the page and the card — Airbnb refuses a
* tonal shift between the two. Cards earn their edges from the
* Hairline Gray border and from full-bleed photography. The
* tertiary tier (--surface-warm) is Soft Cloud, used only for
* footer backgrounds, map-view wrappers, and middle-of-range
* date cells in the date picker. */
--bg: #ffffff;
--surface: #ffffff;
--surface-warm: #f7f7f7;
/* ─── Foreground ramp (4 levels) ────────────────────────────────
* Ink Black (#222222) carries roughly ninety percent of every
* page — every heading, every body paragraph, every nav label,
* every price. The brand's near-black is never true #000000.
* Charcoal (--fg-2) is a one-step-down emphasis tier reserved
* for focused-input text. Ash Gray (--muted) is the secondary
* label — "Cottage rentals" subtitles, muted footer links.
* Mute Gray (--meta) is reserved for disabled buttons and
* low-priority metadata. */
--fg: #222222;
--fg-2: #3f3f3f;
--muted: #6a6a6a;
--meta: #929292;
/* ─── Border (2 levels) ─────────────────────────────────────────
* Hairline Gray (#dddddd) is the workhorse — every card-to-card
* divider, every amenity-row separator, every footer-column
* rule. The soft tier is a lighter hairline used to break up
* dense list interiors without competing with the primary card
* edge. */
--border: #dddddd;
--border-soft: #ebebeb;
/* ─── Accent ────────────────────────────────────────────────────
* Rausch coral-pink — the single signature color. Reserve for
* primary CTAs (Reserve, Search, Add dates), the active-tab
* underline, the wishlist heart fill, and price emphasis. Hard
* cap of two visible uses per viewport; if a third Rausch
* element shows up, neutralize one. Plus Magenta (#92174d) and
* Luxe Purple (#460479) are kept inline at their product-tier
* surfaces, not promoted to shared accent slots. */
--accent: #ff385c;
--accent-on: #ffffff;
--accent-hover: #e31c5f; /* hand-picked, between Rausch and Deep Rausch */
--accent-active: #e00b41; /* Deep Rausch — DESIGN.md §2 pressed-state value */
/* ─── Semantic ──────────────────────────────────────────────────
* Error Red is explicit from DESIGN.md §2; success and warn are
* not specified by the brand and are bound to desaturated values
* that survive the white canvas without competing with Rausch.
* Keep total semantic-color pixels well under five percent of
* any page — the brand almost never renders status. */
--success: #008a05; /* hand-picked warm green — available/in-stock */
--warn: #c47700; /* burnt amber — never tailwind yellow */
--danger: #c13515; /* DESIGN.md §2 Error Red */
/* ─── Typography — fonts ────────────────────────────────────────
* Airbnb Cereal VF is the proprietary face that carries every
* label from 8px superscript to 28px section heading. The
* documented fallback chain renders acceptably on macOS/iOS
* where system-ui resolves to San Francisco. Display and body
* share the same stack — the visual identity comes from the
* family itself, never from typeface mixing. Mono is a generic
* stack for kbd, tabular metrics, and the rare code label. */
--font-display:
"Airbnb Cereal VF", "Airbnb Cereal App", Circular,
-apple-system, system-ui, "Helvetica Neue", Roboto, sans-serif;
--font-body:
"Airbnb Cereal VF", "Airbnb Cereal App", Circular,
-apple-system, system-ui, "Helvetica Neue", Roboto, sans-serif;
--font-mono:
ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Monaco,
Consolas, monospace;
/* ─── Typography — type scale (px) ──────────────────────────────
* Derived from DESIGN.md §3 Hierarchy table. The system clusters
* tightly between 11 and 22px (the conversational range) and
* jumps to 28px for section headings; the higher tiers are used
* sparingly for the Guest Favorite rating lockup (4456px) and
* the rare hero numeral. No 400-regular: Cereal's body weight
* is 500, which any consuming component should set explicitly. */
--text-xs: 12px; /* micro / footer disclaimer */
--text-sm: 14px; /* button default, link, caption */
--text-base: 16px; /* body medium, button large, subtitle bold */
--text-lg: 20px; /* listing title */
--text-xl: 22px; /* subsection heading */
--text-2xl: 28px; /* section heading */
--text-3xl: 44px; /* Guest Favorite rating, hero numeral */
--text-4xl: 56px; /* display ceiling — Guest Favorite max */
/* ─── Typography — leading & tracking ───────────────────────────
* Tight for display (1.20 — chiseled headlines), generous for
* body (1.43 — magazine reading comfort). Display tracking
* compresses negatively at sizes ≥20px; body and caption hold
* at zero. The two-mode rhythm is what gives Airbnb pages
* their travel-magazine feel rather than a utility-app feel. */
--leading-body: 1.43;
--leading-tight: 1.2;
--tracking-display: -0.02em; /* applied at sizes ≥20px only */
/* ─── Spacing — base scale ──────────────────────────────────────
* 8px base unit per DESIGN.md §5. The brand uses fine-grained
* off-grid values (5.5, 10, 11, 18.5, 22) for pixel-perfect
* icon alignment, but the shared scale stays on the standard
* 4px grid — off-grid spacing belongs inline at the component
* level, not in shared tokens. */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-12: 48px;
/* ─── Section rhythm ────────────────────────────────────────────
* Section padding ranges 4864px on desktop, 2432px on mobile
* per DESIGN.md §5. We bind the upper bound on desktop because
* Airbnb's content-dense pages benefit from generous breathing
* room between content blocks; the lower phone bound keeps the
* vertical rhythm tight on small screens. */
--section-y-desktop: 64px;
--section-y-tablet: 48px;
--section-y-phone: 32px;
/* ─── Radius ────────────────────────────────────────────────────
* The brand's signature soft-circle geometry. 14px (--radius-md)
* is the workhorse — every listing card photograph, every
* amenity badge, every generic content container lands here.
* 20px (--radius-lg) is reserved for the booking panel and hero
* photo frames; 8px (--radius-sm) is the small radius for
* buttons, dropdowns, and inputs; --radius-pill (9999px) is the
* system's signature round geometry, applied to every circular
* icon button, every avatar, and the wishlist heart. */
--radius-sm: 8px;
--radius-md: 14px;
--radius-lg: 20px;
--radius-pill: 9999px;
/* ─── Elevation ─────────────────────────────────────────────────
* Airbnb forbids single drop shadows; the system uses stacked
* layered shadows or no shadow at all. Listing cards sit flat
* on the canvas. Booking panels, modals, and dropdowns use the
* signature three-layer stack — a 2% hairline ring, a 4% short
* blur, and a 10% medium blur — which reads as one cohesive
* lift while giving the perimeter a premium anti-aliased edge. */
--elev-flat: none;
--elev-ring: 0 0 0 1px var(--border);
--elev-raised:
rgba(0, 0, 0, 0.02) 0 0 0 1px,
rgba(0, 0, 0, 0.04) 0 2px 6px 0,
rgba(0, 0, 0, 0.1) 0 4px 8px 0;
/* ─── Focus ring ────────────────────────────────────────────────
* 2px solid Ink Black, NOT the schema's accent-tinted default.
* DESIGN.md §6 mandates this exact treatment so focus indicators
* read cleanly against full-bleed colorful photography — a
* Rausch-tinted ring would disappear against warm sunset shots
* or sea-blue beach overlays. Pair with a 4px white separator
* ring at circular-icon buttons that float on images. */
--focus-ring: 0 0 0 2px var(--fg);
/* ─── Motion ────────────────────────────────────────────────────
* DESIGN.md did not capture transition timings (static
* extraction scope). We bind defaults consistent with the
* brand's tactile feel: 150ms for micro-states (the famous
* `transform: scale(0.92)` pressed-button rebound) and 200ms
* for general state changes. Standard easing is the schema
* default — short, purposeful, no overshoot. */
--motion-fast: 150ms;
--motion-base: 200ms;
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
/* ─── Layout ────────────────────────────────────────────────────
* Detail pages cap at 1280px per DESIGN.md §5; the homepage
* listing grid lets the canvas breathe to 17601920px on
* ultra-wide. We bind the detail-page width as the shared
* default because that is the geometry agents will most often
* generate (rooms, experiences, host profiles). Gutters
* tighten progressively: 40px desktop, 24px tablet, 16px
* phone per DESIGN.md §8 small-mobile clause. */
--container-max: 1280px;
--container-gutter-desktop: 40px;
--container-gutter-tablet: 24px;
--container-gutter-phone: 16px;
}
/* ─── Reset (minimal) ───────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--fg);
font-family: var(--font-body);
font-size: var(--text-base);
font-weight: 500; /* Cereal's body weight — DESIGN.md §3 */
line-height: var(--leading-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ─── Layout primitives ─────────────────────────────────────── */
.container {
max-width: var(--container-max);
margin-inline: auto;
padding-inline: var(--container-gutter-desktop);
}
section {
padding-block: var(--section-y-desktop);
}
section + section {
border-top: 1px solid var(--border);
}
@media (max-width: 1023px) {
.container { padding-inline: var(--container-gutter-tablet); }
section { padding-block: var(--section-y-tablet); }
}
@media (max-width: 639px) {
.container { padding-inline: var(--container-gutter-phone); }
section { padding-block: var(--section-y-phone); }
}
/* ─── Typography ─────────────────────────────────────────────
* Display sizes ≥20px compress tracking negatively per
* DESIGN.md §3 ("Negative tracking on display type only").
* Body and caption sizes hold at zero tracking for
* readability. Weights: 500 body, 600 emphasis, 700 display
* — no 400 anywhere, no italic anywhere. */
h1, h2, h3 {
font-family: var(--font-display);
line-height: var(--leading-tight);
margin: 0;
}
h1 {
font-size: var(--text-2xl);
font-weight: 700;
letter-spacing: var(--tracking-display);
}
h2 {
font-size: var(--text-xl);
font-weight: 500;
/* 22px Subsection Heading: line-height 1.18 per DESIGN.md §Typography.
Overrides the shared 1.20 (--leading-tight) set on h1/h2/h3 above. */
line-height: 1.18;
letter-spacing: var(--tracking-display);
}
h3 {
font-size: var(--text-base);
font-weight: 600;
/* 16px Subtitle/Body size: line-height 1.25 per DESIGN.md §Typography.
Overrides the shared 1.20 (--leading-tight) set on h1/h2/h3 above. */
line-height: 1.25;
}
p { margin: 0; }
.lede {
font-size: var(--text-base);
line-height: var(--leading-body);
color: var(--fg);
}
.body-muted { color: var(--muted); }
.body-meta { color: var(--meta); font-size: var(--text-sm); }
.body-sm { font-size: var(--text-sm); }
/* The single uppercase moment in the system, per DESIGN.md §3
* Principles ("No all-caps except at 8px"). We elevate this
* to a 12px eyebrow for fixture legibility, with tracking
* well above the craft 0.06em floor. Used once per section. */
.eyebrow {
font-family: var(--font-display);
font-size: var(--text-xs);
font-weight: 700;
line-height: 1.33;
color: var(--fg);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.stack-2 > * + * { margin-block-start: var(--space-2); }
.stack-3 > * + * { margin-block-start: var(--space-3); }
.stack-4 > * + * { margin-block-start: var(--space-4); }
.stack-6 > * + * { margin-block-start: var(--space-6); }
/* ─── Buttons ────────────────────────────────────────────────
* Primary CTA is filled Rausch on Canvas White text. Active/
* pressed uses Deep Rausch as the published brand value
* (DESIGN.md §4 Primary CTA) combined with the famous
* `transform: scale(0.92)` rebound that gives the brand its
* tactile feel. Secondary is a Hairline Gray outlined pill
* over the canvas. Both share the 2px Ink Black focus ring
* — see the #Bind 6 rationale in tokens.css. */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: 14px 24px;
border: 1px solid transparent;
border-radius: var(--radius-sm);
font-family: var(--font-display);
font-weight: 500;
font-size: var(--text-base);
line-height: 1;
cursor: pointer;
text-decoration: none;
transition:
transform var(--motion-fast) var(--ease-standard),
background-color var(--motion-fast) var(--ease-standard),
border-color var(--motion-fast) var(--ease-standard),
box-shadow var(--motion-fast) var(--ease-standard);
}
.btn:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
.btn:active {
transform: scale(0.96); /* approaching DESIGN.md §4's scale(0.92) */
}
.btn-primary {
background: var(--accent);
color: var(--accent-on);
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-primary:active {
background: var(--accent-active);
transform: scale(0.92);
}
.btn-secondary {
background: var(--surface);
color: var(--fg);
border-color: var(--border);
border-radius: var(--radius-lg); /* 20px pill — DESIGN.md §4 Secondary */
}
.btn-secondary:hover {
border-color: var(--fg);
}
/* Compact button — listing-card price row, secondary nav links */
.btn-sm {
padding: 10px 16px;
font-size: var(--text-sm);
}
/* Circular icon button — the brand's signature geometry
* (DESIGN.md §4 Icon-Only Circular Button). Used for back,
* share, favorite, and carousel controls. The 4px white
* separator ring on `:hover` is what separates the button
* from colorful photography backgrounds. */
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
border: 1px solid var(--border);
border-radius: var(--radius-pill);
background: var(--surface);
color: var(--fg);
cursor: pointer;
transition:
transform var(--motion-fast) var(--ease-standard),
box-shadow var(--motion-fast) var(--ease-standard);
}
.btn-icon:hover {
box-shadow: 0 0 0 4px var(--surface), var(--elev-ring);
}
.btn-icon:active {
transform: scale(0.92);
}
.btn-icon:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
.btn-icon svg { width: 16px; height: 16px; }
/* ─── Inputs ─────────────────────────────────────────────────
* Generic text input per DESIGN.md §4 Text Input — 1px
* Hairline Gray edge, 8px radius, 14px·16px padding. Focus
* switches the edge to Ink Black and adds the 2px outer
* ring. Error state swaps to Error Red on both border and
* helper text. */
.field {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.field label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--fg);
}
.field input,
.field select {
padding: 14px 16px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--surface);
color: var(--fg);
font-family: inherit;
font-weight: 500;
font-size: var(--text-base);
outline: none;
transition:
border-color var(--motion-fast) var(--ease-standard),
box-shadow var(--motion-fast) var(--ease-standard);
}
.field input:focus-visible,
.field select:focus-visible {
border-color: var(--fg);
box-shadow: var(--focus-ring);
color: var(--fg-2);
}
.field input::placeholder { color: var(--muted); }
.field-help {
font-size: var(--text-xs);
color: var(--muted);
}
.field-error .field-help { color: var(--danger); }
.field-error input { border-color: var(--danger); }
/* Search pill — three-segment "Where / When / Who" with a
* Rausch circular submit button at the right edge. DESIGN.md
* §4 Search Bar. The 32px pill radius is one of the brand's
* signature radii. */
.search-pill {
display: inline-flex;
align-items: center;
gap: 0;
padding: 8px;
border: 1px solid var(--border);
border-radius: var(--radius-pill);
background: var(--surface);
box-shadow:
rgba(0, 0, 0, 0.04) 0 2px 6px 0;
max-width: 100%;
}
.search-segment {
display: flex;
flex-direction: column;
padding: 6px var(--space-5);
min-width: 0;
cursor: pointer;
}
.search-segment + .search-segment {
border-left: 1px solid var(--border);
}
.search-segment-label {
font-size: var(--text-xs);
font-weight: 600;
color: var(--fg);
line-height: 1.2;
}
.search-segment-value {
font-size: var(--text-sm);
font-weight: 500;
color: var(--muted);
line-height: 1.3;
}
.search-submit {
display: inline-flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
margin-left: var(--space-2);
border: none;
border-radius: var(--radius-pill);
background: var(--accent);
color: var(--accent-on);
cursor: pointer;
transition:
background-color var(--motion-fast) var(--ease-standard),
transform var(--motion-fast) var(--ease-standard);
}
.search-submit:hover { background: var(--accent-hover); }
.search-submit:active {
background: var(--accent-active);
transform: scale(0.92);
}
.search-submit:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
.search-submit svg { width: 16px; height: 16px; }
/* ─── Cards ──────────────────────────────────────────────────
* Two card shapes: the listing card (no border, no shadow,
* 14px image radius, text directly below the photo per
* DESIGN.md §4 Listing Card) and the booking-panel card
* (1px Hairline border, 20px radius, three-layer shadow
* stack — the system's most recognizable elevation). */
.listing {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.listing-photo {
position: relative;
aspect-ratio: 4 / 3;
border-radius: var(--radius-md);
overflow: hidden;
background: var(--surface-warm);
}
/* Photograph placeholder — gentle linear gradient inside the
* 14px radius. In production this slot is a 4:3 muscache CDN
* image with loading="lazy"; the fixture renders a tonal
* surface so the card geometry is still legible without art. */
.listing-photo::before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(
135deg,
var(--surface-warm) 0%,
var(--border) 60%,
var(--surface-warm) 100%
);
}
.listing-photo .btn-icon {
position: absolute;
top: var(--space-3);
right: var(--space-3);
z-index: 1;
}
.listing-photo .listing-badge {
position: absolute;
top: var(--space-3);
left: var(--space-3);
z-index: 1;
}
.listing-meta {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.listing-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
}
.listing-title {
font-size: var(--text-base);
font-weight: 600;
color: var(--fg);
}
.listing-subtitle {
font-size: var(--text-sm);
font-weight: 500;
color: var(--muted);
}
.listing-price {
font-size: var(--text-base);
font-weight: 600;
color: var(--fg);
}
.listing-price .per {
font-weight: 500;
color: var(--muted);
}
.listing-rating {
font-size: var(--text-sm);
font-weight: 500;
color: var(--fg);
font-variant-numeric: tabular-nums;
}
/* Booking panel — the brand's signature sticky elevation.
* Three-layer shadow + Hairline border + 20px radius +
* 24px padding. DESIGN.md §4 Detail Page Booking Panel. */
.booking-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--elev-raised);
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-4);
max-width: 370px;
}
.booking-headline {
display: flex;
align-items: baseline;
gap: var(--space-2);
}
.booking-price {
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 600;
color: var(--fg);
letter-spacing: var(--tracking-display);
}
.booking-per {
font-size: var(--text-base);
font-weight: 500;
color: var(--muted);
}
.booking-grid {
display: grid;
grid-template-columns: 1fr 1fr;
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
.booking-grid-cell {
padding: var(--space-3) var(--space-4);
display: flex;
flex-direction: column;
gap: 2px;
cursor: pointer;
background: var(--surface);
}
.booking-grid-cell + .booking-grid-cell {
border-left: 1px solid var(--border);
}
.booking-grid-cell.full {
grid-column: 1 / -1;
border-top: 1px solid var(--border);
border-left: none;
}
.booking-cell-label {
font-size: 10px;
font-weight: 700;
color: var(--fg);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.booking-cell-value {
font-size: var(--text-sm);
font-weight: 500;
color: var(--muted);
}
.booking-disclaimer {
font-size: var(--text-xs);
color: var(--muted);
text-align: center;
line-height: var(--leading-body);
}
/* ─── Badges ─────────────────────────────────────────────────
* Two shapes: the rounded brand badge (14px radius, used on
* listing photos as "NEW" or "Guest favorite" markers) and
* the dotted status badge (pill, used in metadata strips).
* Both stay under DESIGN.md §3's "no all-caps except 8px"
* rule by using sentence case at 12px·600. */
.badge {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: 6px var(--space-3);
border-radius: var(--radius-pill);
font-family: var(--font-display);
font-size: var(--text-xs);
font-weight: 600;
line-height: 1.2;
}
.badge-rausch {
background: var(--accent);
color: var(--accent-on);
border-radius: var(--radius-md);
}
.badge-soft {
background: var(--surface);
color: var(--fg);
border-radius: var(--radius-md);
box-shadow: var(--elev-ring);
}
.badge-status {
background: var(--surface-warm);
color: var(--fg);
}
.badge-status .badge-dot {
width: 6px;
height: 6px;
border-radius: var(--radius-pill);
background: var(--success);
}
/* ─── Links ──────────────────────────────────────────────────
* Body links inherit Ink Black, NOT Rausch. Rausch is
* reserved for buttons and the active-tab indicator;
* promoting it to link color would dilute the accent and
* trip the two-uses-per-viewport cap. Underline appears on
* hover at generous offset. */
a {
color: var(--fg);
text-decoration: underline;
text-underline-offset: 3px;
text-decoration-thickness: 1px;
text-decoration-color: var(--border);
transition: text-decoration-color var(--motion-fast) var(--ease-standard);
}
a:hover {
text-decoration-color: var(--fg);
}
a:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
border-radius: var(--radius-sm);
}
/* ─── Kbd ────────────────────────────────────────────────────
* Hairline outline + Soft Cloud fill + tabular mono. Used
* for the rare keyboard shortcut hint. */
kbd {
display: inline-block;
font-family: var(--font-mono);
font-size: var(--text-xs);
padding: 2px 6px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--surface-warm);
color: var(--fg);
line-height: 1.2;
font-variant-numeric: tabular-nums;
}
/* ─── Distinctive: Guest Favorite Award lockup ───────────────
* The brand's most recognizable moment (DESIGN.md §4
* Signature Components). A centered rating numeral at
* 4456px·700 flanked by two laurel wreath SVGs. Below the
* lockup: a 12px·700 uppercase label with 0.32px tracking
* (we widen to 0.06em — the craft floor) and a 14px·500
* Ash Gray sub-label. Full-width block, no border, sits
* directly on the canvas. */
.award {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: var(--space-3);
padding-block: var(--space-8);
}
.award-row {
display: flex;
align-items: center;
gap: var(--space-5);
color: var(--fg);
}
.award-rating {
font-family: var(--font-display);
font-size: var(--text-3xl);
font-weight: 700;
color: var(--fg);
line-height: 1;
letter-spacing: var(--tracking-display);
font-variant-numeric: tabular-nums;
}
.award-laurel {
width: 48px;
height: 64px;
color: var(--fg);
flex-shrink: 0;
}
.award-laurel.right { transform: scaleX(-1); }
.award-label {
font-family: var(--font-display);
font-size: var(--text-xs);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg);
line-height: 1;
}
.award-sub {
font-size: var(--text-sm);
font-weight: 500;
color: var(--muted);
max-width: 44ch;
line-height: var(--leading-body);
}
/* Tri-tab category picker (DESIGN.md §4 Tri-Tab) — the brand's
* other instantly-recognizable nav moment. We render with
* type-only icons (no 3D illustrations) because the fixture
* stays raster-free, but the underline + label rhythm holds. */
.tabs {
display: inline-flex;
gap: var(--space-8);
padding-block-start: var(--space-3);
border-bottom: 1px solid var(--border);
}
.tab {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
padding-block-end: var(--space-3);
font-family: var(--font-display);
font-size: var(--text-base);
font-weight: 500;
color: var(--muted);
background: none;
border: none;
cursor: pointer;
position: relative;
text-decoration: none;
}
.tab-glyph {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
color: var(--fg);
}
.tab[aria-selected="true"] {
color: var(--fg);
}
.tab[aria-selected="true"]::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -1px;
height: 2px;
background: var(--fg);
border-radius: var(--radius-pill);
}
.tab-new {
position: absolute;
top: -4px;
right: -10px;
padding: 2px 6px;
border-radius: var(--radius-md);
background: var(--fg);
color: var(--accent-on);
font-size: 10px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0.06em;
text-transform: uppercase;
}
/* ─── Section-specific layout ──────────────────────────────── */
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
gap: var(--space-12);
align-items: start;
}
@media (max-width: 1023px) {
.hero-grid { grid-template-columns: 1fr; gap: var(--space-8); }
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
margin-block-start: var(--space-6);
}
.listings-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-6);
margin-block-start: var(--space-8);
}
@media (max-width: 1023px) {
.listings-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 639px) {
.listings-grid { grid-template-columns: 1fr; gap: var(--space-4); }
}
.form-row {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
gap: var(--space-12);
align-items: start;
}
@media (max-width: 1023px) {
.form-row { grid-template-columns: 1fr; }
}
.form {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.row-between {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
}
.row-cluster {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
}
.divider-soft {
height: 1px;
background: var(--border-soft);
border: none;
margin: 0;
}
.nav-row {
display: flex;
align-items: center;
justify-content: space-between;
padding-block: var(--space-4);
gap: var(--space-4);
}
.nav-logo {
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: 700;
color: var(--accent);
letter-spacing: var(--tracking-display);
}
.icon { width: 16px; height: 16px; flex-shrink: 0; }
</style>
</head>
<body>
<main class="container">
<!-- ════════════════════════════════════════════════════════════
NAV — exercises: .nav-row, .nav-logo (Rausch wordmark),
.tabs (tri-tab picker, DESIGN.md §4 signature). Active
"Homes" tab gets the 2px Ink Black underline; "Experiences"
carries the small Ink Black NEW pill from §4.
═══════════════════════════════════════════════════════════════ -->
<nav class="nav-row" aria-label="Primary">
<span class="nav-logo">airbnb</span>
<div class="tabs" role="tablist">
<button class="tab" role="tab" aria-selected="true">
<span class="tab-glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" width="24" height="24">
<path d="M3 11l9-7 9 7v9a1 1 0 0 1-1 1h-5v-6h-6v6H4a1 1 0 0 1-1-1z" />
</svg>
</span>
Homes
</button>
<button class="tab" role="tab" aria-selected="false">
<span class="tab-glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" width="24" height="24">
<ellipse cx="12" cy="9" rx="6" ry="7" />
<path d="M9 16l-1 5 4-2 4 2-1-5" />
</svg>
</span>
Experiences
<span class="tab-new" aria-label="New">New</span>
</button>
<button class="tab" role="tab" aria-selected="false">
<span class="tab-glyph" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" width="24" height="24">
<path d="M6 16a6 6 0 1 1 12 0z" />
<path d="M12 6V3M5 18h14M11 19v2h2v-2" />
</svg>
</span>
Services
<span class="tab-new" aria-label="New">New</span>
</button>
</div>
<div class="row-cluster">
<button class="btn btn-secondary btn-sm" type="button">Become a host</button>
<button class="btn-icon" type="button" aria-label="Menu">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.75" stroke-linecap="round"
stroke-linejoin="round" aria-hidden="true">
<path d="M4 7h16M4 12h16M4 17h16" />
</svg>
</button>
</div>
</nav>
<!-- ════════════════════════════════════════════════════════════
HERO — exercises: .eyebrow, h1, .lede, .search-pill (the
three-segment Where/When/Who with the Rausch circular
submit, DESIGN.md §4), .btn-primary, .btn-secondary,
kbd, .badge-status (success dot).
═══════════════════════════════════════════════════════════════ -->
<section data-od-id="hero">
<div class="hero-grid">
<div class="stack-4">
<p class="eyebrow">Reference fixture · airbnb</p>
<h1 style="max-width: 18ch">
Find a place where the photograph is the interface.
</h1>
<p class="lede body-muted" style="max-width: 56ch">
A token system distilled from Airbnb's 2026 rules — Canvas
White surfaces, a single Rausch coral accent, generous
soft-circle radii, and Cereal at one family. The fixture
you are reading uses the same token block agents paste
into every artifact, so the rules survive the paste.
</p>
<div class="search-pill" role="search" aria-label="Find a stay">
<div class="search-segment">
<span class="search-segment-label">Where</span>
<span class="search-segment-value">Search destinations</span>
</div>
<div class="search-segment">
<span class="search-segment-label">When</span>
<span class="search-segment-value">Add dates</span>
</div>
<div class="search-segment">
<span class="search-segment-label">Who</span>
<span class="search-segment-value">Add guests</span>
</div>
<button class="search-submit" type="submit" aria-label="Search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<path d="M21 21l-4.3-4.3" />
</svg>
</button>
</div>
<div class="hero-actions">
<a href="./tokens.css" class="btn btn-primary">
View the token block
<svg
class="icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</a>
<a href="./DESIGN.md" class="btn btn-secondary">Read the brand</a>
</div>
</div>
<aside class="booking-panel" aria-label="Token status">
<div class="booking-headline">
<span class="booking-price">56 tokens</span>
<span class="booking-per">in :root</span>
</div>
<div class="booking-grid" role="group">
<div class="booking-grid-cell">
<span class="booking-cell-label">Last reviewed</span>
<span class="booking-cell-value">
<time datetime="2026-05-14">May 14, 2026</time>
</span>
</div>
<div class="booking-grid-cell">
<span class="booking-cell-label">Schema</span>
<span class="booking-cell-value">v0.1 · A1/A2/B</span>
</div>
<div class="booking-grid-cell full">
<span class="booking-cell-label">Accent budget</span>
<span class="booking-cell-value">≤ 2 visible Rausch uses per viewport</span>
</div>
</div>
<button type="button" class="btn btn-primary" style="width: 100%">
Inspect tokens
</button>
<p class="booking-disclaimer">
You won't be charged — this is a static fixture. Press
<kbd></kbd> <kbd>K</kbd> to jump to the token grep.
</p>
<div class="row-between" style="margin-block-start: var(--space-2)">
<span class="badge badge-status">
<span class="badge-dot" aria-hidden="true"></span>
Tokens passing schema
</span>
<span class="listing-rating" aria-label="Token coverage">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
style="vertical-align: -2px; margin-right: 4px"
>
<path d="M12 2l2.9 6.9 7.1.6-5.4 4.7 1.7 7L12 17l-6.3 4.2 1.7-7L2 9.5l7.1-.6z" />
</svg>
4.92
</span>
</div>
</aside>
</div>
</section>
<!-- ════════════════════════════════════════════════════════════
LISTINGS — exercises: .listing (4:3 photo, no border, no
shadow, 14px radius), .listing-photo with floated circular
favorite button, .badge-rausch, .listing-rating, links.
Three cards in a 3-col grid that reflows to 2 then 1.
═══════════════════════════════════════════════════════════════ -->
<section data-od-id="listings">
<div class="stack-3">
<p class="eyebrow">What the fixture exercises</p>
<h2 style="max-width: 32ch">
Three listings, every token resolved through var(--*).
</h2>
<p class="body-muted" style="max-width: 56ch">
No raw hex outside the :root paste. No second type family.
No drop shadow on the listing cards — separation comes from
the photograph and the 24px gutter between them.
</p>
</div>
<div class="listings-grid">
<article class="listing" aria-label="Cottage in the Cotswolds">
<div class="listing-photo">
<span class="listing-badge badge badge-soft">Guest favorite</span>
<button class="btn-icon" type="button" aria-label="Save to wishlist">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.75" stroke-linecap="round"
stroke-linejoin="round" aria-hidden="true">
<path d="M12 21s-7-4.5-7-10a4 4 0 0 1 7-2.7A4 4 0 0 1 19 11c0 5.5-7 10-7 10z" />
</svg>
</button>
</div>
<div class="listing-meta">
<div class="listing-row">
<span class="listing-title">Cotswolds, England</span>
<span class="listing-rating">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
style="vertical-align: -1px; margin-right: 3px"
>
<path d="M12 2l2.9 6.9 7.1.6-5.4 4.7 1.7 7L12 17l-6.3 4.2 1.7-7L2 9.5l7.1-.6z" />
</svg>
4.97
</span>
</div>
<span class="listing-subtitle">Cottage rentals · 2 hours by train</span>
<span class="listing-subtitle">Apr 12 17</span>
<span class="listing-price">
$182 <span class="per">/ night</span>
</span>
</div>
</article>
<article class="listing" aria-label="Loft in Lisbon">
<div class="listing-photo">
<span class="listing-badge badge badge-rausch">New</span>
<button class="btn-icon" type="button" aria-label="Save to wishlist">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.75" stroke-linecap="round"
stroke-linejoin="round" aria-hidden="true">
<path d="M12 21s-7-4.5-7-10a4 4 0 0 1 7-2.7A4 4 0 0 1 19 11c0 5.5-7 10-7 10z" />
</svg>
</button>
</div>
<div class="listing-meta">
<div class="listing-row">
<span class="listing-title">Lisbon, Portugal</span>
<span class="listing-rating">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
style="vertical-align: -1px; margin-right: 3px"
>
<path d="M12 2l2.9 6.9 7.1.6-5.4 4.7 1.7 7L12 17l-6.3 4.2 1.7-7L2 9.5l7.1-.6z" />
</svg>
4.81
</span>
</div>
<span class="listing-subtitle">Loft apartments · Alfama</span>
<span class="listing-subtitle">May 03 09</span>
<span class="listing-price">
$96 <span class="per">/ night</span>
</span>
</div>
</article>
<article class="listing" aria-label="Yacht tour in Mykonos">
<div class="listing-photo">
<span class="listing-badge badge badge-soft">Experience</span>
<button class="btn-icon" type="button" aria-label="Save to wishlist">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.75" stroke-linecap="round"
stroke-linejoin="round" aria-hidden="true">
<path d="M12 21s-7-4.5-7-10a4 4 0 0 1 7-2.7A4 4 0 0 1 19 11c0 5.5-7 10-7 10z" />
</svg>
</button>
</div>
<div class="listing-meta">
<div class="listing-row">
<span class="listing-title">Mykonos, Greece</span>
<span class="listing-rating">
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
style="vertical-align: -1px; margin-right: 3px"
>
<path d="M12 2l2.9 6.9 7.1.6-5.4 4.7 1.7 7L12 17l-6.3 4.2 1.7-7L2 9.5l7.1-.6z" />
</svg>
4.89
</span>
</div>
<span class="listing-subtitle">Small Group Yacht Tour</span>
<span class="listing-subtitle">4 hours · Unlimited fruit</span>
<span class="listing-price">
$148 <span class="per">/ person</span>
</span>
</div>
</article>
</div>
</section>
<!-- ════════════════════════════════════════════════════════════
GUEST FAVORITE AWARD — the brand's signature distinctive
component. Centered rating numeral at 44px·700 flanked by
two laurel wreath SVGs at 48×64. Below, an uppercase 12px·700
"GUEST FAVORITE" label and a 14px·500 Ash Gray sub-line.
Full-width block, no border, sits directly on the canvas
per DESIGN.md §4 Signature Components.
═══════════════════════════════════════════════════════════════ -->
<section data-od-id="award">
<div class="award" aria-label="Guest Favorite award">
<div class="award-row">
<svg
class="award-laurel"
viewBox="0 0 48 64"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M40 60 C 14 56, 6 38, 12 16" />
<path d="M30 56 C 18 50, 14 44, 14 36" />
<path d="M18 22 C 8 22, 6 14, 12 8" />
<path d="M20 30 C 8 30, 6 24, 10 18" />
<path d="M22 38 C 12 38, 8 32, 12 26" />
<path d="M26 46 C 16 46, 10 40, 14 34" />
<path d="M30 52 C 22 52, 16 48, 18 42" />
</svg>
<span class="award-rating">4.92</span>
<svg
class="award-laurel right"
viewBox="0 0 48 64"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M40 60 C 14 56, 6 38, 12 16" />
<path d="M30 56 C 18 50, 14 44, 14 36" />
<path d="M18 22 C 8 22, 6 14, 12 8" />
<path d="M20 30 C 8 30, 6 24, 10 18" />
<path d="M22 38 C 12 38, 8 32, 12 26" />
<path d="M26 46 C 16 46, 10 40, 14 34" />
<path d="M30 52 C 22 52, 16 48, 18 42" />
</svg>
</div>
<p class="award-label">Guest favorite</p>
<p class="award-sub">
One of the most loved tokensets on Open Design, according
to agents who paste it.
</p>
</div>
</section>
<!-- ════════════════════════════════════════════════════════════
FORM — exercises: .field (text input, focus rings the
border to Ink Black and adds the 2px outer ring per
DESIGN.md §4 Text Input), .field-error (Error Red),
.btn-primary, .btn-secondary, kbd. The right column re-
uses the .booking-panel pattern as a price summary.
═══════════════════════════════════════════════════════════════ -->
<section data-od-id="form">
<div class="form-row">
<div class="stack-4">
<p class="eyebrow">Form components</p>
<h2 style="max-width: 24ch">
Inputs inherit the same tokens.
</h2>
<p class="body-muted" style="max-width: 52ch">
Focus rings, borders, and placeholder color all derive
from --fg and --border. Submit reuses .btn-primary
unchanged. No new token introduced for this section —
if a value here is not in the :root paste above, it
does not belong in the artifact at all.
</p>
<hr class="divider-soft" />
<p class="body-meta">
Full reference at <a href="./tokens.css">tokens.css</a>
· brand at <a href="./DESIGN.md">DESIGN.md</a>.
</p>
</div>
<form class="form" onsubmit="event.preventDefault();">
<div class="field">
<label for="email">Travel notes email</label>
<input
id="email"
type="email"
placeholder="you@somewhere.travel"
autocomplete="email"
required
/>
<p class="field-help">
We'll send a single dispatch a season — never more.
</p>
</div>
<div class="field">
<label for="destination">Where</label>
<input
id="destination"
type="text"
placeholder="Search destinations"
autocomplete="off"
/>
</div>
<div class="field field-error">
<label for="dates">Dates</label>
<input
id="dates"
type="text"
placeholder="Add dates"
aria-invalid="true"
/>
<p class="field-help">Pick a check-in and check-out before you continue.</p>
</div>
<div class="row-cluster" style="margin-block-start: var(--space-2)">
<button type="submit" class="btn btn-primary">Send the dispatch</button>
<button type="button" class="btn btn-secondary">Skip for now</button>
</div>
</form>
</div>
</section>
</main>
</body>
</html>