← Back to the screen

Design system portfolio

calm-practice — the record

The four direction lines behind this page, the generator command that produced its tokens, and the pairs it was measured against.

A caseload midwifery practice in Leeds: the page expectant parents land on.

Where the style comes from

The reference is an Elementor site for a counselling practice in Texas. The brief was to carry everything about its design except the colour, so the vocabulary below was measured off its own published kit and its rendered DOM rather than described from a look at it.

DecisionMeasured
Display faceCormorant Garamond, weight 500 throughout
Text faceBarlow — body 16px desktop, 15px mobile, leading 1.6em
Serif ramp68 / 58 / 46 / 36 / 30 / 25 / 22, leading 1.1 at the top loosening to 1.4
EyebrowBarlow 600, uppercase, 2.6px of tracking at 15px
Buttons50px pill, 50px tall, 12px 27px, one wide variant at 15px 75px, sentence case
Container1024px on 53 elements; 1240px on two
Radius census50px ×29, 10px ×12, 50% ×6, 10px 10px 0 0 ×5, 60px ×4
Shadowone, 2px 8px 23px 3px rgba(0,0,0,0.2), used three times
MotionfadeIn ×29, fadeInUp ×7, fadeInRight ×3; transitions all 0.3s
Linkscoloured with the accent, not the primary

Structurally: a dark utility strip above a sticky header; a centred nav with carets on the sections that open a submenu and a pill call-to-action on the right; a 50/50 hero with the words centred on paper and a full-bleed picture beside them; an italic phrase inside the roman headline; two filled buttons side by side; a circular medallion sitting on the seam between the hero and the band under it; three columns separated by thin rules rather than boxed; a large italic pull-quote; alternating picture-and-words rows; a bulleted list of concerns; cards whose pictures are rounded at the top only; a numbered process; and an FAQ.

Only the hue family moved: sage at roughly h 145 became a dusty blue at h 251.142, read out of the reference's palette by colorkit.py. The clay accent, h 48.691, is the reference's own.

The direction

The product changed once, deliberately. The first version of this specimen was a client's private care record. Most of the reference's vocabulary — the FAQ, the numbered process, the fee cards, the two-button call to action, the utility strip — is public-page vocabulary that a private record cannot host honestly, so carrying all of it meant making the page the one the reference itself is: a practice explaining itself to people deciding whether to book. The urgent panel survived the move and is better for it; somebody frightened at three in the morning reaches a public page, not a logged-in portal.

Generator

python3 tools/calm_tokens.py --both > systems/calm-practice/tokens.css

Nineteen of the other twenty specimens take tokens from a vendor's published generator. This style has no vendor, so the portfolio ships one. It declares the grounds — a ground is the one honest choice in a palette — and solves everything else: the nearest OKLCh lightness, at a declared chroma and hue, that clears the WCAG ratio its role owes against the theme's worst ground.

The type ramp is not a ratio anybody chose. The reference publishes every step three times, at desktop, tablet and mobile, so each size is one clamp() that type_scale.py solved through its own two endpoints — 68/40 at the top down to 15/14 at the bottom. Its ramp is not a constant ratio (68/58 is 1.17, 58/46 is 1.26, 36/30 is 1.20) and imposing one would have replaced a measured ladder with a tidier invented one. --self-test asserts every emitted clamp still brackets the published endpoints.

--check measures 66 pairs across both themes. --self-test also runs the negative half: an unreachable ratio must raise rather than return a colour that does not clear it, and the decorative rule must sit below the 3:1 a control boundary owes, so substituting it for stroke has to be somebody's decision.

What the scripts found that reading would not have

The third grey did not exist. ink, ink-muted and ink-subtle were a hierarchy until --check printed them: the second and third both owe 4.5:1 against the same ground, so they solved to the same lightness and measured 5.28:1 apiece. Two ink levels ship; below secondary, rank is carried by size and tracking.

Primary and accent were the same tone. composition.py measured them at 1.029:1 in greyscale — 159.5 degrees apart in hue and identical in value, because both solved to 3:1 against the same ground. Two button fills that differ only in hue are one button to anyone reading without colour. primary now solves to 4.5:1, and the self-test asserts the greyscale gap.

