What the marketing site settled on over two days of design passes: one flat,
token-driven system with no boxes, one heading scale and one body size. Written down so the next
change does not have to rediscover it. Token values here are read from tokens.css;
where this page and the codebase disagree, the codebase wins.
Most of the work over these two days was removing things. The system is subtractive: rules and type carry the structure, not containers.
--wrap-interior: 1000px on
every interior page.01 · Find became
01 Find.Two faces. Geist for everything readable, Geist Mono for technical naming and
small labels. There is no third display face on this site — --font-display
resolves to Geist.
--h-displayclamp(38px,6vw,72px)--h1clamp(34px,3.6vw,52px)--h2clamp(28px,3.4vw,44px)--h3clamp(24px,2.6vw,32px).band-h. If a page invents its own heading size, it is wrong.--statement--h3. Closers, CTA headlines, in-band leads.--body: 17px--measure: 770pxMono, uppercase and wide-tracked reads as a label. That is right for group headings inside the nav panel, figure captions and technical identifiers. It is wrong for anything that is a proper noun or prose:
ACREW CAPITAL — a firm is a proper noun, not an eyebrow.io.moderne.prethink.ExtractCodingConventions became
"Extract coding conventions"..tmk by lib/trademark, sized
max(9px,.58em) with line-height:0. The floor matters: a flat
.58em put the trademark attribution in an 11.5px legal disclaimer at
6.67px, the one place it has to be legible..nb (nowrap) by lib/nobreak, up to 22 characters.
CSS cannot do this: hyphens:none and word-break:keep-all both
leave an explicit hyphen breakable. Verified in Chromium.<br/>. At any
max-width that fits line two, a word creeps up and strands the last one.Never hardcode. Every colour is a token, and the day/night values are mixed
through a single --warm dial, so a raw hex in a stylesheet cannot follow the
ground it sits on.
On light grounds the accent is the forest green #1D5937, not
Digital Green #30F284 — the bright green falls to about 1.5:1 on cream and is
unreadable. Green is also never the only signal: it is always paired with an
underline or an icon.
--line--hair--line. For ruling large
grids, where a full-strength rule on every cell edge reads as a cage.--tick--logo-filterOne divider, not two. Two components that each own a top rule will sit 26–125px apart and read as a doubled line. Only one of them should draw it.
Figures are same-origin HTML documents in iframes under
/embeds/, auto-sized to their content. They are authored, not drawn, so they can be
re-rendered when the copy or the palette moves.
SVG text is in user units and scales with the container. A fixed
13px label against an 860-wide viewBox renders at 13.6px near 900 and
5.8px at an iframe width of 386 — unreadable on a phone.
Step the user-unit size up as the scale falls. The value is
13 ÷ (iframeWidth ÷ viewBoxWidth) at each breakpoint. After fixing:
12.6–13.6px at 1440, 820, 600 and 430 alike.
#F7F5F0 — lighter than the cream ground, deliberately not
white.--body colour. 12px at --muted made them
the faintest text in the figure, and they are what the table hangs off.documentElement.scrollHeight is clamped to the iframe's own
viewport, so it can never report less than the height already set. One oversized
first measurement locks the frame tall permanently and the ResizeObserver can
only re-confirm the bad value — a one-way ratchet. That was the enormous white space
under an in-article diagram.
body.scrollHeight is content-driven and shrinks back. Add a 1px tolerance
before writing the height, or the write resizes the frame, re-fires the observer, and the two
loop on sub-pixel deltas.
The standing rule: use the asset that was given to you. Do not re-author it, do not trace it as SVG, do not "improve" a hand-drawn mark. Crop it, extend it, or place it — but it stays the same artwork.
When a portrait is framed too tight for its plate, extend the image, don't crop it harder. Pad sideways with matched background so the full subject fits the target ratio.
-wide / -padded, so framing can
be regenerated.Images use initLightbox via .js-zoom. Iframes cannot
— a click inside an iframe never reaches the parent document — so diagram embeds get
a transparent button laid over the frame. Zoom re-lays the document out at 820px and pans
rather than scaling the phone layout up; scaling only enlarges the same small labels.
n / total count, right-aligned dash indicators.
The blog featured slider and the newsroom carousel share it.Blog and social cards are authored as HTML and rendered, never drawn in a raster editor. The source lives beside the embeds so a copy or palette change is a re-render, not a redraw.
/embeds/ at the exact thumbnail frame, with a comment
saying so: do not hand-edit the raster.og:image and twitter:image point at the same file. Check the
share preview before merging.One top rule over the group, a hairline between columns, no plate and no radius. The trap is
the gutter: the rule sits at a column's right edge, so the padding before it
needs matching space after it, or the next column's text butts straight against the line. Either
a column-gap, or padding-left on non-first columns — not both, or
the gutter doubles.
Internal hairlines only: a cell rules the side facing a neighbour and never an outer edge, so the wall gains structure without gaining a frame. Express it as "top rule from row 2 on, left rule from column 2 on" — then no rule depends on knowing the final row, and a ragged last row stays clean at any item count.
4:3 (not 16:9 — the headshots are square and a wider plate crops past the chin), greyscaled so unrelated studio backdrops stop fighting, name in Geist, role in mono, an underlined text link. No circular avatars, no cards.
Four small marks at the corners frame a quote without a box. Drawn as four background layers — pseudo-elements only give you two.
Each of these cost real time over the two days. They all look correct in the stylesheet.
:not(:nth-child(Nn+1))Restating a ruled grid per breakpoint with a plain class reset silently loses — it renders correctly at one width and draws stray edge rules at the others. Give each column count its own bounded media range instead of overriding the one above it.
Two rules at (0,1,0) are decided by which loads last — and the dev
server's per-file order is not the production bundle's. A hero paragraph rendered
white locally and dark slate on the deployed site. Scope to raise specificity rather than
relying on order. A local-only check will not catch this.
If the layout change alters the container's own display, the
container-type has to sit one level up.
z-index child paints over its parent's backgroundUsed as a "bar behind a button", it swallows the button's own fill. And a
box-shadow spread inherits the element's border-radius, so it paints
a rounded slab, not a bar. Both need a real wrapper.
A 1px gap on a flex column lands on top of negative margins and throws a
38px row pitch to 39. An inline-block child sits on a baseline and pushes a
44px row to 49 — make the row a flex container and the problem disappears.
A page called initLightbox but did not import the sheet that styles
.lightbox. The overlay was built and un-hidden on click, with no position, no
scrim and no sizing — so the image simply never appeared to zoom. If a helper is shared,
its CSS belongs in the shared sheet.