Style guide

Style guide

Every color, size, component, and rule of the Coastal Healthcare Advocates site, shown as it ships. The specimens are the real components wherever a component can stand on its own.

Below: the booking band’s maze, solved

The system is called The Harbor Light. A harbor light doesn’t shout; it stands steady so a boat can find its way in. Navy is the ground and the voice, one Sky accent marks the way, and everything else stays pale, flat, and quiet for a reader who is stressed and often older.

This page documents what tailwind.config.js, src/input.css, and assets/logomarks/ actually ship. DESIGN.md holds the reasoning behind every rule named here. Token values and contrast ratios are written into this page by scripts/build-docs.js, so they are what the source says. If anything on this page disagrees with assets/logomarks/README.txt, the README wins: it is the source these tokens are built from. For the wider identity—voice, usage, and the mark in context—see the Coastal Brand Guidelines.

Palette

Navy, Sky, and Paper carry the site. Midnight grounds the dark theme, Coral and Cyan are small accents, and three warmer colors appear only as pale tints.

These ten hex values are the brand’s own, and everything else is built from them. The surfaces, inks, and borders a component actually uses are semantic tokens that point at these (see CSS tokens), which is how one palette serves both themes.

Core

Navy#0F1E4A
Sky#4FACFE
Paper#F5FBFF

Dark grounds

Midnight#0A0F24
Deep-tone·alt#2E3F72

Accents

Coral#FE1E1A
Cyan#00F2FE

Supporting tints

Seagrass#0BA678
Lantern#E07A00
Sand#FFC93C

What each color is for

ColorTokenRole
Navy In the mark--navyBody ink and headings, the primary button fill and the focus ring in the light theme, and the navy booking band.
Sky In the mark--skyThe one guiding accent: heading rules, line icons, the Get started tab and path-card buttons, and the footer ground in the light theme. In the dark theme it is also the link color, the focus ring, and the primary button fill.
Paper In the mark--paperText and button fill on Navy and Midnight grounds, and the shield in the Reversed mark. The light page ground is --bg, not Paper.
Midnight--midnightThe dark theme’s page ground and footer ground, and the ground the Dark mark is drawn for. Every shadow is Midnight at low alpha.
Deep-tone·alt In the markNo tokenThe alternate shield, and the line color inside the dark theme’s drawer. Darkened to #1E2B5A for contrast, it sets the second line of the footer wordmark in the light theme.
Coral In the mark--coralThe mark’s heart in Primary and Deep-tone·alt. On the site: the fourth How-it-works step, the 22-years ring in the trust stack, the ✕ markers in the Before panel, error borders and markers, and the over-limit character count. Required-field asterisks are Ink, so in a form Coral only ever means an error.
Cyan In the mark--cyanThe faint hero glow, the second line of the footer wordmark in the dark theme, and the mark’s heart in the Reversed colorway. Nothing else.
Seagrass--seagrassThe second How-it-works step, the first of every three service rows, the After panel with its ✓ markers, and the tick on the fee promise.
Lantern--lanternThe third How-it-works step, the second of every three service rows, and the Advocate and Human icons.
Sand--sandA highlighter only: the marked rows on the sample statement, and the character count as it nears the limit.

Seagrass and Lantern rotate with Sky through the color-keyed pieces—the How-it-works steps, the service rows, and the trust-stack and “What we do” icons—as 9–18% washes mixed into Surface, or as their -ink steps where they carry meaning.

Nearest Pantone

Solid Coated, for reference only. Don’t substitute these into production files, and check a physical fan deck before a print run. Paper is a substrate, not an ink, so it has no match.

ColorPantoneDifference
Navy2767 CΔE 1.2
Sky284 CΔE 3.3
Cyan3105 CΔE 6.3, the weakest match
CoralBright Red CΔE 2.0
Midnight5395 CΔE 7.1
Deep-tone·alt534 CΔE 2.1
Seagrass3278 CΔE 3.8
Lantern152 CΔE 1.6
Sand123 CΔE 1.1
  • The One Beacon RuleSky guides; it never reads. Use it for rules, icons, fills behind Navy text, and accents on dark grounds. Any text or meaningful mark on a light ground uses --link or --sky-ink instead.
  • The Supporting Tints RuleSeagrass, Lantern, Sand, and Coral appear as pale washes, step accents, and small marks. They are never large fills or text on a light ground; where one must carry meaning, use its -ink step. The one solid use is the 3.5rem Who-we-help icon tiles, which always carry a Navy icon.
  • The Color-Keyed Sections RuleServices and Who we help give each item its own accent so the eye can tell items apart at a glance. Color is never the only cue: every item keeps its icon and heading. Coral stays out of both, because in forms it means an error.

Light, dark, and contrast

The palette is fixed; what changes is which token points at what. Dark redefines 57 of the 73 custom properties, and no component knows which theme it is in.

Both panels below are the same markup. Each one takes a palette rather than being told its colors, which is what every component on the site does. The navy booking band, this guide’s bar, and its masthead keep their ground in both themes.

Light

Understanding benefits.

Body copy sits on the page ground at full strength, with secondary text one step softer.

An inline link

Primary action

Dark

Understanding benefits.

Body copy sits on the page ground at full strength, with secondary text one step softer.

An inline link

Primary action

Measured contrast

Computed from the shipped token values by scripts/build-docs.js with the WCAG 2.1 formula, and rounded down to one decimal so that no pair is ever rounded up over a threshold. Text needs 4.5:1; large text and non-text marks such as the focus ring need 3:1. Each sample is painted from the tokens themselves, in its own theme’s scope.

