Foundations of Early Spring.
A working spec for building sites in the same style — tokens, voice, type, components, motion, and atmospheric effects. The page itself is the artifact: every pattern documented here is also rendered here.
The site should feel like a well-set page, not a brochure.
Editorial calm before everything: paper, sapphire, Lausanne, generous breath. The visitor should feel they've walked into a room — quiet, deliberately arranged, slightly under-lit — not been served a sales surface.
- Paper before texture.
- Ink before color.
- Type before image.
- Stillness before motion.
- One small surprise per page is enough.
Two colors. One typeface. Five sizes of breath.
Color
Two foundational colors: paper and sapphire. The other two entries are sapphire at reduced opacity. No greys. No black. No second hue. Use opacity, not new colors. Faded ink is reserved for soft emphasis (<em>), supporting metadata, and dimmed states.
Marks
/early-spring-lockup.svg/early-spring-stacked.svg/early-spring-isotype.svgEach mark ships as an SVG sized to its native ratio. Render in sapphire via CSS mask so a single asset reproduces in any color the page calls for. Never recolor amber, never composite over photography, never reflow the elements inside the lockup.
Need a file? Each card downloads on demand in sapphire, black, or paper (white) — as SVG (vector, recolored at source) or PNG (~2K longest edge). Every export has a transparent background; the paper variant is for placing on dark surfaces.
/* CSS mask pattern — paints the SVG in any single color */
.mark--lockup {
width: 124px;
height: 26px;
background: var(--ink);
-webkit-mask: url(/early-spring-lockup.svg) left center/contain no-repeat;
mask: url(/early-spring-lockup.svg) left center/contain no-repeat;
}
Typography
TWK Lausanne. Self-host all five weights. No fallback typeface mixing — system-ui only as a network-failure backup. Designed by Kasper-Florio in Switzerland.
@font-face { font-family: "TWK Lausanne"; src: url("/fonts/TWKLausanne-300.woff2") format("woff2"); font-weight: 300; font-display: swap; }
@font-face { font-family: "TWK Lausanne"; src: url("/fonts/TWKLausanne-400.woff2") format("woff2"); font-weight: 400; font-display: swap; }
@font-face { font-family: "TWK Lausanne"; src: url("/fonts/TWKLausanne-600.woff2") format("woff2"); font-weight: 600; font-display: swap; }
@font-face { font-family: "TWK Lausanne"; src: url("/fonts/TWKLausanne-800.woff2") format("woff2"); font-weight: 800; font-display: swap; }
@font-face { font-family: "TWK Lausanne"; src: url("/fonts/TWKLausanne-900.woff2") format("woff2"); font-weight: 900; font-display: swap; }
Spacing
:root {
--gutter: clamp(20px, 3vw, 56px); /* horizontal padding */
--max: 1640px; /* page max-width */
}
Section vertical padding scales with the section's weight: 56px for utility sections, 64–96px for primary, 96px+ for hero. Always vertical first, horizontal second.
Surfaces
/* Floating panels, cards, the Forecast Builder */
background: rgba(255,255,255,0.62);
backdrop-filter: blur(28px) saturate(140%);
border: 1px solid rgba(15,82,186,0.16);
border-radius: 18px;
box-shadow: 0 32px 80px rgba(15,82,186,0.22);
Editorial, atmospheric, slightly underwritten. Never corporate.
Naming rules
U+00B7⌥ + ⇧ + 9 on Mac.<em>, not boldem is rendered at 42% sapphire — it dims rather than shouts. Used for the second half of a phrase, alternate readings, or quiet asides.Tone calibration
Default register: a curator describing what they made, addressed to one reader. Avoid the "we" of advertising. Avoid the "you" of self-help. The studio narrates; the reader observes.
Numbers as figures (5 continents, 40 speakers, 2024 Miami Open) — the precision is part of the texture. Spell out when running prose demands it.
Tight headlines. Open body.
Sizing
/* Display (hero, page titles) */
font-size: clamp(48px, 7vw, 144px);
font-weight: 800;
line-height: 0.94;
letter-spacing: -0.028em;
/* H2 (section titles) */
font-size: clamp(28px, 3.4vw, 48px);
font-weight: 800;
line-height: 1.04;
letter-spacing: -0.022em;
/* Kicker (above titles, brand markers) */
font-size: 11px;
font-weight: 700;
letter-spacing: 0.10em;
text-transform: uppercase;
opacity: 0.6;
/* Body */
font-size: 15px;
font-weight: 400-500;
line-height: 1.55-1.6;
max-width: 60-64ch;
Letter-spacing curve
Bigger type → tighter letter-spacing (negative tracking pulls the headline together). Smaller type, especially uppercase kickers, opens up to 0.06–0.10em. The visual rhythm is part of the brand.
Italics
True italics for foreign words, cited titles, and the occasional editorial flourish. Soft emphasis is the dimmed sapphire-via-<em> pattern, not italic.
Few. Each one earned.
Nav
Sticky, paper background with backdrop-filter blur, faint rule below. Three-column grid: brand left, page links centered, time + lab pill right. Sapphire on paper, no shadow.
Hero (carousel)
Aspect ratio 21/9 on desktop, taller on mobile. Slides cross-fade by opacity (no slide motion). A muted bubble row at bottom-right; active bubble stretches into a pill.
Work rows
Expandable accordion list. Each row: number / client / title / year. Click expands inline with case-study overview + "See the full case study →" link.
Case study pages
Hero media, 4-column meta dl (Client / Year / Type / Location), 16/9 hero image or video, 2-column overview + insight grid below. No forced "Ask / Solution / Result" format unless the project has it.
Floating panels
Frosted glass at 62–78% paper opacity, 22–28px backdrop blur, faint sapphire border, 18px radius, soft sapphire shadow. Slide in from edge with cubic-bezier easing.
Buttons
Pill (999px radius) for nav and inline actions; rectangle for form primary. Default is paper bg + sapphire 1px border + sapphire text; hover fills with sapphire and reverses to paper.
Form fields
No box border. Underline only — border-bottom: 1px solid rgba(255,255,255,0.32). The whole field is sapphire-on-ink (the contact form sits inside the sapphire footer panel, inverting the page).
Sliders (Forecast Builder pattern)
accent-color: var(--ink). 18px round thumb desktop, 22px mobile. Track: 4px rgba(15,82,186,0.18). No label tick marks.
The "em dimmed" pattern
/* Apply once, use everywhere via <em> in HTML */
em { font-style: normal; color: rgba(15,82,186,0.42); }
/* Italics get an explicit class instead */
.italic { font-style: italic; color: inherit; }
Hijacking <em> for visual dimming (rather than italics) lets editorial copy use semantic markup that produces the brand's signature soft-emphasis treatment automatically.
Quiet, cubic, never bouncy.
Motion is for confirmation, not entertainment. A hover should acknowledge the cursor; a panel should arrive with weight; an effect should feel meteorological, not animated. No spring overshoot, no easter-egg bounce, no parallax.
Easings
/* Default — for entrances, panel slides, large transitions */
cubic-bezier(0.2, 0.8, 0.2, 1)
/* State changes — hover, focus, color shifts */
ease (or ease-in-out for symmetric)
Durations
- 0.18s — hover and small state changes (color, opacity, border)
- 0.25–0.4s — toggles, accordion expansions, button presses
- 0.45s — panel slide-in from edge
- 0.6–1.2s — overlay fades, large reveals
- 5–7s+ — ambient cycles (only when used; ambient motion is optional, not foundational)
Hover defaults
/* Move up by a hair, never sideways */
.element:hover { transform: translateY(-1px); }
/* Or fill with ink, reverse to paper */
.btn:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }
BEM-ish. Schema-rich. Lab-aware.
CSS naming
.case__title /* block__element */
.row__client /* repeated row component */
.fc-panel /* feature-prefix when isolated subsystem */
.fc-row__value /* feature-prefix__element */
body.is-windy /* state class on body for global modes */
HTML conventions
- Every page:
<link rel="canonical">, OG tags, twitter card, theme-color, favicon set - Schema.org as a single
<script type="application/ld+json">with@graph - Page-specific schema types:
WebSite,Organization,Person,BreadcrumbList,AboutPage,CreativeWork(case studies),NewsArticle(press),FAQPage,ItemList - Lab/internal pages:
<meta name="robots" content="noindex,nofollow">
Section IDs (the public IA)
id="top" — page brand anchor
id="work" — case study list
id="thinking" — essays, forecasts, editorial
id="contact" — form + footer
/about — about page
/work/[slug] — case study page
/lab/[slug] — unlisted experiment
URL conventions
- Clean URLs always — never link to
.htmlpaths from rendered HTML - Set up Vercel rewrites:
/about → /early-spring-about-lab.html,/lab/guidelines → /lab-guidelines.html - Set up 301 redirects from the bare
.htmlpaths to the clean URL - Cross-domain canonical: every old URL gets a 301 to the new one
Routing pattern (vercel.json)
{
"rewrites": [
{ "source": "/about", "has": [{ "type": "host", "value": "earlyspring.nyc" }],
"destination": "/early-spring-about-lab.html" },
{ "source": "/lab/:slug", "has": [{ "type": "host", "value": "earlyspring.nyc" }],
"destination": "/lab-:slug.html" }
],
"redirects": [
{ "source": "/early-spring-about-lab.html", "destination": "/about", "permanent": true }
]
}
A barebones page in the style.
Drop into any new site. Renders a top nav, a hero block, and a contact section using the design system above. Replace placeholders, keep the architecture.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>[Page] — [Brand]</title>
<link rel="canonical" href="https://[brand].com/[path]" />
<style>
@font-face { font-family: "TWK Lausanne"; src: url("/fonts/TWKLausanne-400.woff2") format("woff2"); font-weight: 400; font-display: swap; }
@font-face { font-family: "TWK Lausanne"; src: url("/fonts/TWKLausanne-800.woff2") format("woff2"); font-weight: 800; font-display: swap; }
:root {
--paper: #FFFFFF;
--ink: #0F52BA;
--rule: rgba(15,82,186,0.18);
--gutter: clamp(20px, 3vw, 56px);
--max: 1640px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: var(--paper); color: var(--ink); font-family: "TWK Lausanne", system-ui, sans-serif; }
::selection { background: var(--ink); color: var(--paper); }
em { font-style: normal; color: rgba(15,82,186,0.42); }
.nav { position: sticky; top: 0; z-index: 20; background: rgba(255,255,255,0.92); backdrop-filter: blur(10px); border-bottom: 1px solid var(--rule); }
.nav__inner { max-width: var(--max); margin: 0 auto; padding: 16px var(--gutter); display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 32px; }
main { max-width: var(--max); margin: 0 auto; padding: 0 var(--gutter); }
.hero { padding: 96px 0; }
.hero h1 { font-size: clamp(48px, 7vw, 144px); font-weight: 800; line-height: 0.94; letter-spacing: -0.028em; max-width: 14ch; }
.kicker { font-size: 11px; font-weight: 700; letter-spacing: 0.10em; text-transform: uppercase; opacity: 0.6; margin-bottom: 18px; }
.contact { background: var(--ink); color: var(--paper); padding: 96px var(--gutter); }
.contact a { color: inherit; }
</style>
</head>
<body>
<header class="nav">
<div class="nav__inner">
<a href="/">Brand</a>
<nav><a href="#work">Work</a> <a href="#thinking">Thinking</a> <a href="/about">About</a></nav>
<span>[Time] · NYC</span>
</div>
</header>
<main>
<section class="hero">
<div class="kicker">Brand · Tagline</div>
<h1>The headline. <em>A softer second half.</em></h1>
</section>
</main>
<section class="contact" id="contact">
<h2>Let's talk.</h2>
</section>
</body></html>