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
#0F1E4A#4FACFE#F5FBFFDark grounds
#0A0F24#2E3F72Accents
#FE1E1A#00F2FESupporting tints
#0BA678#E07A00#FFC93CWhat each color is for
| Color | Token | Role |
|---|---|---|
| Navy In the mark | --navy | Body ink and headings, the primary button fill and the focus ring in the light theme, and the navy booking band. |
| Sky In the mark | --sky | The 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 | --paper | Text and button fill on Navy and Midnight grounds, and the shield in the Reversed mark. The light page ground is --bg, not Paper. |
| Midnight | --midnight | The 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 mark | No token | The 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 | --coral | The 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 | --cyan | The 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 | --seagrass | The 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 | --lantern | The third How-it-works step, the second of every three service rows, and the Advocate and Human icons. |
| Sand | --sand | A 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.
| Color | Pantone | Difference |
|---|---|---|
| Navy | 2767 C | ΔE 1.2 |
| Sky | 284 C | ΔE 3.3 |
| Cyan | 3105 C | ΔE 6.3, the weakest match |
| Coral | Bright Red C | ΔE 2.0 |
| Midnight | 5395 C | ΔE 7.1 |
| Deep-tone·alt | 534 C | ΔE 2.1 |
| Seagrass | 3278 C | ΔE 3.8 |
| Lantern | 152 C | ΔE 1.6 |
| Sand | 123 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
--linkor--sky-inkinstead. - 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
-inkstep. 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.
Primary action
Dark
Understanding benefits.
Body copy sits on the page ground at full strength, with secondary text one step softer.
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.
| Sample | Pair | Ratio | Used for |
|---|---|---|---|
| Light theme | |||
| Aa Ink | Ink on the page ground | 15.9:1 AAA | Headings and body copy. |
| Aa Ink Soft | Ink Soft on the page ground | 8.6:1 AAA | Secondary text, hints, table heads, field borders. |
| Aa Ink Soft | Ink Soft on Tint Wash | 7.7:1 AAA | Text inside tiles, the callout, and code chips. |
| Aa a link | Link on the page ground | 6.5:1 AA | Inline prose links. |
| Aa Sky Ink | Sky Ink on the page ground | 5.5:1 AA | Icons and numerals that carry meaning; the header wordmark’s second line. |
| Aa Seagrass Ink | Seagrass Ink on the page ground | 5.2:1 AA | The second step’s numeral, service-row icons and bullets. |
| Aa Lantern Ink | Lantern Ink on the page ground | 5.0:1 AA | The third step’s numeral, service-row icons and bullets. |
| Aa Coral Ink | Coral Ink on the page ground | 6.0:1 AA | The fourth step’s numeral, the PDF icon. |
| Aa Book a call | Paper on Navy | 15.4:1 AAA | The primary button, the booking band, this guide’s bar. |
| Aa Get started | Navy on Sky | 6.6:1 AA | The Get started tab, path-card buttons, the light footer. |
| Aa Footer | #20305C on Sky | 5.2:1 AA | Footer column titles and dimmed text in the light theme. |
| Aa After | White on Seagrass Ink | 5.3:1 AA | The After panel. |
| 580 / 600 | Midnight on Sand | 12.3:1 AAA | The character count near its limit. |
| 612 / 600 | Midnight on Coral | 4.8:1 AA | The character count past its limit. |
| ring | The focus ring on the page ground | 15.9:1 AA, UI | The 3px focus ring on every control. |
| Dark theme | |||
| Aa Ink | Ink on Midnight | 16.6:1 AAA | Headings and body copy. |
| Aa Ink Soft | Ink Soft on Midnight | 11.7:1 AAA | Secondary text. |
| Aa Ink Soft | Ink Soft on Surface | 10.5:1 AAA | Text inside cards and fields. |
| Aa a link | Sky on Midnight | 7.8:1 AAA | Links, the focus ring, and Sky Ink, which points back at Sky in dark. |
| Aa Book a call | Navy on Sky | 6.6:1 AA | The primary button; the contact drawer and mobile menu, which invert to a Sky ground. |
| Aa Seagrass Ink | Seagrass Ink on Midnight | 8.4:1 AAA | The accent inks. Lantern points back at the raw palette; Seagrass and Coral take lighter steps. |
| Aa Lantern | Lantern on Midnight | 6.2:1 AA | |
| Aa Coral Ink | Coral Ink on Midnight | 6.7:1 AA | |
| Aa After | Midnight on Seagrass | 6.0:1 AA | The After panel. |
| Aa Footer | Paper on Midnight | 18.1:1 AAA | The 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.
| Sample | Pair | Ratio | Instead |
|---|---|---|---|
| Sky on the light ground | 2.4:1 Below AA | Text takes --link; a meaningful icon or numeral takes --sky-ink. | |
| Paper on Sky | 2.3:1 Below AA | A Sky fill always takes Navy ink. That is what --cta-ink is for in dark. | |
| Lantern on the light ground | 2.9:1 Below AA | The -ink step: --lantern-ink, --seagrass-ink. | |
| Seagrass on the light ground | 3.0:1 Below AA | ||
| Coral on the light ground | 3.8:1 Below AA | Error text stays Ink. The Coral border and the ▲ carry the signal. |
How the switch works
| Part | What it does |
|---|---|
| Default | No attribute: the page follows the device through prefers-color-scheme. |
| Override | data-theme="light|dark" on <html>, set by the theme buttons in the header, the footer, and this guide’s bar. |
| Expiry | An override is stored with its timestamp and lasts 8 hours; then the device setting applies again. |
| Switching | theme.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 paint | theme.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-scheme | Set alongside the tokens, so native controls, scrollbars, and form widgets follow too. |
| The mark | Swapped 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: theprefers-color-schemeone 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. Adark:utility is a color decision hidden in a class list. - Re-check contrast on the dark sideThe
-inkaccents 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-inkand--ba-after-inkare 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
Aa
Tenor Sans
400. The wordmark and the How-it-works numerals. It never sets running text.
Coastal 1 2 3 4
$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
| Role | Typeface | Where it’s used |
|---|---|---|
| Logo | Tenor Sans 400 | The wordmark and the How-it-works numerals. Never running text. |
| H1 | Questrial 400 | The page or hero heading, one per page. |
| H2 to H4 | Questrial 400 | Section and card headings. Never bold. |
| Labels | Questrial 400 | Buttons, nav, table heads, tracked uppercase labels. |
| Body | Ysabeau 400–600 | Reading copy, ledes, meta text. |
| Figures | IBM Plex Mono 400 | Prices only, through .num and .price-fee. Phone numbers stay in Ysabeau: Mono’s full-width space and brackets open visible gaps in them. |
| Form errors | IBM Plex Mono 500 | Inline 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.
Understanding benefits.
Resolving bills.
Advocating for you.
Medical Bill & EOB Review
A calm, plain-spoken read for intros: the size that carries a section’s opening sentence.
Running text sits at 18px, a size chosen for older readers, per the brief’s accessibility requirement, and stops at a 62-character measure.
Insurance Claim Appeal
Field hints, the privacy note, and price units.
Book a free call
Where it’s used
Hourly services
$1,250.00
Enter your name.
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.
-
Primary
Sky shield · Coral heart -
Dark
Light Sky shield · near-white heart -
One-color
Navy only: letterhead, stamps -
Reversed
Paper shield · Cyan heart -
Deep-tone·alt
Muted navy shield · Coral heart
Size and clear space
| Measure | Value |
|---|---|
| Minimum size | 20px for the full mark; 28px and up recommended. |
| Clear space | 12 units, 10% of the mark’s width, on all sides. |
| Below about 28px | The stripes close up. Use a favicon variant, never the full mark. |
Where it appears
| Where | Height | Colorway |
|---|---|---|
| Site header lockup | 3.25rem on phones, 5.46875rem from 480px, 6.25rem from 1024px (--logo-h) | Primary; Dark in the dark theme, through --brand-mark |
| Site footer lockup | 5rem | Deep-tone·alt on the Sky footer; Reversed on the Midnight one |
| Message-sent panel | 3.5rem | Primary or Dark, with the light drawn behind it |
| This guide’s bar | 2.5rem | Reversed, 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.
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.
Horizontal
the default
Stacked
for a square space
Wordmark
no mark
Logotype
“Coastal” alone
| Form | Minimum width | Use |
|---|---|---|
| Horizontal | 180px | The default, wherever there is room. |
| Stacked | 120px | Square spaces. |
| Wordmark | 140px | Where the mark already appears nearby. |
| Logotype | 90px | App headers and tight spaces. |
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
Span 9 — 1000px · hero, full-bleed section
Span 6 + 3 — 659 / 317px · article beside a rail
Span 3 × 3 — 317px each · card rows
Span 5 — 545px · running text, capped at 62ch
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.
| Class | What it sets |
|---|---|
.canvas | The reading column: 1000px, centered, 1.5rem of side padding (2rem from 480px up). Wraps the content of every section. |
.canvas-wide | 1180px, same padding. The site header and footer only, so they breathe a little wider than the reading column. |
.section | Vertical rhythm: padding-block: clamp(2.25rem, 1.5rem + 3.25vw, 4rem). Block padding only, so it can share an element with .canvas. |
.section--tight | The same, one step down (2rem to 3rem). For a closing or secondary band. |
Breakpoints
| Name | Width | What changes |
|---|---|---|
sm | 480px | Canvas padding goes from 1.5rem to 2rem. |
md | 768px | Where most two-up card grids open. |
nav | 930px | The hamburger gives way to the desktop links. Measured, not guessed: re-measure if the nav, logo, or icons change. |
lg | 1024px | .grid-9 takes its nine tracks and column spans start applying. This guide’s contents rail moves beside the reading column. |
xl | 1200px | Available; 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
| Token | Width | For |
|---|---|---|
measure | 62ch | Running copy. |
prose | 68ch | Legal pages and FAQ answers, where a longer line costs less than more scrolling. This guide’s prose, too. |
.lede | 60ch | A 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-9only 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 token
gap-gutterin 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.
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
| Depth | Shadow | On |
|---|---|---|
| Rest | shadow-card | Cards at rest. |
| Lift | shadow-lift | Framed photographs, the How-it-works step cards, and tile hover. |
| Header scrolled | 0 4px 16px rgba(10, 15, 36, 0.07) | Added to the site header once the page leaves the top. |
| Floating layer | 0 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
| Radius | Token | On |
|---|---|---|
| 12px | rounded-card | Cards, buttons, photo frames, and the callout. |
| 10px | rounded-tile | Tiles. |
| 8px | Literal | Form fields and notes. |
| 22px and 15px | Literal | The step cards: a Surface frame around a tinted inner panel. |
| Round | 999px | True 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.
| Piece | What it is |
|---|---|
| Entry-path cards | Two 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 cards | Four 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, read | A 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 band | Two 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 sent | When 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 tab | The 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.
Anatomy
Every control is one .field block: a label, the control, an optional
hint, and an error slot that starts hidden.
| Class | What it is |
|---|---|
.field | The wrapper: a <p>, or a <fieldset> for a group. |
.field-label | Questrial 15px. On a group this is the <legend>. |
.input .textarea .select | Full width, 48px minimum height, 8px corners, a 1px Ink Soft border. On focus the border darkens to Ink under the global ring. |
.field-hint | Caption-size help, referenced by the control’s aria-describedby. |
.field-error | Hidden until it has text. Plex Mono 500 with a Coral ▲ before it. |
.req | The required asterisk. Ink, not Coral, so Coral only ever means an error. |
.choice | A 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-note | The tinted notice above the fields. This is where the health-details warning lives. |
.error-summary | A Coral-bordered list of every error, above the fields, linking to each one. |
.hp-field | The honeypot. Visually hidden, never display: none, or a bot sees that it is inert. |
- Don’t build a second formAdd fields to
#intake-forminstead. 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 clonecf-;modal.jsre-keys them so both copies can share a document. A validated fieldXneeds an error elementX-err. That is the whole contract. - Label with
for, help witharia-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 ways
aria-invalid="true"on the control, the error id joined onto itsaria-describedby, and the message inX-err. On a failed submit the summary takes focus, which is how it gets announced; there is norole="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
companyfield, the two-second time-trap, and the Turnstile check are load-bearing;contact-form.jsdrops 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.
| Overlay | Shape | Opened by | Also |
|---|---|---|---|
| Contact dialog | A 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 preferences | A 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 menu | A 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
| Hook | What it does |
|---|---|
data-open-contact | On 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-title | The dialog heading. Defaults to the trigger’s own text. |
data-context | Passed through to the form’s hidden context field, so a submission says which entry point it came from. |
data-close-contact | On anything inside the dialog. Closes it. |
data-calendly-toggle | The 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-after | For 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:close | Dispatched on document if you need to react. |
Parts
| Part | What it is |
|---|---|
#contact-modal | The <dialog>. An edge-anchored drawer; .is-lightbox centers it. |
#contact-backdrop | The dim. A sibling element, not ::backdrop. |
.modal-scroll | The scrolling body, 100dvh so mobile browser chrome doesn’t clip it. |
.modal-close | Pinned to the dialog rather than the scroller, so it stays put. |
.modal-confirm | The “discard your message?” cover, shown when closing with text in the form. |
.is-closing | Added 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-formit 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#intakeand adddata-open-contacton 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, thenclose()onanimationend. Callingclose()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 moves | How | With reduced motion, or Animations off |
|---|---|---|
| Hero entrance | The 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 headings | Clear from a 5px blur over 0.82s; then the Sky rule sweeps out from its start over 1s. | Already in place. |
| Lists and table rows | Children 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 paths | Drift in 28px from their own sides. In the Before and After pair, After slides out from under Before. | Already in place. |
| Photographs | Fade in over 0.9s while easing back from a 6% zoom over 1.4s. | Already in place. |
| How-it-works thread | Draws itself once, then marches slowly while on screen and pauses off it. | Drawn and still. |
| Sample statement | The document rises; then each highlight swipes in and its pen mark draws, one note at a time. | Already marked. |
| Booking-band maze | As 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 sent | A Sky glow blooms behind the mark and one slow beam sweeps round. Once per message. | The glow fades in. |
| Get started tab | The first time it comes out it leans past its resting place and settles back, 0.9s. Later appearances slide. | Appears. |
| Drawer and lightbox | Slide or scale in; the close plays out before the dialog really closes. | A fade. |
| Theme switch | Light and dark blend like dusk over 280ms, as one View Transition across the whole page. | An instant swap. |
| Buttons, tiles, links | A 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.cssname::view-transition-*(root)and carry no class, because a page has one root transition at a time. A second feature that calledstartViewTransitionwould inherit the theme’s 280ms cross-fade, so give that one its ownview-transition-namerather 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.
| Token | Light | Dark | What it is for |
|---|---|---|---|
| Color | |||
--bg | #FEFEFE | #0A0F24 | The page ground. |
--surface | #FDFEFF | #101A3A | Cards, form fields, tables, and alternating section bands. |
--ink | #0F1E4A | #EAF0FF | Headings and body copy. |
--ink-soft | #3C4A73 | #BCCCF0 | Secondary text, table heads, hints, and form-field borders. |
--line | #DDE4F2 | #25315C | Hairline borders and dividers. |
--line-soft | #EAEFF9 | #1B274A | Table rows and quieter section edges. |
--tag-bg | #EAF3FF | #16234A | Tint 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 #E8C878 | The draft and to-do flags on the legal pages. |
--link | #0B5FA5 | var(--sky) | Inline prose links. |
--sky-ink | #1B6AB4 | var(--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 | #A85A00 | var(--lantern) | |
--coral-ink | #C4140F | #FF6A66 | |
--cta-bg --cta-ink | var(--navy) var(--paper) | var(--sky) var(--navy) | The primary button. |
--accent-hover-bg --accent-hover-ink | var(--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-ink | var(--seagrass-ink) #FFFFFF | var(--seagrass) var(--midnight) | The After panel. |
--ring --focus-on-dark | var(--navy) var(--sky) | var(--sky) var(--sky) | The focus ring, and the ring under .on-dark. |
--footer-bg --footer-ink | var(--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 tokens | A Sky ground with Navy ink | The contact drawer and the mobile menu, which invert in dark. |
--brand-mark --brand-foot-mark | Primary, Deep-tone·alt | Dark, Reversed | The mark’s SVG URL in the header and footer lockups. |
| Type | |||
font-logo | Tenor Sans 400 | The wordmark and the How-it-works numerals. | |
font-label | Questrial 400 | Every heading, button, nav item, and tracked uppercase label. Questrial ships 400 only, so these are never bold. | |
font-body | Ysabeau 400–600 | Reading copy, ledes, and meta text. One variable file covers the range. | |
font-mono | IBM Plex Mono 400 and 500 | Prices and inline form errors only. | |
--font-sans | The font-label stack | For 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-lede | See Type scale | The five sizes that are tokens. | |
| Layout | |||
max-w-canvas | 1000px | The reading column. The header and footer use 1180px. | |
max-w-measure max-w-prose | 62ch, 68ch | Line-length limits. | |
gap-gutter | 24px | The grid gap. | |
rounded-card rounded-tile | 12px, 10px | Corners. Fields are a literal 8px. | |
shadow-card shadow-lift | See Components | Neutral near-black elevation only; no chromatic or navy glow. | |
ease-calm | cubic-bezier(0.22, 1, 0.36, 1) | The easing for buttons and the Get started tab. | |
--logo-h --header-h | 3.25rem, then 5.46875rem from 480px and 6.25rem from 1024px | The 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.
| Name | Set by | Meaning |
|---|---|---|
data-theme | theme.js | A stored light or dark override. |
data-text | theme.js | Text size: lg, xl, or xxl. It scales the root by 1.125, 1.25, or 1.5 through --text-scale. |
data-motion | theme.js | off when the Animations switch is off. |
data-reader | theme.js | on in reader mode. |
--i --reveal-i | Inline; main.js | An 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.
| Page | Made from |
|---|---|
| JavaScript reference | The 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 values | Custom 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 reference | The 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 README | Written by hand: the authoritative brand spec for the mark, the lockups, and the palette. |
DESIGN.md | Written by hand, in the repository: the reasoning behind every rule on this page. |
ARCHITECTURE.md | Written 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 adata-*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’sdata-calendly-aftercopy 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.