SamplePairRatioUsed for
Light theme
Aa InkInk on the page ground15.9:1 AAAHeadings and body copy.
Aa Ink SoftInk Soft on the page ground8.6:1 AAASecondary text, hints, table heads, field borders.
Aa Ink SoftInk Soft on Tint Wash7.7:1 AAAText inside tiles, the callout, and code chips.
Aa a linkLink on the page ground6.5:1 AAInline prose links.
Aa Sky InkSky Ink on the page ground5.5:1 AAIcons and numerals that carry meaning; the header wordmark’s second line.
Aa Seagrass InkSeagrass Ink on the page ground5.2:1 AAThe second step’s numeral, service-row icons and bullets.
Aa Lantern InkLantern Ink on the page ground5.0:1 AAThe third step’s numeral, service-row icons and bullets.
Aa Coral InkCoral Ink on the page ground6.0:1 AAThe fourth step’s numeral, the PDF icon.
Aa Book a callPaper on Navy15.4:1 AAAThe primary button, the booking band, this guide’s bar.
Aa Get startedNavy on Sky6.6:1 AAThe Get started tab, path-card buttons, the light footer.
Aa Footer#20305C on Sky5.2:1 AAFooter column titles and dimmed text in the light theme.
Aa AfterWhite on Seagrass Ink5.3:1 AAThe After panel.
580 / 600Midnight on Sand12.3:1 AAAThe character count near its limit.
612 / 600Midnight on Coral4.8:1 AAThe character count past its limit.
ringThe focus ring on the page ground15.9:1 AA, UIThe 3px focus ring on every control.
Dark theme
Aa InkInk on Midnight16.6:1 AAAHeadings and body copy.
Aa Ink SoftInk Soft on Midnight11.7:1 AAASecondary text.
Aa Ink SoftInk Soft on Surface10.5:1 AAAText inside cards and fields.
Aa a linkSky on Midnight7.8:1 AAALinks, the focus ring, and Sky Ink, which points back at Sky in dark.
Aa Book a callNavy on Sky6.6:1 AAThe primary button; the contact drawer and mobile menu, which invert to a Sky ground.
Aa Seagrass InkSeagrass Ink on Midnight8.4:1 AAAThe accent inks. Lantern points back at the raw palette; Seagrass and Coral take lighter steps.
Aa LanternLantern on Midnight6.2:1 AA
Aa Coral InkCoral Ink on Midnight6.7:1 AA
Aa AfterMidnight on Seagrass6.0:1 AAThe After panel.
Aa FooterPaper on Midnight18.1:1 AAAThe footer.

The logo lockup is held to the same bar. WCAG 2.1 exempts logotypes from contrast (1.4.3, “Incidental”), so the “Healthcare Advocates” line would be allowed to sit at the raw Sky and Deep-tone·alt values it has in the logo files. It does not. The header line is drawn in --sky-ink (5.5:1 on the light ground) or #6FC3FF (9.8:1 in dark), and the footer line in #1E2B5A (5.5:1 on the Sky footer) or Cyan (13.6:1 on the dark one). The exemption is a fallback the site does not rely on.

Pairs that are never used

Recorded so that nobody reaches for them.

SamplePairRatioInstead
Sky on the light ground2.4:1 Below AAText takes --link; a meaningful icon or numeral takes --sky-ink.
Paper on Sky2.3:1 Below AAA Sky fill always takes Navy ink. That is what --cta-ink is for in dark.
Lantern on the light ground2.9:1 Below AAThe -ink step: --lantern-ink, --seagrass-ink.
Seagrass on the light ground3.0:1 Below AA
Coral on the light ground3.8:1 Below AAError text stays Ink. The Coral border and the ▲ carry the signal.

How the switch works

PartWhat it does
DefaultNo attribute: the page follows the device through prefers-color-scheme.
Overridedata-theme="light|dark" on <html>, set by the theme buttons in the header, the footer, and this guide’s bar.
ExpiryAn override is stored with its timestamp and lasts 8 hours; then the device setting applies again.
Switchingtheme.js changes the attribute inside a View Transition, so the browser cross-fades the old page into the new one on the compositor. Without that API it falls back to the .theme-transition class, which gives every element its own color transition for 320ms—the same picture, at roughly six times the work per frame.
First painttheme.js is a render-blocking classic script in <head>, not deferred. It must run before the body renders, or a dark visitor gets a white flash.
color-schemeSet alongside the tokens, so native controls, scrollbars, and form widgets follow too.
The markSwapped by token, not by a second <img>: --brand-mark holds the SVG URL.
  • The Semantic Token RuleComponents never hard-code light or dark values. If a rule needs a color that isn’t already a token, add a token. That is the whole reason a component can be unaware of the theme.
  • Add it in three places, onceThe light value goes in :root, and the dark value in both dark blocks: the prefers-color-scheme one and the [data-theme="dark"] one. They carry identical values and must stay in lockstep; miss one and the toggle disagrees with the device.
  • Don’t use Tailwind’s dark: for colorThe variant is configured and matches the same two states, but nothing in the markup uses it. A dark: utility is a color decision hidden in a class list.
  • Re-check contrast on the dark sideThe -ink accents are darkened for the light ground and point back at brighter values in dark. On a bright accent ground in dark, put dark ink on it; that is what --cta-ink and --ba-after-ink are for.
  • Test the toggle, not just the device settingTwo code paths reach dark. The gear in this guide’s bar opens Site preferences, which is the quickest way to exercise both.

Type system

Four typefaces, each with one job. Every heading is Questrial 400, so hierarchy comes from size and space and never from weight.

Aa

Questrial

400 only. Every heading, button, nav link, table head, and tracked uppercase label.

Resolving bills 0123456789

Aa

Ysabeau

400 to 600, one variable file. Reading copy, ledes, and meta text; the site’s default face.

A calm, plain-spoken read 0123456789

$1

IBM Plex Mono

400 for prices, 500 for form errors. On this page it also sets code, which is the other thing a mono face is for.

$1,250.00

Roles

