This site's own system

The design system, as it actually is

Every number on this page is read out of styles.css when the page loads. Nothing here is transcribed by hand, so this cannot drift out of date the way a written style guide does: if the stylesheet changes, this page changes with it.

Which also means it cannot flatter. Several counts below are marked ad hoc because they are: this stylesheet still carries values chosen one page at a time, and a guide that rounded those down would be the kind nobody trusts twice. The numbers are the point: they are how we know whether the last tidy-up worked.

This documents lunarwebstudio.com. A build on the Voyager plan gets one of these for its own brand (the same structure, its colors, its components, its numbers) not a copy of ours.

How to use it

Everything below this section is measured from the stylesheet. This part is not: it is the set of rules the numbers cannot state, written down because each one was learned the expensive way.

Reaching for a value
  • Spacing, type, radius: use a step. If nothing fits, the honest move is usually a different layout rather than a new value: every off-scale length in this file started as somebody deciding this one case was special.
  • Under 4px is not spacing. The handful of .1rem and .15rem values are optical nudges holding one thing in line with another. Snapping them to a grid breaks the alignment they exist for.
  • em means "scales with its parent". It is not a smaller rem, and swapping one for a step changes what the value depends on.
Color
  • A translucent version of a palette color goes through the palette: rgb(var(--accent-rgb) / .34), never a fresh rgba(255, 138, 60, .34). The second one is a copy that stops tracking the moment the palette changes.
  • Gradient stops are not duplicates. Over half the colors in this file are inside a gradient(), and stops that sit close together are the gradient. Merging near-identical colors by proximity flattens thirty-five of them into bands.
  • The moon's craters look like a mistake and are not. Several near-identical slate tones are individual craters, deliberately varied.
Shadow
  • Only two shadows are shared. The rest are one-off effects doing specific jobs, and that is not a backlog: there is no elevation system here to file them under, and inventing one is a redesign.
  • Two shadows looking alike in a list does not make them the same shadow. The two moons differ because one is 84px and the other is up to 210px; their inset shading is scaled to the moon.
Breakpoints
  • Seven widths. Put a new rule at one of them before inventing an eighth: the twelve this file used to have were twelve components each choosing privately.
  • If a boundary genuinely has to move, move its complement with it. max-width: 960px and min-width: 961px divide the range with no gap and no overlap; changing one alone leaves a band matching both rules or neither.
Before and after any change to the stylesheet
  • npm run visual:baseline, change it, then npm run visual:check. Four viewports, about two minutes.
  • Touching a breakpoint needs VISUAL_VIEWPORTS=bands on both commands. The four defaults cannot see a twenty-pixel band, so a run that moves a breakpoint comes back clean while the site changes underneath it.
  • A page height that jumps by a full line means text rewrapped; a page that grows by a few pixels per row means spacing accumulated. They read the same in a summary and are not the same thing.
  • Keep the reduced-motion block working. It is how the site settles for a screenshot, so breaking it takes the differ with it.
Things that look wrong and are load-bearing
  • padding-right: 36px on .form-group select is clearance for the chevron.
  • padding-top: 17rem clears the hero art.
  • The multi-corner radii are shapes (a teardrop, a rounded-top tab) not steps that were missed.
  • contract.html carries its own styles and loads none of this. Nothing here reaches it.

System health

How many distinct values each dimension uses. A dimension with a handful of values is a scale you can hold in your head; one with thirty is a set of decisions made a page at a time. The thresholds are arbitrary but fixed: 8 or fewer reads as a scale, 9-14 as loose, 15 or more as ad hoc. The palette is counted but not graded: how many colors a brand needs is a question with no right answer. The number worth watching is the one beside it: colors written straight into a rule instead of referenced from :root.

Color

Custom properties declared on :root that resolve to a color. These are the palette.

Contrast

Every foreground token measured against every background token, worst first, with the ratio the browser actually paints. 4.5:1 is the WCAG AA floor for body text, 3:1 for large text, 7:1 is AAA. Not every pair here is one the site uses (the ones it does use are the ones to care about) but a pair that fails is a pair nobody should reach for without knowing.

Other tokens

The rest of :root: timings and geometry the launch animation reads.

Type scale

The --text-* steps, each rendered at its own size. Whole pixels through the body range, because the stylesheet used to carry nineteen sizes between 12 and 15px and nobody can see the difference between .82rem and .83rem.

Off the scale

Sizes still written directly into a rule. Most of what is left belongs there: an em resolves against its parent rather than the root, so it is not interchangeable with a step, and the fluid clamp() sizes are responsive decisions rather than steps: they are shown live below, so they respond as you resize this window.

Spacing

The --space-* steps, drawn to scale. These are the vocabulary: a padding, margin or gap should reach for one of these rather than name a length.

Off the scale

Lengths still written directly into a padding, margin or gap instead of referencing a step. This is the working list, most-used first, though not all of it should become a step. Some of these are not spacing decisions at all: the 36px is clearance for the select chevron, the 17rem clears the hero art, and the values under 4px are optical nudges. Those belong as literals.

Corner radius

The --radius-* steps.

Off the scale

Radii still written into a rule. What is left here is shapes rather than steps: 0, inherit, and the multi-corner forms that round one side or make a teardrop. Those are not scale values and should stay where they are.

Shadow

The shadows that are shared rather than bespoke, on an identical tile.

One-off shadows

Everything else. Unlike spacing or type this is not a tail to work through: these are distinct effects doing distinct jobs: the moon's crater shading, the rocket's exhaust, focus rings, button rest and hover. Two of them looking similar in a list does not make them duplicates; the two moons differ because one is 84px and the other is up to 210px, and their inset shading is scaled to match.

Breakpoints

Every distinct @media condition. The one matching your window right now is marked.

Components

Live specimens using the real classes, not screenshots and not copies. What renders here is what renders on the site.

Buttons: .button
Default Primary Primary, pulsing Danger
Badge & text helpers Badge

A .lead paragraph: the size that opens a section.

A .page-intro paragraph, which sits under a page title.

A .section-note, the smaller print that closes a section.

Cards: .card, .card-glow, accent bars, icons

Blue accent

A card with the blue bar and icon.

Amber accent

A card with the amber bar and icon.

Purple accent

A card with the purple bar and icon.

Plan list & price: .plan-list, .plan-price

A plan $149/month

What the plan includes, in one line

  • A first item
  • A second item
  • A third item
Comparison table: .plan-table
Specimen of the plan comparison table
First $99/mo Second $199/mo
A figure row $594 $1,194
An included row Included Included
An excluded row Not included Included
Back to top: .back-to-top

Not in any page's markup: back-to-top.js builds it and appends it to <body> on the twenty pages that carry the site chrome. Fixed to the bottom-right corner, and hidden until the page has scrolled one full viewport, so a page shorter than the screen never shows one. If you have read this far, the live one is in the corner to your right; the copy below is the same button parked in place, which is the only way a fixed element can sit in a row of specimens.

Clicking it scrolls with window.scrollTo(0, 0) and then hands focus to the header, so the keyboard arrives where the eye did. The two-argument call is deliberate: it defers to the CSS scroll-behavior, which the reduce-motion block turns back to auto. Under 640px it stands down entirely while the consent banner is up, rather than stack on top of it.

Form controls: .form-group