The solver was right and the stylesheet was wrong. a11y-probe.mjs found four dark-theme pairs at 4.47:1 against the 4.5 they owe while --check reported 4.50. The generator solved in continuous space and emitted a rounded hex, and because it stops at the nearest lightness that clears the bar, every role sat exactly on its minimum and any downward rounding put it under. It now measures the quantized value.

A band pads whether or not anything is in it. The slot holding the hidden urgent panel reserved 128px above the first word on every load.

Two figure sets, both wrong by default. Cormorant Garamond ships old-style figures, so the emergency telephone number drew with descending zeros and the step counter drew "1" as a lowercase-l shape beside a lining 2 and 3. The phone number moved to the sans; the counter is set lining-nums. Old-style stays everywhere it belongs, which is running text.

Mist on mist. The team cards' pictures used the same ground as the band behind them, so the picture half of every card disappeared and the cards read as labels floating in space.

The contrast checker passed text it could not see. The closing band sets cream type over the lake frame. a11y-probe.mjs reported it clean, because it resolves a background colour and a photograph does not have one. tools/measure_onimage.mjs shoots the band twice — once normally, once with the glyphs hidden, since a bright background pixel and a cream glyph are the same colour to a histogram — and reads the worst pixel actually behind every run at three widths. It found 7 of 12 failing, the eyebrow at 1.07:1 on a phone. The first scrim was bottom-weighted on the argument that the type would sit low over dark water; the crop moves at every width and the argument did not survive it. A 0.40 floor everywhere, which is scrim.py's whole-frame answer, gives 0 failing.

One of those twelve was the checker's fault, not the page's. The phone eyebrow measured 1.07:1 against a near-white pixel that no 0.40 scrim could produce — because the sticky masthead was sitting on top of it. Which exposed the real defect: collapsed to a disclosure the bar is 149px on a 375px screen, and it was 195px before that. It is now sticky only above 64rem, where it is 75px.

A solver that walks the wrong way. image-ground is the colour under a photograph, and asking the lightness solver for it returned #4e545a — a mid grey. The solver walks toward the LIGHTEST value that clears a ratio, which is correct for text on a ground and backwards for a ground. It is declared at the frame's own darkness and the ratio is asserted against it instead, at 14.43:1. That token is not cosmetic: it is what the cream type lands on if a frame 404s, and without it the type was invisible in that case.

min-height is a floor. In flow the hero frame's intrinsic 2:3 ratio set the grid row to 1080px and put the primary call 925px down — under the fold on every laptop. A min-height cannot cap that. The frame is out of flow now and the words decide the height; the call clears the fold at 1280x720, 1440x800, 1440x900 and 1920x1080.

The hero owns the first screen, and the bar sits on the page's own hold

Two corrections, both asked for after looking at the rendered page rather than the source.

The hero takes the whole first screen. It was min(84svh, 46rem), which left 37px of the mist band showing at 1440x900 and 217px at 1920x1080 — the shape that reads as a page continuing rather than a page opening. It is calc(100svh - var(--cp-head-h)) now, so the band under it is something you scroll to. --cp-head-h is composed rather than read off a screenshot: the strip is its own row height between two --cp-space-2xs, the masthead is one --cp-control-height between two --cp-space-xs plus its rule, and measure_calm.mjs asserts the sum against the rendered bars at 1440 — 127 against 127 — because a header that grows by a line is a hero that quietly stops filling the screen and nothing on the page looks wrong while it happens. Measured at 1152x864, 1366x768, 1440x900, 1680x1050 and 1920x1080 the next band begins exactly at the fold; at 1280x720 and 1024x768 the words are taller than the remainder and it begins below it. Below 64rem nothing changed: the hero was already 983px against an 812px phone.

One hold down the whole page. The strip, the masthead and the footer held 1240px while the fifty-three elements under them held 1024, so at 1440 the wordmark began 108px to the left of the first word of the hero and the bar read as a different document laid over the page. All three take --cp-container now and hold--wide has no users left. It costs one thing and the nav pays it: six links need 534px at --cp-space-md against the 517px the narrower hold leaves between the wordmark and the ask, so the last one wrapped and the masthead grew a row. The nav gap is one step down the scale at --cp-space-sm, which is 494px and fits from 1152 up. At 1024 it still wraps, as it did before the change, because the gutter takes the hold to 922px there either way.

Deviations from CONTRACT.md