RoleTypefaceWhere it’s used
LogoTenor Sans 400The wordmark and the How-it-works numerals. Never running text.
H1Questrial 400The page or hero heading, one per page.
H2 to H4Questrial 400Section and card headings. Never bold.
LabelsQuestrial 400Buttons, nav, table heads, tracked uppercase labels.
BodyYsabeau 400–600Reading copy, ledes, meta text.
FiguresIBM Plex Mono 400Prices only, through .num and .price-fee. Phone numbers stay in Ysabeau: Mono’s full-width space and brackets open visible gaps in them.
Form errorsIBM Plex Mono 500Inline field errors and the error summary, through .field-error and .error-summary. Plex Mono ships 400 and 500 only, so never bolder.
  • The Level Voice RuleEvery heading is Questrial 400. Hierarchy comes from size and space, never from bold weights. Questrial ships one weight, so never request faux bold.
  • The Figures-in-Mono RuleIBM Plex Mono is reserved for real figures and form validation errors. It is never a stylistic costume for labels or headings, which is why this guide’s own annotations are set in Questrial.

Type scale

Roles are named for the job and not the value. The heading sizes are fluid between a phone and a desktop value, and each step is at least 1.25 times the one below it.

The five heading and lede sizes are tokens in tailwind.config.js (fontSize); the smaller roles are set where they are used. Every sample below is set live in its own face and size.

Displaytext-h1
36px to 58px · leading 1.08
the hero, and two display headings

Understanding benefits.

Headlinetext-h2
30px to 40px · leading 1.18
section headings, over the Sky rule

Resolving bills.

Titletext-h3
23px to 30px · leading 1.25
path cards, success messages, legal sections

Advocating for you.

Title smalltext-h4
23px · leading 1.3
services, steps, FAQ questions

Medical Bill & EOB Review

Lede.lede
21px · leading 1.45 · 60 characters

A calm, plain-spoken read for intros: the size that carries a section’s opening sentence.

Body18px · leading 1.6 · 62 characters
68 for legal pages and FAQ answers

Running text sits at 18px, a size chosen for older readers, per the brief’s accessibility requirement, and stops at a 62-character measure.

Row title17px · Questrial
pricing rows, the hero phone link

Insurance Claim Appeal

Body small.meta
15px · leading 1.5

The tagline, bylines, captions, and supporting meta.

Caption14px · Ysabeau
the smallest running text

Field hints, the privacy note, and price units.

Label15px · Questrial · 0.06em · uppercase
button text

Book a free call

Label small13px · 0.08em · uppercase
column heads, footer column titles

Where it’s used

Overline12px · 0.1 to 0.14em · uppercase
one to three words only

Hourly services

Figure.num
IBM Plex Mono 400, tabular

$1,250.00

Error.field-error
14px · IBM Plex Mono 500

Enter your name.

Numeral.step-num
44px · Tenor Sans · 0.02em

1 2 3 4

  • The Readable-First RuleBody text never drops below 18px, and supporting text never below 14px. Headings balance their line breaks and paragraphs wrap to avoid orphans. Older readers are a core audience, so legibility outranks density.
  • Measure governs width; the scale only governs heightProse stops at 62 characters, legal pages and FAQ answers at 68, and ledes at 60.

The mark

The Advocate Beacon: a lighthouse inside a shield. Negative-space stripes wind up the tower so the shield color shows through them, and there is no separate stripe ink.

Five colorways, each tied to a specific ground. The tiles below are fixed to their documented ground, so they stay correct in either theme.

  • Advocate Beacon mark, Primary colorway

    Primary
    Sky shield · Coral heart

  • Advocate Beacon mark, Dark colorway

    Dark
    Light Sky shield · near-white heart

  • Advocate Beacon mark, One-color

    One-color
    Navy only: letterhead, stamps

  • Advocate Beacon mark, Reversed colorway

    Reversed
    Paper shield · Cyan heart

  • Advocate Beacon mark, Deep-tone·alt colorway

    Deep-tone·alt
    Muted navy shield · Coral heart

Size and clear space

MeasureValue
Minimum size20px for the full mark; 28px and up recommended.
Clear space12 units, 10% of the mark’s width, on all sides.
Below about 28pxThe stripes close up. Use a favicon variant, never the full mark.

Where it appears

WhereHeightColorway
Site header lockup3.25rem on phones, 5.46875rem from 480px, 6.25rem from 1024px (--logo-h)Primary; Dark in the dark theme, through --brand-mark
Site footer lockup5remDeep-tone·alt on the Sky footer; Reversed on the Midnight one
Message-sent panel3.5remPrimary or Dark, with the light drawn behind it
This guide’s bar2.5remReversed, on Navy

Favicons are a different drawing

A favicon is not a scaled-down full mark. The tower fills solid Navy with no stripes, and the lighthouse is scaled 1.625 times. It is correct for 16 to 48px only; use the full mark above that, and never regenerate a favicon from it.

Favicon mark, primary Favicon mark, dark ground

The two favicon marks at 32px: primary, and the one drawn for a dark ground

Beside it

The Greater National Advocates badge is required in the site footer. It ships as assets/img/gna-logo-navy.svg and gna-logo-white.svg, and a full-color lockup replaces the white badge on paper.

Logo lockups

The mark with the wordmark, in four forms. All type is outlined, so there is no font dependency at a printer.

Shown here in Primary. Each form also ships in Dark, One-color, Reversed, and Deep-tone·alt: 20 files in all, in assets/logomarks/logos/, each as SVG plus 512, 1024, and 2048px PNG.

Coastal Healthcare Advocates, horizontal lockup

Horizontal
the default

Coastal Healthcare Advocates, stacked lockup

Stacked
for a square space

Coastal Healthcare Advocates wordmark, no mark

Wordmark
no mark

Coastal logotype, wordmark alone

Logotype
“Coastal” alone

FormMinimum widthUse
Horizontal180pxThe default, wherever there is room.
Stacked120pxSquare spaces.
Wordmark140pxWhere the mark already appears nearby.
Logotype90pxApp headers and tight spaces.

Read the full asset README

Grid and measure

One centered reading column, 1000px at most, with a nine-column grid inside it and a 24px gutter.

Two layouts use the grid for a real column relationship: the hero and Lindsey’s bio. Everything else is a plain two- or three-up grid, because most of this site is one column of prose and a row of cards. Every one of them is drawn out under Layouts.

