New
Cinematic SupercarLanding Pages
A four-page product launch site built entirely from type — no images, no icons, no fonts to fetch and nothing to license. Display headline, a looping spec strip, four claims each backed by a number, the full specification table and a reservation form that admits it has no backend.
import type { Metadata } from "next"
import { SiteFooter } from "./components/site-footer"
import { SiteHeader } from "./components/site-header"
import { brand, hero } from "./data/product"
import "./monolith-launch.css"
export const metadata: Metadata = {
title: {
default: `${brand.product} — ${brand.descriptor}`,
template: `%s — ${brand.name}`,
},
description: hero.lede,
}
/**
* The template shell.
*
* A nested layout, so it renders no `<html>` or `<body>` — the app's root layout
* owns those. `.monoSite` is the single wrapper every style in
* `monolith-launch.css` hangs off, which is what keeps this sheet from touching
* anything else in the app.
*
* The `<noscript>` block matters: scroll reveals start at `opacity: 0` in CSS so
* they cannot flash in before hydration, which means without JavaScript they
* would never appear at all. This turns them on for that reader instead of
* showing them an empty page.
*/
export default function MonolithLaunchLayout({ children }: { children: React.ReactNode }) {
return (
<div className="monoSite">
<noscript>
<style
dangerouslySetInnerHTML={{
__html: ".monoReveal{opacity:1;transform:none}.monoTickerTrack{animation:none}",
}}
/>
</noscript>
<SiteHeader />
<main>{children}</main>
<SiteFooter />
</div>
)
}/*
* monolith-launch — a single-product launch site built almost entirely from type.
*
* Nothing here can leak. Every class name carries a `mono` prefix and every
* bare-element rule is scoped under `.monoSite`. A template that ships names
* like `.hero`, `.eyebrow` and `.rule` into a consumer's global stylesheet is a
* collision waiting to happen; a prefix plus one wrapper element makes the whole
* sheet inert outside itself.
*
* There are no images anywhere in this template. The composition is the type
* scale, the hairlines and the vertical rhythm, which is why it weighs nothing
* and why it will not look dated when a stock photograph would.
*
* The two things most people will want to change are at the top: the palette,
* and the display scale. Nothing below hard-codes a value that is a variable
* up here.
*/
.monoSite {
/* Palette. Monochrome by design — one accent is enough for a launch page. */
--ink: #f4f3f1;
--ink-dim: rgba(244, 243, 241, 0.62);
--ink-faint: rgba(244, 243, 241, 0.38);
--ink-ghost: rgba(244, 243, 241, 0.14);
--surface: #0b0b0c;
--surface-lift: #131315;
--line: rgba(244, 243, 241, 0.12);
--line-strong: rgba(244, 243, 241, 0.28);
--accent: #d8734a;
/*
* Display scale. The viewport unit is what makes the headline monumental on a
* desktop and still legible on a phone; the clamp stops it doing either badly.
*/
--display: clamp(3.2rem, 13vw, 13.5rem);
--display-sm: clamp(2.4rem, 7.4vw, 6.5rem);
--heading: clamp(1.75rem, 3.4vw, 3.1rem);
--body: clamp(1rem, 1.12vw, 1.1875rem);
/* Rhythm. Sections are tall on purpose — the space is part of the design. */
--gutter: clamp(1.25rem, 4.5vw, 6rem);
--section: clamp(5.5rem, 12vh, 11rem);
--measure: 34rem;
--font-display: var(--font-mono-display, "Inter Tight", "Helvetica Neue", Arial, sans-serif);
--font-body: var(--font-mono-body, ui-sans-serif, system-ui, "Segoe UI", Arial, sans-serif);
--font-mono: ui-monospace, "SFMono-Regular", "Cascadia Mono", "Roboto Mono", monospace;
position: relative;
min-height: 100vh;
background: var(--surface);
color: var(--ink);
font-family: var(--font-body);
font-size: var(--body);
line-height: 1.6;
/* Contains the sticky header and any blend mode so neither escapes the wrapper. */
isolation: isolate;
overflow-x: hidden;
}
.monoSite *,
.monoSite *::before,
.monoSite *::after { box-sizing: border-box; }
/*
* `:where()` contributes no specificity, which is the whole point: `.monoSite a`
* would be (0,1,1) and would beat every single-class rule below it, so a solid
* button would inherit white text on a white background. This way the reset is
* (0,1,0) and any later class rule wins.
*/
.monoSite :where(a) { color: inherit; text-decoration: none; }
.monoSite :where(button, input, textarea, select) { font: inherit; color: inherit; }
.monoSite ::selection { background: var(--accent); color: #0b0b0c; }
/* One visible focus style for the whole template, on the element that has focus. */
.monoSite :focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
border-radius: 2px;
}
/* ---------------------------------------------------------------- primitives */
.monoWrap {
width: 100%;
max-width: 96rem;
margin: 0 auto;
padding-inline: var(--gutter);
}
/* The hairline that separates every section. Its own element so a section can
opt out without a `:last-child` exception. */
.monoRule {
height: 1px;
background: var(--line);
border: 0;
margin: 0;
}
.monoSection { padding-block: var(--section); }
/* Wide-tracked micro-label. Used for eyebrows, group headings and table heads. */
.monoLabel {
margin: 0;
font-family: var(--font-mono);
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink-faint);
}
.monoLede {
max-width: var(--measure);
margin: 0;
font-size: clamp(1.0625rem, 1.5vw, 1.375rem);
line-height: 1.55;
color: var(--ink-dim);
text-wrap: pretty;
}
.monoHeading {
margin: 0;
font-family: var(--font-display);
font-size: var(--heading);
font-weight: 500;
line-height: 1.04;
letter-spacing: -0.025em;
text-wrap: balance;
}
/* Label + heading, above a band of rows. */
.monoSectionHead {
display: grid;
gap: 1.125rem;
max-width: 40rem;
margin-bottom: clamp(2rem, 5vw, 3.5rem);
}
/* The same pair with no space under it — for use inside a grid that already gaps. */
.monoStack { display: grid; gap: 1.125rem; align-content: start; }
/*
* Consecutive sections would otherwise stack two full `--section` paddings
* against each other. The first one in a run keeps its top padding; the rest
* carry this.
*/
.monoSectionTight { padding-top: 0; }
/* The head that has a link on the far side of it at desktop width. */
.monoSectionHeadRow {
display: grid;
gap: 1.5rem;
margin-bottom: clamp(2rem, 5vw, 3.5rem);
}
@media (min-width: 52rem) {
.monoSectionHeadRow {
grid-template-columns: minmax(0, 1fr) auto;
align-items: end;
gap: 2.5rem;
}
.monoSectionHeadRow .monoSectionHead { margin-bottom: 0; }
}
/* ------------------------------------------------------------------- buttons */
.monoBtn {
display: inline-flex;
align-items: center;
gap: 0.625rem;
padding: 0.9375rem 1.75rem;
border: 1px solid var(--line-strong);
border-radius: 999px;
background: transparent;
font-size: 0.9375rem;
font-weight: 500;
letter-spacing: -0.005em;
cursor: pointer;
transition:
background-color 0.35s cubic-bezier(0.22, 1, 0.36, 1),
color 0.35s cubic-bezier(0.22, 1, 0.36, 1),
border-color 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.monoBtn:hover {
background: var(--ink);
border-color: var(--ink);
color: var(--surface);
}
.monoBtnSolid {
background: var(--ink);
border-color: var(--ink);
color: var(--surface);
}
.monoBtnSolid:hover {
background: var(--accent);
border-color: var(--accent);
color: #0b0b0c;
}
.monoBtn:disabled { opacity: 0.5; cursor: default; }
/* The arrow that leans forward on hover. Inline so no icon library is needed. */
.monoBtn > .monoArrow {
display: inline-block;
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.monoBtn:hover > .monoArrow { transform: translateX(3px); }
/* -------------------------------------------------------------------- header */
.monoHeader {
position: sticky;
top: 0;
z-index: 20;
background: color-mix(in oklab, var(--surface) 82%, transparent);
backdrop-filter: blur(14px) saturate(140%);
border-bottom: 1px solid var(--line);
}
.monoHeaderRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
height: 4.25rem;
}
.monoBrand {
display: flex;
align-items: baseline;
gap: 0.625rem;
font-family: var(--font-display);
font-size: 1.0625rem;
font-weight: 600;
letter-spacing: -0.03em;
}
.monoBrandMark {
/* The logo is a square. A launch page does not need more than a square. */
width: 0.6875rem;
height: 0.6875rem;
background: var(--accent);
transform: translateY(-0.05em);
}
.monoBrandDescriptor {
font-family: var(--font-mono);
font-size: 0.625rem;
font-weight: 400;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-faint);
}
.monoNav {
display: none;
align-items: center;
gap: 2rem;
}
.monoNavLink {
position: relative;
font-size: 0.875rem;
color: var(--ink-dim);
transition: color 0.25s ease;
}
.monoNavLink:hover { color: var(--ink); }
/* Underline grows from the left rather than fading in — cheaper to read. */
.monoNavLink::after {
content: "";
position: absolute;
left: 0;
bottom: -0.3125rem;
width: 100%;
height: 1px;
background: currentColor;
transform: scaleX(0);
transform-origin: left;
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.monoNavLink:hover::after,
.monoNavLink[aria-current="page"]::after { transform: scaleX(1); }
.monoNavLink[aria-current="page"] { color: var(--ink); }
/*
* Mobile navigation is a native `<details>`. No state, no JS, no focus trap to
* get wrong — and it still works if hydration never happens, which a marketing
* page should survive.
*/
.monoMenu { position: relative; }
.monoMenuSummary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.875rem;
border: 1px solid var(--line-strong);
border-radius: 999px;
font-family: var(--font-mono);
font-size: 0.625rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-dim);
cursor: pointer;
list-style: none;
}
/* Safari still paints a disclosure triangle without this. */
.monoMenuSummary::-webkit-details-marker { display: none; }
.monoMenu[open] .monoMenuSummary { color: var(--ink); border-color: var(--ink); }
.monoMenuPanel {
position: absolute;
right: 0;
top: calc(100% + 0.75rem);
z-index: 30;
display: grid;
gap: 0.25rem;
min-width: 12rem;
padding: 0.75rem;
border: 1px solid var(--line);
border-radius: 0.75rem;
background: var(--surface-lift);
}
.monoMenuLink {
padding: 0.5rem 0.625rem;
border-radius: 0.5rem;
font-size: 0.9375rem;
color: var(--ink-dim);
}
.monoMenuLink:hover { background: rgba(244, 243, 241, 0.06); color: var(--ink); }
.monoMenuLink[aria-current="page"] { color: var(--ink); }
@media (min-width: 52rem) {
.monoNav { display: flex; }
.monoHeaderRow { height: 5rem; }
.monoMenu { display: none; }
}
/* ---------------------------------------------------------------------- hero */
.monoHero {
padding-top: clamp(3.5rem, 9vh, 7rem);
padding-bottom: clamp(2rem, 5vh, 4rem);
}
.monoHeroEyebrow {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: clamp(2rem, 6vh, 4.5rem);
}
.monoHeroEyebrow::after {
content: "";
flex: 1;
height: 1px;
background: var(--line);
}
/*
* The display headline. One word per line, set as tight as the face allows.
* `text-transform` is deliberately absent — the scale is the emphasis, so the
* words keep their natural case and stay readable.
*/
.monoDisplay {
margin: 0;
font-family: var(--font-display);
font-size: var(--display);
font-weight: 500;
line-height: 0.86;
letter-spacing: -0.045em;
}
.monoDisplayWord { display: block; }
/* Every other word steps back, which gives the block depth without colour. */
.monoDisplayWord:nth-child(even) {
color: var(--ink-faint);
padding-left: 0.09em;
}
.monoHeroFoot {
display: grid;
gap: 2.5rem;
margin-top: clamp(2.5rem, 7vh, 5rem);
}
.monoHeroActions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}
@media (min-width: 52rem) {
.monoHeroFoot {
grid-template-columns: 1fr auto;
align-items: end;
gap: 4rem;
}
}
/* --------------------------------------------------------------------- ticker */
.monoTicker {
border-block: 1px solid var(--line);
padding-block: 1.125rem;
overflow: hidden;
/* Fades the ends so the loop never shows a hard cut at the viewport edge. */
mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
}
.monoTickerTrack {
display: flex;
width: max-content;
animation: monoTickerScroll 38s linear infinite;
}
.monoTickerItem {
display: flex;
align-items: center;
gap: 2.75rem;
padding-right: 2.75rem;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-faint);
white-space: nowrap;
}
.monoTickerItem::after {
content: "";
width: 0.25rem;
height: 0.25rem;
background: var(--accent);
flex-shrink: 0;
}
@keyframes monoTickerScroll {
from { transform: translate3d(0, 0, 0); }
to { transform: translate3d(-50%, 0, 0); }
}
/* ----------------------------------------------------------------- manifesto */
.monoManifesto {
display: grid;
gap: clamp(2rem, 5vw, 4.5rem);
}
.monoManifestoBody { display: grid; gap: 1.5rem; }
.monoManifestoBody p {
margin: 0;
max-width: 40rem;
font-size: clamp(1.0625rem, 1.35vw, 1.3125rem);
line-height: 1.6;
color: var(--ink-dim);
text-wrap: pretty;
}
/* First paragraph reads as the statement, the rest as the argument. */
.monoManifestoBody p:first-child {
color: var(--ink);
font-size: clamp(1.1875rem, 1.7vw, 1.625rem);
line-height: 1.45;
}
@media (min-width: 60rem) {
.monoManifesto { grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); }
}
/* ------------------------------------------------------------------- pillars */
.monoPillars {
display: grid;
gap: 0;
border-top: 1px solid var(--line);
}
.monoPillar {
display: grid;
gap: 1rem;
padding-block: clamp(2rem, 4.5vw, 3.25rem);
border-bottom: 1px solid var(--line);
}
.monoPillarIndex {
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.12em;
color: var(--accent);
}
.monoPillarTitle {
margin: 0;
font-family: var(--font-display);
font-size: clamp(1.5rem, 2.6vw, 2.375rem);
font-weight: 500;
line-height: 1.06;
letter-spacing: -0.03em;
}
.monoPillarBody {
margin: 0;
max-width: 32rem;
color: var(--ink-dim);
text-wrap: pretty;
}
.monoPillarMetric {
display: grid;
gap: 0.25rem;
align-content: start;
}
.monoPillarMetricValue {
font-family: var(--font-display);
font-size: clamp(2rem, 4.5vw, 3.5rem);
font-weight: 500;
line-height: 0.95;
letter-spacing: -0.04em;
font-variant-numeric: tabular-nums;
}
@media (min-width: 60rem) {
.monoPillar {
grid-template-columns: 4rem minmax(0, 1fr) minmax(0, 1fr) 10rem;
gap: 2.5rem;
align-items: start;
}
.monoPillarMetric { justify-items: end; text-align: right; }
}
/* --------------------------------------------------------------------- specs */
.monoSpecGroup { border-bottom: 1px solid var(--line); }
.monoSpecGroup:first-of-type { border-top: 1px solid var(--line); }
.monoSpecGroupInner {
display: grid;
gap: 1.25rem;
padding-block: clamp(1.75rem, 3.5vw, 2.75rem);
}
.monoSpecRows {
display: grid;
gap: 0;
margin: 0;
}
/* A definition list, so a screen reader gets term/value pairing for free. */
.monoSpecRow {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
gap: 1rem 1.5rem;
padding-block: 0.8125rem;
border-top: 1px solid var(--line);
font-size: 0.9375rem;
}
.monoSpecRow:first-child { border-top: 0; }
.monoSpecRow dt { color: var(--ink-faint); }
.monoSpecRow dd { margin: 0; color: var(--ink); }
@media (min-width: 60rem) {
.monoSpecGroupInner { grid-template-columns: 12rem minmax(0, 1fr); gap: 2.5rem; }
}
/* --------------------------------------------------------------------- offer */
.monoOffer {
display: grid;
gap: clamp(2rem, 5vw, 4rem);
padding: clamp(2rem, 5vw, 4rem);
border: 1px solid var(--line);
border-radius: 1.25rem;
background: var(--surface-lift);
}
.monoOfferPrice {
font-family: var(--font-display);
font-size: var(--display-sm);
font-weight: 500;
line-height: 0.9;
letter-spacing: -0.04em;
font-variant-numeric: tabular-nums;
}
.monoOfferNote {
margin: 0;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.05em;
color: var(--ink-faint);
}
.monoOfferBody { display: grid; gap: 1.5rem; align-content: start; }
@media (min-width: 60rem) {
.monoOffer { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1fr); align-items: start; }
}
/* --------------------------------------------------------------------- story */
.monoBeat {
display: grid;
gap: 0.875rem;
padding-block: clamp(1.75rem, 4vw, 3rem);
border-top: 1px solid var(--line);
}
.monoBeatYear {
font-family: var(--font-mono);
font-size: 0.8125rem;
letter-spacing: 0.1em;
color: var(--accent);
font-variant-numeric: tabular-nums;
}
.monoBeatTitle {
margin: 0;
font-family: var(--font-display);
font-size: clamp(1.375rem, 2.2vw, 2rem);
font-weight: 500;
line-height: 1.1;
letter-spacing: -0.03em;
}
.monoBeatBody {
margin: 0;
max-width: 38rem;
color: var(--ink-dim);
text-wrap: pretty;
}
@media (min-width: 60rem) {
.monoBeat { grid-template-columns: 8rem minmax(0, 1fr); gap: 2.5rem; }
.monoBeatYear { padding-top: 0.3rem; }
}
/* ---------------------------------------------------------------------- form */
.monoForm { display: grid; gap: 1.25rem; max-width: 32rem; }
.monoField { display: grid; gap: 0.5rem; }
.monoFieldLabel {
font-family: var(--font-mono);
font-size: 0.6875rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-faint);
}
.monoInput {
width: 100%;
padding: 0.8125rem 1rem;
border: 1px solid var(--line);
border-radius: 0.5rem;
background: var(--surface-lift);
font-size: 0.9375rem;
transition: border-color 0.25s ease;
}
.monoInput:hover { border-color: var(--line-strong); }
.monoInput::placeholder { color: var(--ink-ghost); }
.monoTextarea { min-height: 6.5rem; resize: vertical; }
.monoFormNote {
margin: 0;
font-size: 0.8125rem;
line-height: 1.55;
color: var(--ink-faint);
}
/* Result line for the demo submit. `role="status"` announces it politely. */
.monoFormStatus {
margin: 0;
padding: 0.875rem 1rem;
border: 1px solid var(--line-strong);
border-radius: 0.5rem;
font-size: 0.875rem;
color: var(--ink);
}
/* -------------------------------------------------------------------- footer */
.monoFooter { border-top: 1px solid var(--line); padding-block: clamp(3rem, 7vw, 5rem); }
.monoFooterTop {
display: grid;
gap: 2.5rem;
padding-bottom: 3rem;
}
.monoFooterBlurb {
margin: 0;
max-width: 30rem;
font-size: 0.9375rem;
line-height: 1.6;
color: var(--ink-dim);
text-wrap: pretty;
}
.monoFooterCols {
display: grid;
gap: 2rem;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
align-content: start;
}
.monoFooterLinks { display: grid; gap: 0.625rem; margin-top: 1.125rem; }
.monoFooterLink {
font-size: 0.9375rem;
color: var(--ink-dim);
transition: color 0.25s ease;
}
.monoFooterLink:hover { color: var(--ink); }
.monoFooterBottom a { color: inherit; transition: color 0.25s ease; }
.monoFooterBottom a:hover { color: var(--ink); }
.monoFooterBottom {
display: flex;
flex-wrap: wrap;
gap: 1rem 2rem;
justify-content: space-between;
padding-top: 1.75rem;
border-top: 1px solid var(--line);
font-family: var(--font-mono);
font-size: 0.6875rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-faint);
}
/*
* The wordmark that closes the page. It is allowed to overflow its box and be
* clipped by the wrapper — that is the effect, not a bug.
*/
.monoFooterWord {
margin: 0;
padding-top: clamp(2rem, 6vw, 4rem);
font-family: var(--font-display);
font-size: clamp(3.5rem, 19vw, 18rem);
font-weight: 500;
line-height: 0.8;
letter-spacing: -0.05em;
color: var(--ink-ghost);
user-select: none;
}
@media (min-width: 52rem) {
.monoFooterTop { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 4rem; }
}
/* ----------------------------------------------------------------- page head */
/*
* The three inner pages open the same way: label, one large line, one lede.
* Sharing it here is what keeps them feeling like one site.
*/
.monoPageHead {
display: grid;
gap: 1.5rem;
padding-top: clamp(3rem, 8vh, 5.5rem);
padding-bottom: clamp(2rem, 5vh, 3.5rem);
}
.monoPageTitle {
margin: 0;
max-width: 22ch;
font-family: var(--font-display);
font-size: var(--display-sm);
font-weight: 500;
line-height: 0.94;
letter-spacing: -0.04em;
text-wrap: balance;
}
/* A link inside body copy still needs to look like one. */
.monoInlineLink {
color: var(--ink);
border-bottom: 1px solid var(--line-strong);
transition: border-color 0.25s ease;
}
.monoInlineLink:hover { border-color: var(--ink); }
/* ------------------------------------------------------------------- reserve */
.monoReserveGrid { display: grid; gap: clamp(2.5rem, 6vw, 4.5rem); }
/* The summary card beside the form. Same material as the offer block. */
.monoAside {
display: grid;
gap: 1rem;
align-content: start;
padding: clamp(1.5rem, 3vw, 2rem);
border: 1px solid var(--line);
border-radius: 1.25rem;
background: var(--surface-lift);
}
.monoAsideList {
display: grid;
gap: 0.75rem;
margin: 0;
padding: 0;
list-style: none;
font-size: 0.9375rem;
color: var(--ink-dim);
}
/* A hairline tick rather than a bullet glyph — no font dependency. */
.monoAsideList li {
display: grid;
grid-template-columns: 0.75rem minmax(0, 1fr);
gap: 0.75rem;
}
.monoAsideList li::before {
content: "";
margin-top: 0.6em;
width: 0.5rem;
height: 1px;
background: var(--accent);
}
@media (min-width: 60rem) {
.monoReserveGrid {
grid-template-columns: minmax(0, 1fr) minmax(0, 0.78fr);
align-items: start;
}
}
/* -------------------------------------------------------------- scroll reveal */
/*
* Reveal on scroll, driven by one IntersectionObserver in `components/reveal.tsx`
* that flips `data-shown` on. Two rules matter for correctness:
*
* - The hidden state is set here, not in JS, so nothing flashes in before the
* observer attaches.
* - Under `prefers-reduced-motion` the element is simply visible. Not a
* shorter animation — none, and no transform, because a translate is exactly
* what triggers vestibular discomfort.
*/
.monoReveal {
opacity: 0;
transform: translate3d(0, 1.25rem, 0);
transition:
opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1),
transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
.monoReveal[data-shown="true"] {
opacity: 1;
transform: translate3d(0, 0, 0);
}
/* Rows in a list stagger by index; the JS sets `--mono-delay`. */
.monoReveal { transition-delay: var(--mono-delay, 0ms); }
@media (prefers-reduced-motion: reduce) {
.monoReveal,
.monoReveal[data-shown="true"] {
opacity: 1;
transform: none;
transition: none;
}
.monoTickerTrack { animation: none; }
.monoSite *,
.monoSite *::before,
.monoSite *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
}
}import Link from "next/link"
import { Reveal } from "./components/reveal"
import { SpecTable } from "./components/spec-table"
import { Ticker } from "./components/ticker"
import { hero, manifesto, offer, pillars, sections, specs } from "./data/product"
/**
* The launch page: one long scroll, no images.
*
* The order is the argument — a claim, the reason for it, four defences with
* numbers, the specifications behind those numbers, then the price. Every string
* comes from `data/product.ts`.
*/
export default function MonolithHome() {
return (
<>
<section className="monoWrap monoHero">
<div className="monoHeroEyebrow">
<p className="monoLabel">{hero.eyebrow}</p>
</div>
<h1 className="monoDisplay">
{hero.words.map((word, index) => (
<span key={`${word}-${index}`} className="monoDisplayWord">
{word}
</span>
))}
</h1>
<div className="monoHeroFoot">
<p className="monoLede">{hero.lede}</p>
<div className="monoHeroActions">
<Link className="monoBtn monoBtnSolid" href={hero.primary.href}>
{hero.primary.label}
<span className="monoArrow" aria-hidden="true">
→
</span>
</Link>
<Link className="monoBtn" href={hero.secondary.href}>
{hero.secondary.label}
</Link>
</div>
</div>
</section>
<Ticker />
<section className="monoSection">
<Reveal className="monoWrap monoManifesto">
<div className="monoStack">
<p className="monoLabel">{sections.manifesto}</p>
<h2 className="monoHeading">{manifesto.heading}</h2>
</div>
<div className="monoManifestoBody">
{manifesto.body.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>
))}
</div>
</Reveal>
</section>
<section className="monoSection monoSectionTight">
<div className="monoWrap">
<Reveal className="monoSectionHead">
<p className="monoLabel">{sections.pillars.label}</p>
<h2 className="monoHeading">{sections.pillars.heading}</h2>
</Reveal>
<div className="monoPillars">
{pillars.map((pillar, index) => (
<Reveal key={pillar.index} className="monoPillar" delay={index * 70}>
<p className="monoPillarIndex">{pillar.index}</p>
<h3 className="monoPillarTitle">{pillar.title}</h3>
<p className="monoPillarBody">{pillar.body}</p>
<div className="monoPillarMetric">
<span className="monoPillarMetricValue">{pillar.metric}</span>
<span className="monoLabel">{pillar.metricLabel}</span>
</div>
</Reveal>
))}
</div>
</div>
</section>
<section className="monoSection monoSectionTight">
<div className="monoWrap">
<div className="monoSectionHeadRow">
<Reveal className="monoSectionHead">
<p className="monoLabel">{sections.specs.label}</p>
<h2 className="monoHeading">{sections.specs.heading}</h2>
</Reveal>
<Link className="monoBtn" href={sections.specs.cta.href}>
{sections.specs.cta.label}
<span className="monoArrow" aria-hidden="true">
→
</span>
</Link>
</div>
</div>
{/* Two groups here; the rest live on the specifications page. */}
<SpecTable groups={specs.slice(0, 2)} />
</section>
<section className="monoSection monoSectionTight">
<div className="monoWrap">
<Reveal className="monoOffer">
<div>
<p className="monoLabel">{offer.eyebrow}</p>
<p className="monoOfferPrice">{offer.price}</p>
<p className="monoOfferNote">{offer.priceNote}</p>
</div>
<div className="monoOfferBody">
<h2 className="monoHeading">{offer.heading}</h2>
<p className="monoLede">{offer.body}</p>
<div className="monoHeroActions">
<Link className="monoBtn monoBtnSolid" href={offer.cta.href}>
{offer.cta.label}
<span className="monoArrow" aria-hidden="true">
→
</span>
</Link>
</div>
<p className="monoOfferNote">{offer.note}</p>
</div>
</Reveal>
</div>
</section>
</>
)
}import type { Metadata } from "next"
import Link from "next/link"
import { Reveal } from "../components/reveal"
import { offer, story } from "../data/product"
export const metadata: Metadata = {
title: "Story",
description: story.lede,
}
/** Four beats, each a year and a paragraph. No timeline component required. */
export default function StoryPage() {
return (
<>
<section className="monoWrap monoPageHead">
<p className="monoLabel">{story.eyebrow}</p>
<h1 className="monoPageTitle">{story.heading}</h1>
<p className="monoLede">{story.lede}</p>
</section>
<section className="monoSection monoSectionTight">
<div className="monoWrap">
{story.beats.map((beat, index) => (
<Reveal key={beat.year} className="monoBeat" delay={index * 70}>
<p className="monoBeatYear">{beat.year}</p>
<div className="monoStack">
<h2 className="monoBeatTitle">{beat.title}</h2>
<p className="monoBeatBody">{beat.body}</p>
</div>
</Reveal>
))}
</div>
</section>
<section className="monoSection monoSectionTight">
<Reveal className="monoWrap monoStack">
<p className="monoLabel">{offer.eyebrow}</p>
<h2 className="monoHeading">{offer.heading}</h2>
<div className="monoHeroActions">
<Link className="monoBtn monoBtnSolid" href={offer.cta.href}>
{offer.cta.label}
<span className="monoArrow" aria-hidden="true">
→
</span>
</Link>
</div>
</Reveal>
</section>
</>
)
}import type { Metadata } from "next"
import Link from "next/link"
import { Reveal } from "../components/reveal"
import { SpecTable } from "../components/spec-table"
import { offer, specs, specsIntro } from "../data/product"
export const metadata: Metadata = {
title: "Specifications",
description: specsIntro.lede,
}
/** Every group, full width, one hairline band each. */
export default function SpecsPage() {
return (
<>
<section className="monoWrap monoPageHead">
<p className="monoLabel">{specsIntro.eyebrow}</p>
<h1 className="monoPageTitle">{specsIntro.heading}</h1>
<p className="monoLede">{specsIntro.lede}</p>
</section>
<section className="monoSection monoSectionTight">
<SpecTable groups={specs} />
</section>
<section className="monoSection monoSectionTight">
<Reveal className="monoWrap monoStack">
<p className="monoLabel">{offer.eyebrow}</p>
<h2 className="monoHeading">{offer.heading}</h2>
<p className="monoLede">{offer.body}</p>
<div className="monoHeroActions">
<Link className="monoBtn monoBtnSolid" href={offer.cta.href}>
{offer.cta.label}
<span className="monoArrow" aria-hidden="true">
→
</span>
</Link>
</div>
</Reveal>
</section>
</>
)
}import type { Metadata } from "next"
import { ReserveForm } from "../components/reserve-form"
import { Reveal } from "../components/reveal"
import { offer, reserve } from "../data/product"
export const metadata: Metadata = {
title: "Reserve",
description: reserve.lede,
}
/** The form, and beside it the terms it commits you to — which is none. */
export default function ReservePage() {
return (
<>
<section className="monoWrap monoPageHead">
<p className="monoLabel">{reserve.eyebrow}</p>
<h1 className="monoPageTitle">{reserve.heading}</h1>
<p className="monoLede">{reserve.lede}</p>
</section>
<section className="monoSection monoSectionTight">
<div className="monoWrap monoReserveGrid">
<ReserveForm />
<Reveal className="monoAside">
<p className="monoLabel">{offer.eyebrow}</p>
<p className="monoOfferPrice">{offer.price}</p>
<p className="monoOfferNote">{offer.priceNote}</p>
<ul className="monoAsideList">
{reserve.terms.map((term) => (
<li key={term}>{term}</li>
))}
</ul>
</Reveal>
</div>
</section>
</>
)
}/**
* Everything the template says, in one file.
*
* The pages read from here and hold no copy of their own, so re-voicing the whole
* site for a different product is one file to edit and no JSX to touch. Swap the
* palette in `monolith-launch.css` and the words here and nothing else needs to
* change.
*
* The demo product is a single-dose espresso machine because a launch page needs
* something with real specifications to talk about. Replace it with yours.
*/
/** Where the template is mounted. Change this and every internal link follows. */
export const BASE = "/monolith-launch"
export const brand = {
name: "Monolith",
product: "Monolith One",
/** Sits in the corner of the header. Keep it to three or four words. */
descriptor: "Single-dose espresso",
email: "hello@monolith.example",
/** Shown in the footer and used for the reserve confirmation copy. */
city: "Turin",
year: 2026,
} as const
export const nav = [
{ label: "Story", href: `${BASE}/story` },
{ label: "Specifications", href: `${BASE}/specs` },
{ label: "Reserve", href: `${BASE}/reserve` },
] as const
/**
* The opening. `words` is set one per line at display size, so three or four
* short ones read best — the line breaks are the composition.
*/
export const hero = {
eyebrow: "A machine that holds its temperature",
words: ["Pressure", "and", "patience"] as const,
lede:
"Ninety-two degrees, held to a tenth, for as long as the shot takes. Everything else on this machine exists to protect that one number.",
primary: { label: "Reserve one", href: `${BASE}/reserve` },
secondary: { label: "Read the story", href: `${BASE}/story` },
/** The strip that scrolls under the hero. Short phrases, no punctuation. */
ticker: [
"Brushed 316 steel",
"58mm basket",
"PID to 0.1°C",
"Nine bar, flat",
"Built in Turin",
"Ten year parts",
] as const,
} as const
/** The quiet full-width statement between the hero and the detail. */
export const manifesto = {
heading: "We removed everything that was not the shot",
body: [
"A pressure gauge tells you what already happened. A second boiler solves a problem you do not have at home. A screen asks you to read while your coffee cools.",
"So none of them are here. What is here is a thermal core that recovers in four seconds, a pump curve you can shape, and a body heavy enough that nothing moves when you lock the basket in.",
] as const,
} as const
/** Three or four. Each one is a claim you can defend with a number. */
export const pillars = [
{
index: "01",
title: "Holds its heat",
body:
"A 1.4kg brass core under PID control, sampled forty times a second. Group temperature stays inside a tenth of a degree from first shot to twentieth.",
metric: "±0.1°C",
metricLabel: "at the group",
},
{
index: "02",
title: "Shapes the pressure",
body:
"A geared pump instead of a vibration one, so the ramp is yours: pre-infuse low and long, hold at nine, then decline into the tail without touching a paddle.",
metric: "0–12 bar",
metricLabel: "programmable",
},
{
index: "03",
title: "Recovers immediately",
body:
"Four seconds back to setpoint after a double. Two people, two shots, one after the other, with no waiting and no compromise on the second.",
metric: "4s",
metricLabel: "to setpoint",
},
{
index: "04",
title: "Opens with two screws",
body:
"Every part inside is a part you can buy from us for ten years, with the diagrams published. Gaskets, the pump, the controller — all of it, all replaceable at the kitchen table.",
metric: "10 yr",
metricLabel: "parts guarantee",
},
] as const
/**
* Section labels and headings for the home page.
*
* These are here rather than in the JSX for the same reason as everything else:
* so that re-voicing the site never means opening a component.
*/
export const sections = {
manifesto: "Position",
pillars: {
label: "The machine",
heading: "Four claims, each with a number behind it",
},
specs: {
label: "Specifications",
heading: "The short version",
cta: { label: "Every specification", href: `${BASE}/specs` },
},
offer: "Availability",
} as const
/** The opener on the specifications page. */
export const specsIntro = {
eyebrow: "Specifications",
heading: "Every number we will stand behind",
lede:
"Measured on production units at 22°C ambient, not modelled. Where a figure carries a tolerance, the tolerance is the figure.",
} as const
/** Grouped for the specifications page and the short list on the home page. */
export const specs = [
{
group: "Thermal",
rows: [
["Core", "1.4kg machined brass, insulated"],
["Control", "PID, 40Hz sampling, 0.1°C band"],
["Recovery", "4 seconds to setpoint after a double"],
["Range", "86–96°C, set in tenths"],
],
},
{
group: "Pressure",
rows: [
["Pump", "Geared rotary, 48V brushless"],
["Profile", "Six programmable points, 0–12 bar"],
["Flow", "0.5–9 ml/s, measured not inferred"],
["Basket", "58mm, 18g and 21g included"],
],
},
{
group: "Body",
rows: [
["Shell", "Brushed 316 stainless, 3mm"],
["Mass", "14.2kg"],
["Footprint", "245 × 310mm, 360mm tall"],
["Water", "2.1L side-fill, or plumbed"],
],
},
{
group: "Electrical",
rows: [
["Supply", "220–240V, 50/60Hz, 1400W"],
["Standby", "Under 0.4W"],
["Warm-up", "11 minutes from cold"],
["Certification", "CE, UKCA, cETLus"],
],
},
] as const
/** The commercial line. `note` carries the honest caveat. */
export const offer = {
eyebrow: "First run",
price: "€2,400",
priceNote: "including VAT, shipped within the EU",
heading: "Four hundred machines, then we stop and listen",
body:
"The first run is built to order in Turin and ships in the order it was reserved. A reservation costs nothing and holds your place; we ask for payment when your machine is two weeks from the bench.",
cta: { label: "Reserve without paying", href: `${BASE}/reserve` },
note: "No card, no deposit, and cancel with one email.",
} as const
/**
* The brand story page. Each beat is a year and a paragraph, which is enough
* structure to carry a page without inventing a timeline component.
*/
export const story = {
eyebrow: "Story",
heading: "It started as a complaint",
lede:
"Two of us spent a decade building thermal controllers for laboratory equipment, then went home and made coffee on machines that could not hold a temperature.",
beats: [
{
year: "2021",
title: "The complaint",
body:
"A borrowed lab thermocouple in the group of a well-reviewed machine. Eleven degrees of swing across five shots. We stopped blaming the beans.",
},
{
year: "2023",
title: "The core",
body:
"Forty brass cores, each one heavier and more insulated than the last, until the curve went flat. The one that worked is the one in the machine — we stopped when it stopped mattering.",
},
{
year: "2025",
title: "The body",
body:
"Steel, because it can be refinished and it does not creak when you lock in a basket. Heavier than it needs to be for the same reason a good anvil is.",
},
{
year: "2026",
title: "The first four hundred",
body:
"Built to order, in the order reserved. When they are gone we will fix what we got wrong before building any more.",
},
] as const,
} as const
/** Reserve page copy. The form itself posts nowhere until you wire it. */
export const reserve = {
eyebrow: "Reserve",
heading: "Hold a place in the first run",
lede:
"One machine per person. We will email you once when your slot is two weeks out, and never otherwise.",
fields: {
name: "Your name",
email: "Email",
country: "Shipping country",
note: "Anything you want us to know (optional)",
},
submit: "Reserve my place",
/** The three lines in the card beside the form. */
terms: [
"No payment now and no card stored — a reservation is a place in the queue.",
`Built to order in ${brand.city}, and shipped in the order reserved.`,
"Cancel with one reply to the email we send you.",
] as const,
finePrint:
"No payment now, and nothing stored beyond what is in this form. Cancelling is one reply to that email.",
} as const
export const footer = {
blurb:
"A single-dose espresso machine built around one number, in a shop in Turin. This is a demo of the Monolith launch template — every word on it lives in one file.",
columns: [
{
heading: "Product",
links: [
{ label: "Story", href: `${BASE}/story` },
{ label: "Specifications", href: `${BASE}/specs` },
{ label: "Reserve", href: `${BASE}/reserve` },
],
},
],
} as const"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useEffect, useRef } from "react"
import { BASE, brand, nav } from "../data/product"
/**
* The header, and the only client component in the chrome.
*
* The mobile menu is a native `<details>` rather than React state — nothing to
* get wrong about focus or aria-expanded. The one hook it needs exists because
* this lives in the layout, and a layout is *not* remounted on navigation: the
* panel would otherwise stay open over the page you just navigated to.
*/
export function SiteHeader() {
const pathname = usePathname()
const menu = useRef<HTMLDetailsElement>(null)
useEffect(() => {
if (menu.current) menu.current.open = false
}, [pathname])
const links = nav.map((link) => ({ ...link, active: pathname === link.href }))
return (
<header className="monoHeader">
<div className="monoWrap monoHeaderRow">
<Link className="monoBrand" href={BASE || "/"}>
<span className="monoBrandMark" aria-hidden="true" />
<span>{brand.name}</span>
<span className="monoBrandDescriptor">{brand.descriptor}</span>
</Link>
<nav className="monoNav" aria-label="Primary">
{links.map((link) => (
<Link
key={link.href}
href={link.href}
className="monoNavLink"
aria-current={link.active ? "page" : undefined}
>
{link.label}
</Link>
))}
</nav>
<details className="monoMenu" ref={menu}>
<summary className="monoMenuSummary">Menu</summary>
<nav className="monoMenuPanel" aria-label="Primary">
{links.map((link) => (
<Link
key={link.href}
href={link.href}
className="monoMenuLink"
aria-current={link.active ? "page" : undefined}
>
{link.label}
</Link>
))}
</nav>
</details>
</div>
</header>
)
}import Link from "next/link"
import { brand, footer } from "../data/product"
/**
* The footer, closing on an oversized wordmark that is meant to be clipped by
* the page edge. Server component — there is nothing here to hydrate.
*/
export function SiteFooter() {
return (
<footer className="monoFooter">
<div className="monoWrap">
<div className="monoFooterTop">
<p className="monoFooterBlurb">{footer.blurb}</p>
<div className="monoFooterCols">
{footer.columns.map((column) => (
<div key={column.heading}>
<h2 className="monoLabel">{column.heading}</h2>
<div className="monoFooterLinks">
{column.links.map((link) => (
<Link key={link.href} href={link.href} className="monoFooterLink">
{link.label}
</Link>
))}
</div>
</div>
))}
</div>
</div>
<div className="monoFooterBottom">
<span>
© {brand.year} {brand.name} — {brand.city}
</span>
<a href={`mailto:${brand.email}`}>{brand.email}</a>
</div>
<p className="monoFooterWord" aria-hidden="true">
{brand.name}
</p>
</div>
</footer>
)
}"use client"
import { useEffect, useRef, useState } from "react"
type RevealProps = {
children: React.ReactNode
/** Merged after `monoReveal`, so this is where the layout class goes. */
className?: string
/** Stagger, in milliseconds. Feeds `--mono-delay` in the stylesheet. */
delay?: number
}
/**
* Fades and lifts its children in when they first scroll into view.
*
* It renders a plain `<div>` and merges `className`, which means the revealed
* element *is* the layout element — `<Reveal className="monoPillar">` is a grid
* child of `.monoPillars`, not a wrapper that breaks the grid. That is the whole
* reason it does not try to be polymorphic.
*
* Three details are deliberate:
*
* - The observer disconnects on the first intersection. Re-hiding content the
* reader has already passed is motion for its own sake, and it means a long
* page keeps zero observers alive by the time you reach the footer.
* - The hidden state lives in CSS, not here, so nothing flashes in before
* hydration and `prefers-reduced-motion` can switch the whole thing off
* without this component knowing.
* - If `IntersectionObserver` is missing, the content shows immediately.
* Failing open is the only acceptable direction for a fade-in.
*/
export function Reveal({ children, className, delay = 0 }: RevealProps) {
const ref = useRef<HTMLDivElement>(null)
const [shown, setShown] = useState(false)
useEffect(() => {
const node = ref.current
if (!node || shown) return
if (typeof IntersectionObserver === "undefined") {
setShown(true)
return
}
const observer = new IntersectionObserver(
(entries) => {
if (!entries.some((entry) => entry.isIntersecting)) return
setShown(true)
observer.disconnect()
},
/* Held back slightly from the bottom edge so it reads as a reveal, not a pop. */
{ rootMargin: "0px 0px -10% 0px", threshold: 0.05 },
)
observer.observe(node)
return () => observer.disconnect()
}, [shown])
return (
<div
ref={ref}
className={className ? `monoReveal ${className}` : "monoReveal"}
data-shown={shown ? "true" : "false"}
style={delay ? ({ "--mono-delay": `${delay}ms` } as React.CSSProperties) : undefined}
>
{children}
</div>
)
}import { hero } from "../data/product"
/**
* The scrolling strip under the hero.
*
* The list is rendered twice because `@keyframes monoTickerScroll` translates the
* track by exactly -50% — with one copy the loop would jump. The second copy is
* `aria-hidden`, so a screen reader hears each phrase once rather than twice.
*/
export function Ticker() {
return (
<div className="monoTicker">
<div className="monoTickerTrack">
{hero.ticker.map((line) => (
<span key={line} className="monoTickerItem">
{line}
</span>
))}
{hero.ticker.map((line) => (
<span key={`echo-${line}`} className="monoTickerItem" aria-hidden="true">
{line}
</span>
))}
</div>
</div>
)
}import { specs } from "../data/product"
import { Reveal } from "./reveal"
type Group = (typeof specs)[number]
/**
* The specification table: one full-bleed hairline band per group, with the group
* name in the left column and the rows in the right.
*
* It is a `<dl>`, so a screen reader gets the term/value pairing for free — the
* thing a grid of `<div>`s throws away. Takes the groups as a prop so the home
* page can show two and the specifications page all of them.
*/
export function SpecTable({ groups }: { groups: readonly Group[] }) {
return (
<>
{groups.map((group, index) => (
<Reveal key={group.group} className="monoSpecGroup" delay={index * 70}>
<div className="monoWrap monoSpecGroupInner">
<h3 className="monoLabel">{group.group}</h3>
<dl className="monoSpecRows">
{group.rows.map(([term, value]) => (
<div key={term} className="monoSpecRow">
<dt>{term}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
</div>
</Reveal>
))}
</>
)
}"use client"
import { useState } from "react"
import { brand, reserve } from "../data/product"
/**
* The reservation form, with no backend — on purpose.
*
* A template cannot host a mail server or a database, and a form that silently
* drops what someone typed is worse than one that admits it. So this hands the
* message to something that can actually deliver it: the user's mail client, via
* a `mailto:` the browser opens. Nothing is stored and nothing is sent anywhere
* else.
*
* To make it real, replace `submit` with a server action or a `fetch` to your own
* route. The markup and the status line can stay exactly as they are.
*/
export function ReserveForm() {
const [sent, setSent] = useState(false)
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const data = new FormData(event.currentTarget)
const get = (key: string) => String(data.get(key) ?? "").trim()
const body = [
`Name: ${get("name")}`,
`Email: ${get("email")}`,
`Country: ${get("country")}`,
"",
get("note") || "(no note)",
].join("\n")
/* `encodeURIComponent` on both halves — a name with an `&` in it would
otherwise truncate the whole mailto. */
const subject = encodeURIComponent(`${brand.product} reservation`)
window.location.href = `mailto:${brand.email}?subject=${subject}&body=${encodeURIComponent(body)}`
setSent(true)
}
return (
<form className="monoForm" onSubmit={submit}>
<div className="monoField">
<label className="monoFieldLabel" htmlFor="mono-name">
{reserve.fields.name}
</label>
<input className="monoInput" id="mono-name" name="name" required autoComplete="name" />
</div>
<div className="monoField">
<label className="monoFieldLabel" htmlFor="mono-email">
{reserve.fields.email}
</label>
<input
className="monoInput"
id="mono-email"
name="email"
type="email"
required
autoComplete="email"
/>
</div>
<div className="monoField">
<label className="monoFieldLabel" htmlFor="mono-country">
{reserve.fields.country}
</label>
<input
className="monoInput"
id="mono-country"
name="country"
required
autoComplete="country-name"
/>
</div>
<div className="monoField">
<label className="monoFieldLabel" htmlFor="mono-note">
{reserve.fields.note}
</label>
<textarea className="monoInput monoTextarea" id="mono-note" name="note" rows={4} />
</div>
<div className="monoHeroActions">
<button type="submit" className="monoBtn monoBtnSolid">
{reserve.submit}
<span className="monoArrow" aria-hidden="true">
→
</span>
</button>
</div>
{sent && (
<p className="monoFormStatus" role="status">
Your mail app should be opening with the message ready to send to {brand.email}. Nothing
was stored by this page.
</p>
)}
<p className="monoFormNote">{reserve.finePrint}</p>
</form>
)
}