§1, "tokens come from that system's own verified generator". There is no vendor and therefore no vendor generator. The portfolio ships calm_tokens.py, cited above and in the page's spec panel. Nothing in tokens.css is hand-written.

§2, "every <img> is inline SVG or a data: URI". The five frames are files under img/ and the page references them by path, which is the deviation luxury-hospitality already takes and for the same reason. A data: URI is base64 on the LCP path and cannot be served per breakpoint; as files the hero is 26 kB of AVIF at the width a 1x laptop asks for and 40 kB at 2x, and a 1440px desktop fetches 246 kB of AVIF in all, or 329 kB on a 2x screen — out of 511 kB across thirteen renditions, which is the ladder rather than anything one reader downloads. tools/encode_images.mjs produces them and writes img/manifest.json, which records for every rendition what made it.

The pictures are generated, and CREDITS.md says so. codex_image.py (gpt-image-2) drew them and lens_post.py put the lens back — vignette, grain, bloom, chromatic aberration. Under photoreal-media's own decision rule this is the side of the line generation belongs on: none of these frames makes a claim about a specific real object anybody commits money against. They are a room, a lake and a person whose face is not visible.

The audit was being run on the wrong file, and that is now fixed. render_audit.py passed every master and the manifest recorded "0 failing on every frame", but nothing anybody downloads had ever been measured. Measured on 2026-09-14 the shipped hero read 0.145 levels sigma — the auditor's "no camera produces this". The resize was innocent; the encoder was not, because throwing noise away is what a lossy encoder is for. encode_images.mjs now re-grains after the resize and searches the sigma against the encoded file, stopping at the first rung inside the audit's 0.6-8.0 band in both formats. Ten of the thirteen renditions needed nothing; only the hero did, at sigma 3.2, for +21 kB on the widest WebP. Every rendition's measured floor is a number in img/manifest.json rather than a sentence. CREDITS.md has the working.

The team stays as drawn monograms, deliberately. A photorealistic face under a name and an NMC registration number is a fabricated credential, not a lifestyle frame, and it is the one place on this page where the decision rule comes down the other way.

§6, direction_check.py --ledger reports hero-undecided. Two structural causes. The rule counts <img|picture|video|canvas as media and §2 of this contract requires every image to be inline SVG — an <img> carrying a data: URI cannot read var(--cp-mist), so the art would stop theming. And its masthead test reads literal font-size values, which §1 forbids writing. The page was measured in a browser instead: the headline computes to 63.33px against a 20px body at 1440, a ratio of 3.17, where direction_check's own focal threshold is 2.0. The same blindness leaves type scale, shape, density and accent reading unset, which README.md already records as the known limit of three of its six dimensions.

§3, the two recessive bands. mist and sunken measure 1.04:1 against each other in greyscale. They alternate by temperature rather than by rank, and no meaning rests on which of the two a section sits in.

Kept warnings

The one thing I am least sure about

Whether the urgent panel should reorder the page at all. Recede-and-overlay keeps the page present and reversible, which is the humane behaviour, but it also means a frightened reader sees what they were reading greyed out behind what they now need. A version that replaced the page outright would be calmer to look at and worse to recover from. This is the decision a real practice would have to test with real clients rather than settle from a rulebook.

The three things the reference does that this now does too

The stamp. Its own is a hand-torn disc with a thin inset ring and a loose inked sprig; three SVG strokes were never going to be that. codex_image.py drew both halves as transparent silhouettes and the page paints a TOKEN through each with mask-image — so the artwork is generated and the colour stays in the design system, which also means the mark inverts correctly in dark, where ink and paper swap. The disc came back with a soft hollow through the middle; tools/solidify_mask.mjs floods the true outside inwards from the border and fills whatever the flood never reached, so the torn edge it got right is kept and the interior it got wrong is repaired rather than regenerated.

The masks are inlined, and that is a correctness fix. Chromium treats a CSS mask as an origin-sensitive resource. These pages run from file:// by §2, so mask-image: url(img/…) failed with ERR_FAILED and the mark silently did not paint. tools/inline_masks.mjs writes them into the page as data: URIs between two markers — 3.8 kB raw, 5.1 kB base64 — which is what §2 asked for anyway. They carry alpha rather than luminance: a greyscale mask is half the bytes and -webkit-mask-image reads alpha, so a source without it comes out a filled square in Safari.