The grid — 9 × 90px, 24px gutters

1
2
3
4
5
6
7
8
9

Span 9 — 1000px · hero, full-bleed section

full width

Span 6 + 3 — 659 / 317px · article beside a rail

main
rail

Span 3 × 3 — 317px each · card rows

card
card
card

Span 5 — 545px · running text, capped at 62ch

body copy
white space

Those figures are the nominal grid, measured across the full 1000px. .canvas spends 2rem of that on side padding, so at full width a column actually measures about 83px and a six-column span about 616px. The ratios are what matter; the pixel values are there to size a comp.

Nine divides cleanly into thirds and keeps the page out of the twelve-column default. Two layouts set their own tracks on top of the nine: the hero puts four columns of text beside a five-column illustration, and About swaps the tracks for an 18–22rem portrait rail. Below 1024px .grid-9 becomes a single column and drops its children’s spans outright (grid-column: auto !important), so a span only ever describes the desktop layout.

The canvas

Every section is a .section for its vertical rhythm with a .canvas inside it for its width. Nothing sets its own max-width. This guide and the two code references are the exception: they are reference pages, and use the shell described at the top of this file.

ClassWhat it sets
.canvasThe reading column: 1000px, centered, 1.5rem of side padding (2rem from 480px up). Wraps the content of every section.
.canvas-wide1180px, same padding. The site header and footer only, so they breathe a little wider than the reading column.
.sectionVertical rhythm: padding-block: clamp(2.25rem, 1.5rem + 3.25vw, 4rem). Block padding only, so it can share an element with .canvas.
.section--tightThe same, one step down (2rem to 3rem). For a closing or secondary band.

Breakpoints

NameWidthWhat changes
sm480pxCanvas padding goes from 1.5rem to 2rem.
md768pxWhere most two-up card grids open.
nav930pxThe hamburger gives way to the desktop links. Measured, not guessed: re-measure if the nav, logo, or icons change.
lg1024px.grid-9 takes its nine tracks and column spans start applying. This guide’s contents rail moves beside the reading column.
xl1200pxAvailable; the canvas has already capped at 1000px, so little uses it.

The hero’s illustration keeps a 38% track between 500 and 1023px and bleeds off the right edge, and is hidden altogether below 500px. The About rail turns sticky at 700px and, between 450 and 699px, drops out of grid layout entirely so the portrait can float right and the bio wraps around it. Both are documented where they live, at the foot of the utilities layer in src/input.css.

Measures

TokenWidthFor
measure62chRunning copy.
prose68chLegal pages and FAQ answers, where a longer line costs less than more scrolling. This guide’s prose, too.
.lede60chA section’s opening sentence.
  • The One Column of Attention RuleContent never spreads wider than the 1000px reading column, and no decision point shows more than a handful of equal options. The persistent Get started tab is the only floating element, and it tucks away once the intake form is on screen.
  • Start with the canvas, not the gridA section is <section class="section"><div class="canvas">. Most of this site is one column, and one column is the right answer more often than it looks.
  • Reach for .grid-9 only for a genuine column relationshipThere are two on the site: the hero and the About rail. For a row of cards use Tailwind directly, grid gap-gutter md:grid-cols-2, which is what every other section does.
  • Prefix every span with lg:Below 1024px the grid is one column and unprefixed spans are overridden anyway, so an unprefixed span is at best noise and at worst a wrong guess about small screens.
  • Use the gutter tokengap-gutter in markup, theme('gap.gutter') in CSS, never a literal 24px.
  • Give a fixed rail a fixed trackWhen a column has an intrinsic width, a share of nine strands a gap beside it. About overrides the tracks with 22rem minmax(0, 1fr), because the rail only ever needs room for the portrait.
  • Write plain width queries@media (min-width: 48rem) is enough. A PostCSS step rewrites it so the Larger and Largest text sizes keep the phone layout at any width. A rule that must survive that rewrite belongs in a container query instead.
  • Don’t let meaning ride on column orderReader mode forces every grid to one column and clears each child’s placement, so the DOM order is the order a reader may get. Check that the section still reads top to bottom.

Layouts

A wireframe of every layout the site uses. Each frame draws the tracks and the reading order and leaves the content out, so it stays true as the copy changes.

The wide frame is the desktop layout and the narrow one beside it is the phone. A dashed outline around a whole group is the .canvas: a width limit rather than anything you can see on the page.

Key

  • block A block of content
  • card A bordered card or panel
  • photo Photography
  • CTA The primary call to action
  • accent A block carrying an accent color
  • space Space the layout leaves empty, or a block absent at that width

The page shell

Section band

.section > .canvas — every page

Any width

Phone

Site header

.site-header > .canvas-wide — src/partials/header.html

Desktop — 930px and up

Phone — menu open

Site footer

.site-footer > .canvas-wide — src/partials/footer.html

Desktop — 1024px and up

Phone

The homepage, in order

Hero

#top .grid-9 — index.html

Desktop — 1024px and up

Phone — under 500px

Two entry paths

.path-card — index.html

480px and up

Phone

Prose with a floated vignette

.what-intro + .vignette-figure + .value-list — index.html #what

Desktop — 1024px and up

Phone

Under 400px

Service rows

.service-rows / .service-row — index.html #services

768px and up

Phone

Who we help

.who-list / .who-item — index.html #who

1024px and up

Phone

About Lindsey—the portrait rail

#about .grid-9 + .about-rail — index.html

700px and up

450–699px

Under 450px

Before and after panels

.ba-grid / .ba-panel — index.html #what

640px and up

Phone

How it works—four steps

.how-steps / .how-step — index.html #how

768px and up

Phone

The marked statement

.fineprint / .fineprint-body — index.html #review

820px and up

Phone

Pricing choices

.price-choices / .price-choice — index.html #pricing

640px and up

Phone

FAQs

.faq-layout / .faq-aside / .faq-list — index.html #faqs

900px and up

Phone

Intake form

#intake-form — index.html #intake

