The system, not the screenshot.
Foundations
The whole system hangs off one number. body is 14px, and every size, gutter, radius and rhythm below is stated in em — so a single font-size change rescales the entire page, and a component dropped inside a smaller context shrinks with it instead of breaking out.
Sizes are relative, not absolute. A tier row set in 1em is 14px on the page and 10px inside the nav rail, because the rail re-declares font-size: 0.520833vw on itself. Nothing needed a second stylesheet for that.
Only three things are absolute. The 1px hairline (a hairline that scales stops being a hairline), the shell offsets --shell-pad-left / --shell-pad-top in vw so any band can offset by the rail without inheriting its font-size, and the fluid scaling head-room below.
The fluid ceiling. --size-container clamps 100vw between 992px and 2440px against a 1440px ideal, so the page keeps growing on very wide screens instead of stranding the content in the middle — but stops before the display type becomes a poster.
| Tier | Query | Columns | What moves |
|---|---|---|---|
| Desktop | ≥ 991px | 12 | The design width. Display 10.5em, H1 7.5em, H2 4.25em. Nav rail is a left column from 1200px. |
| Tablet | ≤ 991px | 6 | The grid halves to six and STAYS there for every tier below. Only H2 steps down (4.25 → 3.5em). Rail becomes a top bar. |
| Mobile landscape | ≤ 767px | 6 | Type compresses — display 6.5em, H2 2.25em, H3 2em — but the grid does not move. |
| Mobile | ≤ 479px | 6 | The scale bottoms out: display 5em, H1 4em, H2/H3 1.5em. H2 and H3 converge — pick by rank, not size. |
The trap: webflow.css re-declares --grid-columns on body at all four tiers, and three of those declarations do nothing. A custom property substitutes its own var() at the element it is declared on, and --grid is declared on :root — so it only ever sees :root's value. Read the painted column count, not the declaration nearest the element.
Colour
Six tokens and two shell-only extras. The palette is deliberately starved: one warm black, one white, one accent, one quiet grey, one hairline. Every surface in the product is built from that, which is why adding a seventh colour is the single fastest way to make the site look like someone else's.
Founder Weekend — Prague
Two days, eight founders, one room.
14–16 March · Vinohrady · 8 seats
Founder Weekend — Prague
Two days, eight founders, one room.
Secondary ink on light is a darkened grey, not --color-gray-light.
The one that bites: shell surfaces float over arbitrary page content, so they must declare their ink. The sticky bar shipped with black-on-glass prices for exactly this reason — it was inheriting the page's rgb(14,13,13). Any fixed or translucent surface sets color: var(--color) on itself.
Typography
One sans (PP Neue Montreal) and one mono. Eleven utility classes, and no size that isn't one of them. The rule that keeps it coherent: the bigger the type, the tighter the leading and the tracking — display runs at 0.9 leading and -0.04em, body at 1.2 and -0.02em.
10.5em · lh (0.9) · wght 500
7.5em · lh (0.9) · wght inherit
4.25em · lh (0.9) · wght 500
3em · lh (1) · wght 500
1.875em · lh (1) · wght 500
1.5em · lh (1) · wght 500
1em · lh (1.2) · wght 500
1em · lh (1.05) · wght 500
1em · lh (1.05) · wght 600
0.875em · lh (1) · wght 400
0.625em · lh (1.2) · wght 400
| Class | ≥992 | ≤991 | ≤767 | ≤479 | Leading | Tracking |
|---|---|---|---|---|---|---|
| .u-display | 10.5em | 10.5em | 6.5em | 5em | --line-height-90 (0.9) | --letter-spacing-tight-04 (-0.04em) |
| .u-h1 | 7.5em | 7.5em | 5em | 4em | --line-height-90 (0.9) | --letter-spacing-tight-02 (-0.02em) |
| .u-h2 | 4.25em | 3.5em | 2.25em | 1.5em | --line-height-90 (0.9) | --letter-spacing-tight-02 (-0.02em) |
| .u-h3 | 3em | 3em | 2em | 1.5em | --line-height-100 (1) | --letter-spacing-tight-01 (-0.01em) |
| .u-h4 | 1.875em | 1.875em | 1.5em | 1.25em | --line-height-100 (1) | --letter-spacing-tight-02 (-0.02em) |
| .u-h5 | 1.5em | 1.5em | 1.5em | 1.25em | --line-height-100 (1) | --letter-spacing-tight-02 (-0.02em) |
| .u-body | 1em | 1em | 1em | 1em | --line-height-120 (1.2) | --letter-spacing-tight-02 (-0.02em) |
| .u-details | 1em | 1em | 1em | 1em | --line-height-105 (1.05) | --letter-spacing-tight-02 (-0.02em) |
| .u-button | 1em | 1em | 1em | 1em | --line-height-105 (1.05) | 0 |
| .u-mono-large | 0.875em | 0.875em | 0.875em | 0.875em | --line-height-100 (1) | --letter-spacing-0 (0) |
| .u-mono-small | 0.625em | 0.625em | 0.625em | 0.625em | --line-height-120 (1.2) | --letter-spacing-loose-05 (0.05em) |
The scale steps, it doesn't interpolate. There is no clamp() on any type token. Sizes are re-declared at four breakpoints, which means the type is predictable at every width and there is no band where a heading is an awkward in-between size nobody designed.
What clamps instead is the container. --size-container is a clamp, and above 1440px it grows the whole fluid scale together. Below 992px it is pinned, and the breakpoint steps take over.
Line length is clamped in em, not px. Standfirsts cap at 34em, hero subcopy at 24em, prose at 38em. That is roughly 60–75 characters at every size, automatically.
Weights are static, not variable. The original export drove weight through font-variation-settings: "wght" 500. PP Neue Montreal ships static cuts, where that declaration is inert — so fonts.css restates font-weight: 500 for the headings. If new type looks unexpectedly light, that is the cause.
Use the ratio, not the pixel. Leading is stored unitless (--line-height-90 is 0.9), so it survives every re-scale. Never copy a resolved px line-height into a component.
Mono is a label typeface. .u-mono-small is uppercase at 0.625em with +0.05em tracking. It carries kickers, keys, counts and captions. It never sets a sentence.
Eight founders, one room
Product, growth engineering and positioning, worked through in person over two days.
Grid
Twelve columns on desktop, six on tablet, a 1.25em gutter and a 1.25em page margin — and the margin and the gutter are the same value on purpose, so a full-bleed edge and an internal gap read as one rhythm. Every band is a .u-container wrapping one or more .u-grid rows.
At 991px --grid-columns drops to 6 and columns 7–12 wrap to a second row — and it stays at 6 all the way down. There is one grid change in the whole system, so a twelve-cell row only ever has to read as two rows of six.
Bands do not nest containers. One .u-container per band, then as many .u-grid rows as the band needs. Nesting a second container doubles the page margin and the band silently narrows.
The shell owns the left edge. --shell-pad-left is applied once on the page wrapper. Bands never offset for the rail themselves — if a band looks pushed in twice, that is why.
Column width is derivable. --grid-column-width computes a single column from viewport, margin, gutter and count — use it when something must be exactly one column wide outside the grid (a pinned label, an absolutely-positioned figure).
Offsets use start/span, not margins. grid-column: 5 / span 6 beats a left margin, because it survives the column count halving.
Declare the count where the grid is. A route that does not load slater.css — the funnel, which drops the motion stack — has to restate --grid and --grid-columns at :root. Restating only the first silently gives every row twelve columns at every width.
Spacing & rhythm
There is no numbered spacing scale in the tokens — there is a set of steps the bands actually use, and staying on them is what makes pages feel like one site. Below is that set, measured out of the shipped CSS rather than invented for this page.
One border, everywhere. 1px solid var(--color-gray-dark). There is no second weight, no second colour and no shadow anywhere in the product — depth is done with blur and gradient, never with a drop shadow.
Above for headers, below for rows. A section header takes a border-top and 1.25em of padding-top. A list row takes a border-bottom and symmetric padding. Mixing the two is what produces doubled rules between stacked blocks.
Radii. 0.1875em for surfaces (chips, panels, plates), 0.125em for tags and inputs, 0.25em for the two emphatic cases (the newsletter button, the offer card). Nothing is fully round except the booster check.
Cards & surfaces
Five surface treatments, each with a job. The rule that stops a page turning to mush: a surface either has a fill or a border, never both, and only one surface per screen gets to be translucent.
Positioning teardown
Your message, pulled apart and rebuilt on the Saturday morning.
Floats over video and arbitrary content, so it declares its own ink.
Unavailable rows drop to 0.45 opacity — they are never removed, because the ladder is the pricing argument.
Dashed = an offer not yet taken. It goes solid and tints on accept, which is the only place the accent is used as a fill.
Forms
Inputs are filled, not outlined — a --color-gray-dark field on the black ground, 2.8125em tall, with a 1.25em left inset that lines up with the page margin. Labels are mono, above, and always present.
Placeholder is a hint, never the label. The label stays.
Errors print in --color-accent-red-light beneath the field at .u-details, never as a toast. Disabled is 0.45 opacity, matching the locked tier row, so “not yet” looks the same everywhere.
Motion & parallax
Three easings, five durations, and a declarative parallax layer driven entirely by data attributes. The house style is fast out, long settle, no bounce — the site should feel weighty, not springy.
| Duration | Where it is used |
|---|---|
| 0.2s | Booster check, hover fills — anything under the cursor. |
| 0.25s | Ink-only hover on quiet controls (drawer close, tier add). |
| 0.4s | Drawer scrim fade. |
| 0.45s | Sticky book bar in/out, drawer panel slide. |
| 0.6s | Nav rail reveal and its background cross-fade. |
| Attribute | Default | Notes |
|---|---|---|
| data-parallax="trigger" | — | Marks the element ScrollTrigger measures. Required. |
| data-parallax="target" | the trigger itself | The element that actually moves. Give it inset overflow to move into. |
| data-parallax-direction | vertical | "horizontal" switches the tween from yPercent to xPercent. |
| data-parallax-start | 20 | Percent offset at scroll-start. Positive = starts pushed down. |
| data-parallax-end | -20 | Percent offset at scroll-end. The default 20 → -20 is a 40% travel. |
| data-parallax-scrub | true | A number becomes GSAP's smoothed scrub, in seconds. 0.5 is the house value when you want lag. |
| data-parallax-scroll-start | top bottom | Wrapped in clamp() so it never overshoots at the top of the page. |
| data-parallax-scroll-end | bottom top | Also clamped. |
| data-parallax-disable | — | "mobile" | "mobileLandscape" | "tablet" — opt a band out per tier. |
The target needs somewhere to go. A parallax target must be larger than its trigger — the panels above use inset: -20% 0 so a 40% travel never exposes an edge. Matching the default travel to the overflow is the whole trick.
Scrub is not a speed. true pins the tween to the scrollbar exactly; a number is the seconds of catch-up lag. Use 0.5 for large soft imagery, and true for anything the eye can check against a hard edge.
Everything is scoped and re-initialised. The layer re-runs per band scope, so a band added to a route picks up its motion with no registration step — and a band removed tears its triggers down with it.
The funnel's own devices
Six pieces that only exist between the basket and the receipt. They are the same system as everything above — the only new idiom is the dashed border, and it means exactly one thing.
A completed step gets a rule through its number. The active one goes to full ink. Nothing animates: a progress bar that moves on its own reads as a loading state, and this is not one.
A funnel that hides its exit is one people learn to distrust. The decline is legible, keyboard-reachable, and plainly not the primary action — which is the whole of the design.
Every figure here is an integer number of cents until the last possible moment. There is no float anywhere in the money path.
What works, what doesn't
The short version. Everything on the right is something that has actually gone wrong in this codebase, not a hypothetical.
- Reach for a token firstIf the value you want isn't in the token set, the question is usually whether the design is right, not whether the token list is short.
- One accent per viewThe red marks the thing you want clicked. Two red things on a screen means neither reads as the action.
- Write the 991px case explicitlyIt is the only grid change in the system — 12 above, 6 below, all the way down. Write it once and nothing else needs a per-tier span.
- Fixed surfaces declare their inkAnything positioned or translucent sets its own color.
- Let the em cascade do the workSize a component once, then let its container decide how big it is in context.
- Hairline above for headers, below for rowsConsistent divider direction is most of what makes long pages feel calm.
- New greysThere are two. A third one always looks like a mistake next to the other two, because it is.
- px inside a componentIt survives until the first time the component is used at a different scale, then it is the only thing that didn't move.
- Hand-rolled buttonsMissing the data attributes gets you a button that renders and never animates — which reads as a broken build, not a variant.
- Drop shadowsDepth here is blur and gradient. A shadow instantly makes the page look like a different product.
- Resolved line-heightsCopying 52.5px instead of 0.9 pins the leading and breaks every other breakpoint.
- Nested containersTwo .u-container elements double the page margin, and the band quietly stops lining up with its neighbours.