The band you scroll past. background-attachment: fixed, which the reference uses twice and switches off at 1024px. Same call here, and off under prefers-reduced-motion as well, where a background refusing to move with the page is the parallax the setting exists to stop. Its frame is the folded linen, and that frame is graded rather than shot as found: ungraded, scrim.py wanted 0.49 of black under cream type or 0.71 of white under dark type, either of which erases the weave. Walking the grade against both checkers put it at 0.66 brightness — the point where render_audit.py still reports 0 failing and the scrim falls to 0.19.

The things that arrive. The reference marks 31 elements with an invisible-until-seen hook and plays fadeIn staggered 0 / 200 / 400ms. That stagger is the part worth carrying: a section whose parts arrive together reads as a slide, one whose parts arrive in order reads as a sentence. Six lists here carry it at 120ms per child.

And the thing that nearly shipped broken. The comment above that reveal said "with no script every element is simply visible". It was false: with JavaScript off, .seen is never added and 8 of the 9 sections sat at opacity 0 — the page's content, invisible. A <noscript> style unhides them now, which is the same device this library's own site uses, and it is the requirement the award's developer guideline states twice. The reduced-motion block had the matching defect: it shortened transition-duration but not transition-delay, so a reader who asked for less motion would have waited out 600ms of stagger and then seen an instant jump. motion_lint named both.

The tell I had the rule for and shipped anyway

anti-slop.md has named "hand-rolled decorative SVG standing in for illustration or photography" since it was written. The section ornament here was exactly that: one bezier with eight identical lens shapes spaced evenly along it. It read as geometry because it was geometry — no two leaves on a real branch are the same length or the same angle, and typing forty different ones is the work the illustration was avoiding. The three team plates were the same defect again, a drawn circle with a letterform in it.

It kept shipping because it was the one tell on that list nothing could measure. slop_scan.py now carries hand-drawn-illustration, and the discriminator is not "inline SVG" — that is how every icon on this page is drawn:

canvasprimitives
an icon16–48 unitsa handful
a tool exportlargehundreds, plus <defs>
hand-plottedlargea few dozen

Run over this portfolio the first version reported 8 findings and 6 were false — four charts, a sparkline and a set of crop-mark guides. A chart is a large canvas with few primitives too; what it is not is decoration standing in for a picture. Three exclusions fixed it and each came from a real page: the data-viz markers, an <svg> carrying <text>, <title> or <desc> (a cider label inside a canvas app labels itself; a branch does not), and the wrapping container, because the waffle chart put its marker on the parent and left the <svg> bare. It now reports clean across all 21 specimens and still fires on the ornament that started it. Four negative fixtures hold that line.

What replaced them: the ornament is a pen-and-ink botanical study with every leaf a different length and angle, carried as a mask so the ink is still a token; the team plates are handmade paper with the initials set on them and one CSS ring, which is a shape a border already makes.

The mark

The first stamp was a botanical sprig, and it read as an olive branch — which is the reference practice's own name and its own logo. That is borrowing an identity rather than a style, and it is the opposite of what a specimen is for.

Wrenfield is a wren and a field, so the mark is both: a wren with its tail cocked the way only a wren holds it, perched on a bending meadow-grass stem with a drooping seed head, inside the same thin ring. The section ornament was re-drawn to match — a stand of meadow grasses rather than a generic leafy branch — so the ornament and the stamp are the same place.

The wordmark is a separate drawing on purpose. The stamp's line work is mud at 30px, so the logo is a solid silhouette with chunky proportions, checked at 28px before it was wired in; the ring around it is a CSS border, because a circle is a shape a border already makes. Both are masks, so the mark takes --cp-ink and inverts with the theme like everything else.

The favicon is the one place with baked colour in this specimen, and it has to be: a favicon is a static file the browser paints outside the document, where no token can reach. tools/inline_masks.mjs generates it from the same silhouette so the identity has one source — knocked out of a filled disc rather than shipped as a bare silhouette, which loses its tail at 16px.

Texture

The reference lays real texture files under two of its sections — texture-1 and texture-3 — and that is most of why its flat colour does not read as flat colour. A sheet of handmade cotton rag is generated here and laid under four bands at 0.4 opacity, multiplied, below the content in the stacking order so it never sits over type. In dark it switches to soft-light at 0.22, because multiply against a near-black ground crushes it to nothing.