Any width

Phone

Closing band

#schedule.schedule-band — index.html

Any width

Phone

Overlays and secondary pages

Contact drawer

#contact-modal + .drawer-tab — modal.js

Closed — the tab

Open — desktop

Document page

.legal-prose + .legal-toc — the legal pages, pricing, 404, this page

Any width

Phone

Card row

grid gap-gutter sm:grid-cols-2 lg:grid-cols-4 — for-professionals.html

1024px and up

Phone

Label-and-value rows

.dl-rows — for-professionals.html; the credits list on about-this-site.html

768px and up

Phone

  • Reader mode redraws all of themSite preferences, then Reader mode, narrows every canvas to 44rem and forces .grid, .grid-9, and the named layouts above to one column, clearing each child’s placement. Every diagram here becomes its phone frame, in DOM order, which is why none of these layouts may carry meaning in its column order.
  • So do the Larger and Largest text sizesAt those two sizes a PostCSS step rewrites every width query, so the page keeps its phone layout and its hamburger at any window width. The narrow frame is what a desktop window shows at those settings.

Buttons and controls

Large, calm targets that say what happens next. An uppercase Questrial label, 12px corners, 48px tall at least, and a hair of lift on hover.

These are the real .btn classes from src/input.css. A button is a link when it goes somewhere and a <button> when it does something; both take the same classes. Every press plays one soft click (see Motion).

On the page ground: .btn-primary, .btn-ghost, and the focus ring, Navy in light and Sky in dark

On Navy, the booking band’s actions: .btn-on-dark, .btn-ghost-on-dark, and the ring in Sky

ButtonFill and inkOn hoverWhere
PrimaryNavy with Paper. In dark, Sky with Navy (--cta-bg, --cta-ink).Deepens to Midnight; in dark, brightens by 8%.The one main action in a view.
GhostTransparent, 1px Ink border, Ink label.Fills with Tint Wash.The second action beside a primary.
BeaconSky with Navy, in both themes. It is .btn-ghost inside a .path-card.Deepens to Navy with Paper; in dark, lifts to Paper with Navy.The two entry-path cards. Labels wrap inside half-width cards.
On darkPaper with Navy.Brightens by 5%.The booking band’s main action.
Ghost on darkTransparent, Paper border at 55%, Paper label.A 14% Paper fill and a full-strength border.The booking band’s call button.

Every button shares one shape: min-height: 48px, 0.75rem by 1.4rem of padding, a 15px label tracked 0.06em. Hover lifts it 1px and a press dips it 1px, over 0.18s. A label stays on one line wherever it fits and wraps inside the button on a narrow phone or at the larger text sizes.

Icon controls

Round is reserved for controls that are only an icon. Each is at least 44px across and carries its name in aria-label and title.

.header-icon-btn at 44px, its focus ring, and .link-quiet, whose arrow nudges in its direction of travel

ControlSizeNotes
Header call and menu buttons44pxThe call button shows the number as text from 1024px, so the phone number is always one tap away.
Theme and preferences buttons36px in the site header, the one exception to the 44px floor; 44px in this guide’s barThe sun slides out and the moon slides in; the gear turns 60° on hover.
Dialog close44pxPinned to the dialog, not the scroller, so it stays put.
Get started tabThe right edge; a full-width bar under 480pxSky with Navy vertical text. It hides while the intake form is on screen.
  • Big enough to hitButtons are at least 48px tall and icon buttons at least 44px, and every control gets the 3px focus ring: Navy on light grounds, Sky on dark ones. Put .on-dark on a navy ground and the ring follows.
  • One primary action in a viewA primary button never competes with a second one. The floating Get started tab is the only persistent control, and there is never a second floating one.
  • Say what happens nextA label names the result: “Book a free call”, not “Submit”. Sentence case in the source; the CSS sets the capitals, so a screen reader reads words and not letters.

Components

Surfaces are pale and mostly flat. Structure comes from hairline borders and tonal washes, and a soft neutral lift is kept for photographs, pinned cards, and floating layers.

Surfaces

Three containers cover almost everything. Headings inside them drop their tracking.

Card

Surface, a 1px Line border, 12px corners, and the Rest shadow.

Tile

Tint Wash, 10px corners. On hover the border turns Sky and it lifts 3px.

Callout

Tint Wash, 12px corners, no shadow, and no hover.

.card, .tile, and .callout, live. Point at the tile.

Depth

Rest
Lift
Floating layer
Edge tab
DepthShadowOn
Restshadow-cardCards at rest.
Liftshadow-liftFramed photographs, the How-it-works step cards, and tile hover.
Header scrolled0 4px 16px rgba(10, 15, 36, 0.07)Added to the site header once the page leaves the top.
Floating layer0 18px 44px -14px rgba(10, 15, 36, 0.32)The mobile menu panel.
Edge tab-2px 2px 16px rgba(10, 15, 36, 0.22)The Get started tab.

In the dark theme shadows fade into the ground and the borders carry the separation.

Shape

12px
10px
8px
Round
RadiusTokenOn
12pxrounded-cardCards, buttons, photo frames, and the callout.
10pxrounded-tileTiles.
8pxLiteralForm fields and notes.
22px and 15pxLiteralThe step cards: a Surface frame around a tinted inner panel.
Round999pxTrue circles only: the header buttons, the dialog close, social links, byline portraits, and the theme switch.

Borders are 1px hairlines. Only an error state uses 2px, in Coral.

Icons

A hand-authored inline SVG sprite on a 24px grid: 1.75px strokes (2px for small UI glyphs such as plus, close, and check), round caps and joins, drawn in currentColor. The homepage carries the full sprite; the secondary pages carry the header and footer’s share, written in at build time from src/partials/icons.html, and this page carries its own few. Social marks are the only filled icons. The brief first asked for hand-drawn doodle icons; that was tried and dropped, so don’t reintroduce it.

Signature pieces

These live in index.html and can’t stand alone on a second page, so they are described here and drawn under Layouts. DESIGN.md has each one in full.