What the grain cost, and the probe that found it

Laying paper over the bands changed the ground every text role sits on, and the generator did not know. Solved against the undarkened value, the accent link on the warm band measured 3.57:1 against the 4.63 the generator believed it had. GRAIN_FACTOR closes that: the shipped texture's 0.5th-percentile luminance is 0.8156, so a multiply at 0.30 takes a ground to 0.9447 of its value, and the light theme now solves against the worst ground darkened by it. The percentile was walked, not picked — at the 2nd percentile three eyebrows still measured 4.40, because grain is stochastic and a ground is a distribution rather than a value.

The sheet is on the paper bands only. Mist is the darkest ground and the one that is not paper, and graining it held seven eyebrows between 4.41 and 4.47. The warm bands carry it with room to spare.

tools/measure_text_grounds.mjs is what found all of this: 320 visible text runs across four widths, 0 failing. Getting it to tell the truth took four corrections, and each one had produced a confident false finding first:

It reportedBecause
1.57:1 on an FAQ answerthe <details> was closed — a box with a rect and nothing on screen
5 runs instead of 50it tested visibility BEFORE scrolling, and a reveal section is opacity 0 until seen
1.03:1 on a filled buttonvisibility: hidden takes the element's own background, which IS its ground
2.35:1 on six list itemsthe element box contains its own ::before bullet; the glyphs' ground is the line box

It measures the text's own client rects, per line, with the glyphs made transparent rather than the box hidden. The ornament failure it found was real: stems passing under the right-hand column read 2.46:1, and 1.69:1 behind the phone link on a narrow screen. Decoration under type is the same defect as type over a photograph, so the ornament moved into the margin — its width is whatever the hold leaves over, and below 80rem there is no margin, so it is not drawn.

The highlight, and why a contrast ratio was the wrong tool

The code chip in the spec panel measured 1.29:1 against the band behind it, and did not read. Reaching for a warmer fill looked like the fix: sunken and accent-quiet both measure 1.03:1 there, which by that number is worse.

A WCAG ratio compares luminance and says nothing about hue, and hue is the whole question for two quiet surfaces. Measured as delta-E OK instead, where about 0.02 is one just-noticeable step, the order inverts:

against the blue bandlightdark
card0.09020.0349
paper0.07150.0414
sunken0.04890.0770
accent-quiet0.04080.0409

card is the best fill in light and the worst in dark; sunken is the reverse. No single token wins both, so each theme takes its own. The edge is rule in both, at 0.2631 light and 0.2242 dark, and it is the edge rather than the fill that makes the shape read at all. stroke measures higher still and is the token this system reserves for a control boundary, which a chip is not.

Chip text lands at 11.00:1 in light and 8.69:1 in dark.

The panel is written for a reader

It was a build log: --cp-space-2xl, a 1024px hold, OKLCh lightness. CONTRACT §3c requires the four direction lines, the generator command verbatim and the two measured pairs, so all of that stays — but the lines are now Shape of the page, What it leads with, The one bold decision, Corners, Where the colours come from, and the command sits after a sentence saying what it does.

Rewriting it caught three stale numbers the panel had been asserting since the palette last re-solved: 7.00 was 7.88, 4.50 was 4.52, and sixty-six pairs were sixty-seven. It also still claimed the pictures were line drawings, which stopped being true four photographs ago.

No em dash appears in anything a reader sees or a screen reader announces.

Against the award rubric

expressive-web.md carries the circuit's published weights rather than a reputation. Awwwards scores Design 40%, Usability 30%, Creativity 20%, Content 10%; the Developer Award weights WPO 0.20, RWD 0.20, Semantics/SEO 0.20, Markup 0.15, Animations 0.15, Accessibility 0.10 — 0.85 of which this library already gates, and this page passes all of it.

expressive_check.py reports four of six expressive levers spent — scroll-motion, material, transition, fallback — and none of the eight genre failures present in the source. The two unspent are deliberate:

Two further numbers sit below that benchmark for the same reason. The display face reaches 11.2% of elements against its 2.6%, and one element carries negative tracking against its 110 — because the reference sets every heading in Cormorant and its kit declares no negative letter-spacing anywhere. Fidelity to the reference and the benchmark's restraint point in opposite directions here, and the brief settles it.