PieceWhat it is
Entry-path cardsTwo equal cards, “I need help right now.” and “I’m helping someone else.”, each a calm photograph under a heavy Surface wash with a Beacon button pinned to the bottom.
How-it-works step cardsFour pinned note cards on a faintly ruled ground, joined by a dashed Sky thread. From 768px they alternate sides, tilted 2.5°, and straighten on hover. The steps take Sky, Seagrass, Lantern, and Coral in order.
Fine print, readA sample itemized statement beside three numbered notes. Three lines are flagged with a Sand highlighter wash, a numbered pin, and one pen mark. The copy says the patient, codes, and amounts are made up, and it never shows savings.
Maze bandTwo generated mazes, walls only. One sits still under the hero; the other runs along the booking band with its route solved in Sky. This guide’s masthead is that band.
Message sentWhen a contact form sends, the mark appears with a soft Sky glow blooming behind it and one slow beam sweeping round. Once per message, and it never delays the confirmation text.
Get started tabThe persistent call to action: a Sky tab on the right edge, a full-width bar under 480px.
  • The Soft Lift RuleSurfaces are flat at rest unless they are a photograph, a pinned card, or a floating layer. Everything else gains elevation only as feedback. Lifts are neutral and soft, never colored glows.
  • The Earned Light RuleThe beacon’s light is reserved for the moment someone reaches out. Don’t reuse it as page decoration, a loading spinner, or a hover effect, or it stops meaning “you’ve been heard”.
  • Not an insurer’s interfaceNo dashboards, hero-metric stat tiles, countdown or scarcity urgency, or aggressive conversion chrome. The design earns trust by being calm, legible, and honest.

Form elements

There is exactly one authored form on this site, #intake-form in index.html. The dialog clones it, and Netlify detects it at build time. Author a second one and you break both.

The states

Live controls, not screenshots, so tab through them. These are demonstration fields and go nowhere.

Please don't enter Social Security numbers, diagnoses, or other sensitive health details here.

Required field

Enter a valid email address.

Enter either an email address or a phone number.

Preferred contact method

Anatomy

Every control is one .field block: a label, the control, an optional hint, and an error slot that starts hidden.

ClassWhat it is
.fieldThe wrapper: a <p>, or a <fieldset> for a group.
.field-labelQuestrial 15px. On a group this is the <legend>.
.input .textarea .selectFull width, 48px minimum height, 8px corners, a 1px Ink Soft border. On focus the border darkens to Ink under the global ring.
.field-hintCaption-size help, referenced by the control’s aria-describedby.
.field-errorHidden until it has text. Plex Mono 500 with a Coral ▲ before it.
.reqThe required asterisk. Ink, not Coral, so Coral only ever means an error.
.choiceA checkbox or radio beside its label. An empty checkbox is drawn like a field (Surface, Ink Soft border, Coral when it is a missed requirement); ticked, it is the browser’s own, tinted with --choice-accent.
.form-noteThe tinted notice above the fields. This is where the health-details warning lives.
.error-summaryA Coral-bordered list of every error, above the fields, linking to each one.
.hp-fieldThe honeypot. Visually hidden, never display: none, or a bot sees that it is inert.
  • Don’t build a second formAdd fields to #intake-form instead. It is the only form Netlify knows about, the only one that works without JavaScript, and the one the dialog clones.
  • Give every field an id prefixThe inline form uses in- and the dialog clone cf-; modal.js re-keys them so both copies can share a document. A validated field X needs an error element X-err. That is the whole contract.
  • Label with for, help with aria-describedbyPlaceholders are not labels, and a hint the control doesn’t point at is invisible to a screen reader.
  • Set autocompletename, email, tel: this is the difference between a one-tap form and a chore on a phone.
  • Mark errors three waysaria-invalid="true" on the control, the error id joined onto its aria-describedby, and the message in X-err. On a failed submit the summary takes focus, which is how it gets announced; there is no role="alert".
  • Keep error text in InkThe Coral border and the ▲ carry the signal. Coral body text at caption size would not clear AA.
  • Never ask for health detailsThe form collects a name, a way to reply, and a short description. The note above the fields says so, and it rides along into the dialog clone.
  • Leave the spam layers aloneThe honeypot company field, the two-second time-trap, and the Turnstile check are load-bearing; contact-form.js drops anything that trips the first two, and holds a send until the third has a token.

Overlays and dialogs

Three overlays. Each one takes the keyboard when it opens and gives it back exactly where it took it.

OverlayShapeOpened byAlso
Contact dialogA drawer anchored to the right edge, min(30rem, 100vw) wide, or a centered lightbox. Both are #contact-modal with the same cloned form inside.Anything with data-open-contact, and the Get started tab.Closing with text in the form asks first. In dark it inverts to a Sky ground.
Site preferencesA native modal <dialog>, built by prefs.js.The gear: data-open-prefs.Theme, text size, animations, reader mode, analytics. Every control applies at once, so there is no Save button.
Mobile menuA compact 15px-rounded panel under the menu button, scaling in from the top right.The 44px menu button, below 930px.Carries its own theme switch and preferences row.

Try it

These buttons open the real contact dialog. This page has no form of its own, so modal.js fetches index.html and borrows it, exactly as any secondary page does. The gear in the bar above opens Site preferences.

Why the contact dialog isn’t showModal()

A dialog opened with showModal() goes into the browser’s top layer. When the form used reCAPTCHA, the dialog’s ::backdrop painted over the challenge overlay that reCAPTCHA appended to <body>, and the visitor got an invisible puzzle. The form now uses Cloudflare Turnstile, whose challenge stays inside the widget, so that constraint is gone; the hand-built dialog stays because it works and is tested. It opens with dialog.show(), and everything the top layer would have given for free is built by hand: a real #contact-backdrop element that also handles a click outside, inert on the rest of <body>, Escape, a focus trap, a scroll lock, and focus returned to the button that opened it. Site preferences has no such history and uses the native modal.

Hooks

HookWhat it does
data-open-contactOn any element. Opens the dialog; this is the whole integration.
data-variant="lightbox"Centers it instead of anchoring it to the edge. Adds .is-lightbox.
data-contact-titleThe dialog heading. Defaults to the trigger’s own text.
data-contextPassed through to the form’s hidden context field, so a submission says which entry point it came from.
data-close-contactOn anything inside the dialog. Closes it.
data-calendly-toggleThe dialog’s “Book time with us” button. Opens the inline scheduler in #cm-booking; closing the dialog removes the frame.
data-calendly-offer data-calendly-afterFor a visitor who has already booked (data-booked on <html>, set by calendly.js from a 30-day localStorage record). An offer to book is hidden, as every scheduler mount and toggle is; data-calendly-after copy, authored hidden, stands in. The footer’s Calendly icon stays, and following it brings the calendar back.
contact:open contact:closeDispatched on document if you need to react.

Parts

PartWhat it is
#contact-modalThe <dialog>. An edge-anchored drawer; .is-lightbox centers it.
#contact-backdropThe dim. A sibling element, not ::backdrop.
.modal-scrollThe scrolling body, 100dvh so mobile browser chrome doesn’t clip it.
.modal-closePinned to the dialog rather than the scroller, so it stays put.
.modal-confirmThe “discard your message?” cover, shown when closing with text in the form.
.is-closingAdded on close; the dialog only really closes once the out-animation ends.
  • Load the module<script type="module" src="assets/js/modal.js">. On a page with no #intake-form it fetches the homepage and borrows the form, the success panel, the dialog, and the backdrop. The page’s CSP <meta> must allow Turnstile and Calendly, or the borrowed dialog’s bot check is silently blocked.
  • Point the trigger somewhere real firstSecondary pages link to /index.html#intake and add data-open-contact on top. If the fetch fails or JavaScript never runs, the link still works.
  • Pick the variant by the weight of the askThe drawer is for a call to action in the flow of a page. The lightbox is for the hero’s primary action, where the page should feel paused.
  • Don’t style the dialog per pageBoth variants are already in src/input.css; a page-local override will drift from the other one.
  • Let the animation finishClosing is .is-closing, then close() on animationend. Calling close() directly cuts the drawer off mid-slide.
  • Check it with Animations offReduced motion swaps the slide for a fade; everything else must still work.

Motion

Gentle and continuous. Things rise in, draw on, or march slowly; each plays once as it comes into view, and then stays put.

ease-calm      cubic-bezier(0.22, 1, 0.36, 1)    buttons, the Get started tab
--reveal-ease  cubic-bezier(0.16, 1, 0.3, 1)     everything that comes into the light
What movesHowWith reduced motion, or Animations off
Hero entranceThe glow settles over 1.2s while each line rises 32px over 0.64s, 90ms apart. Once per session.Already in place.
Reveal (data-reveal)An 18px rise and fade, 0.72s, as the element crosses into view.Nothing is armed, so nothing is hidden.
Section headingsClear from a 5px blur over 0.82s; then the Sky rule sweeps out from its start over 1s.Already in place.
Lists and table rowsChildren rise in turn, 75ms apart (rows 55ms); line icons settle a beat later and ticks draw left to right.Already in place.
The two entry pathsDrift in 28px from their own sides. In the Before and After pair, After slides out from under Before.Already in place.
PhotographsFade in over 0.9s while easing back from a 6% zoom over 1.4s.Already in place.
How-it-works threadDraws itself once, then marches slowly while on screen and pauses off it.Drawn and still.
Sample statementThe document rises; then each highlight swipes in and its pen mark draws, one note at a time.Already marked.
Booking-band mazeAs the footer comes into view the ring appears, the route draws over 4.2s, and the dot lands. Once per page view.Shown without the draw.
Message sentA Sky glow blooms behind the mark and one slow beam sweeps round. Once per message.The glow fades in.
Get started tabThe first time it comes out it leans past its resting place and settles back, 0.9s. Later appearances slide.Appears.
Drawer and lightboxSlide or scale in; the close plays out before the dialog really closes.A fade.
Theme switchLight and dark blend like dusk over 280ms, as one View Transition across the whole page.An instant swap.
Buttons, tiles, linksA 1px lift or a 3px lift, color swaps, and the quiet link’s arrow nudge, 0.18 to 0.2s.Color and opacity only, 0.12s.

Write a plain prefers-reduced-motion query. A PostCSS step (scripts/postcss/motion-pref.js) makes every such block also answer the Animations off switch in Site preferences (:root[data-motion="off"]). JavaScript that animates checks prefersReducedMotion in main.js, which covers both.

Sound

Pressing any button plays one soft, synthesised click: a short wooden tick over a falling sine, a few percent different each time, at low volume. It is the only sound on the site. It never plays on hover, scroll, or page load, and it is silent with reduced motion, with Animations off, or when localStorage.sound is off. There is no audio file; sound.js builds it with Web Audio.

  • The Calm Motion RuleMotion moves one way and never demands attention: things rise in, draw on, or march slowly, and loops pause when off screen. Nothing flashes, bounces, or blocks reading, and reduced motion keeps only color and opacity changes.
  • Nothing waits on an animationContent is fully visible without JavaScript, and a failed script hides nothing. Start states begin at an opacity of 0.01, not 0, so the browser still counts the hero as painted.
  • Only the theme switch starts a view transitionIts two rules in src/input.css name ::view-transition-*(root) and carry no class, because a page has one root transition at a time. A second feature that called startViewTransition would inherit the theme’s 280ms cross-fade, so give that one its own view-transition-name rather than retiming these.

CSS tokens

Semantic custom properties on :root in src/input.css, swapped by data-theme or the visitor’s device setting. This is what the theme buttons actually switch.

The ledger below is the working set, and scripts/build-docs.js writes its values in from the source, exactly as authored: a value that reads var(--sky) points at the palette. The CSS reference lists all 73 custom properties.

TokenLightDarkWhat it is for
Color
--bg#FEFEFE#0A0F24The page ground.
--surface#FDFEFF#101A3ACards, form fields, tables, and alternating section bands.
--ink#0F1E4A#EAF0FFHeadings and body copy.
--ink-soft#3C4A73#BCCCF0Secondary text, table heads, hints, and form-field borders.
--line#DDE4F2#25315CHairline borders and dividers.
--line-soft#EAEFF9#1B274ATable rows and quieter section edges.
--tag-bg#EAF3FF#16234ATint Wash: tiles, the callout, pricing group rows, ghost-button hover, and this guide’s code chips.
--warn-bg --warn-line --warn-ink#EDF4FE #C3D8F2 #2F4468#241C0C #4A3A14 #E8C878The draft and to-do flags on the legal pages.
--link#0B5FA5var(--sky)Inline prose links.
--sky-ink#1B6AB4var(--sky)An accent as ink: the darker step for when an accent carries text or meaning on the light ground.
--seagrass-ink#087A58#1FC493
--lantern-ink#A85A00var(--lantern)
--coral-ink#C4140F#FF6A66
--cta-bg --cta-inkvar(--navy) var(--paper)var(--sky) var(--navy)The primary button.
--accent-hover-bg --accent-hover-inkvar(--navy) var(--paper)var(--paper) var(--navy)Hover for the controls that rest on Sky: path buttons and the Get started tab.
--ba-after-bg --ba-after-inkvar(--seagrass-ink) #FFFFFFvar(--seagrass) var(--midnight)The After panel.
--ring --focus-on-darkvar(--navy) var(--sky)var(--sky) var(--sky)The focus ring, and the ring under .on-dark.
--footer-bg --footer-inkvar(--sky) var(--navy)var(--midnight) var(--paper)The site footer. Seven more --footer-* tokens set its dimmed text, links, and switch.
--panel-*The page’s own tokensA Sky ground with Navy inkThe contact drawer and the mobile menu, which invert in dark.
--brand-mark --brand-foot-markPrimary, Deep-tone·altDark, ReversedThe mark’s SVG URL in the header and footer lockups.
Type
font-logoTenor Sans 400The wordmark and the How-it-works numerals.
font-labelQuestrial 400Every heading, button, nav item, and tracked uppercase label. Questrial ships 400 only, so these are never bold.
font-bodyYsabeau 400–600Reading copy, ledes, and meta text. One variable file covers the range.
font-monoIBM Plex Mono 400 and 500Prices and inline form errors only.
--font-sansThe font-label stackFor rules that need the family from a var() rather than theme(). Resolved from tailwind.config.js, so the stack has one source.
text-h1 to text-h4, text-ledeSee Type scaleThe five sizes that are tokens.
Layout
max-w-canvas1000pxThe reading column. The header and footer use 1180px.
max-w-measure max-w-prose62ch, 68chLine-length limits.
gap-gutter24pxThe grid gap.
rounded-card rounded-tile12px, 10pxCorners. Fields are a literal 8px.
shadow-card shadow-liftSee ComponentsNeutral near-black elevation only; no chromatic or navy glow.
ease-calmcubic-bezier(0.22, 1, 0.36, 1)The easing for buttons and the Get started tab.
--logo-h --header-h3.25rem, then 5.46875rem from 480px and 6.25rem from 1024pxThe header logo’s height, and the sticky header’s. Scroll padding and every sticky offset derive from --header-h.

Set from script

A few attributes and properties carry a visitor’s choice or a measurement, never a design decision.

NameSet byMeaning
data-themetheme.jsA stored light or dark override.
data-texttheme.jsText size: lg, xl, or xxl. It scales the root by 1.125, 1.25, or 1.5 through --text-scale.
data-motiontheme.jsoff when the Animations switch is off.
data-readertheme.json in reader mode.
--i --reveal-iInline; main.jsAn element’s place in an entrance order.

Code reference

The source comments are the documentation. Every build regenerates the reference pages from them, so they always match what ships.

PageMade from
JavaScript referenceThe JSDoc in src/js/. Each script has a page covering how it loads, what it does, its functions, and its annotated source.
This page’s valuesCustom properties from src/input.css and theme values from tailwind.config.js, written into any element marked data-token, data-tw, or data-count. Each data-contrast pair is resolved to its two colors in its theme and measured. The comment at the top of this file has the details.
CSS referenceThe comments in src/input.css plus the tailwind.config.js theme: every custom property in light and dark, each rule with its note, and the annotated source.
Logomarks READMEWritten by hand: the authoritative brand spec for the mark, the lockups, and the palette.
DESIGN.mdWritten by hand, in the repository: the reasoning behind every rule on this page.
ARCHITECTURE.mdWritten by hand, in the repository: how the site is connected end to end, from DNS to the form’s database.

After changing a token, a theme value, or this page, run the builder and commit what it writes. To change what a reference page says, edit the source comments. In src/input.css, a /* ==== */ banner opens a section, /* --- Title: text --- */ opens a sub-section, and the comment directly above a rule becomes its note.

npm run build:docs    # writes site/docs/js and site/docs/css, and updates this page
npm run check:docs    # writes nothing; exits 1 if this page is out of date
npm run build         # the full build; Netlify runs this
  • The Three Layers RuleContent lives in the HTML, presentation in the CSS, and behavior in the JavaScript. Every word a visitor can reach is written once, in the page, and structured data marks up those words instead of repeating them. Script shows, hides, and moves what’s already there. It never writes copy or markup, and never sets a style except to hand CSS a number only it can measure.
  • State is an attributeScript sets open, hidden, aria-expanded, or a data-* value, and CSS decides what each state looks like. Where the words change with the state, write both versions and show one, as the booking band’s data-calendly-after copy does.
  • Native before scriptedUse the element that already does the job before writing script that imitates it. The FAQ rows are <details> elements with microdata on each question, and no script at all.

An internal page: not in the site’s navigation and marked noindex. Values on it are written by scripts/build-docs.js. Return to the homepage.