Skip to content

Harbor DS / foundations

Calm on the surface. Exact underneath.

Stone, paper, olive and clay for the calm. One violet for the moment that matters, and a clear next step on every screen.

Japandi + violetTokens: W3C DTCG 2025.10Inter for everything you read and count, Geist Mono for the instrument

01 / Color

Ramps

Authored in OKLCH, one hue per family. Ratios are computed live from the rendered swatch against white (w) and against stone-950 (k).

Semantic surfaces and text

Components reference these, never the ramps. Each tile shows the three text tiers on that surface.

02 / Clinical status

Six roles, never color alone

Every status carries an icon and a word. Violet is never a status, olive is never anything but a call, and normal results stay neutral.

Critical

Critical value, severe allergy, destructive action. The scarcest color.

K+ 6.2 criticalCritical
Abnormal

High or low value, overdue, needs attention.

Hgb 10.9 lowAbnormal
Approved

Signed, approved, verified, final. Powder forest, always with a check or signature.

SignedApproved
Info

What the system knows: a result arrived, a fax was filed.

Result receivedInfo
Pending

Preliminary, unsigned, awaiting. Neutral, dashed, clock.

Awaiting payerPending
Inactive

Entered in error.

VoidedEntered in error

02b / Two greens

Light sage calls. Deep forest approves.

Lightness does the separating, so it survives colour blindness. Calls always carry a handset; approvals always carry a check or a signature.

As designed
Simulated deuteranopia
Simulated protanopia
Distance between the pair (computed from the token source)

02c / Action rule (open question)

Sumi does the work. Violet moves you forward.

Proposal: everyday primary buttons are sumi ink. Violet is kept for brand moments and the one key action per screen, so it still pops against the natural base.

Proposed: sumi primary, one violet key action
Alternative: violet for every primary

The "Primary rule" switch in the rail applies the alternative to every primary button on this page.

03 / Contrast

Computed, not estimated

Every pair the foundations use, checked in light, dark and both contrast-more themes at build time. Text needs 4.5:1 with no large-text exemption; controls and the focus ring need 3:1.

All pairs

04 / Type

Two faces, one job each

Inter speaks and does the work: headlines at 24px and up, light subheads, thin accents, every label, sentence and number. Geist Mono joins in DS-02.

Data: tabular, slashed, unambiguous

Brand text: proportional digits, no tnum
13.8111.1410.098.44.17
harbor-type-data: tnum, zero
13.8111.1410.098.44.17
Brand text (top) vs data role: I l 1, O 0, µ, ≤ ≥
Il1 O0 µ ≤ ≥
Il1 O0 µ ≤ ≥

05 / Space, radius, elevation

A 4px grid with room to breathe

Lift means you can act on it. Tint means it is telling you something.

Space

Radius

Semantic radii follow density: control, card, panel and tag step down one size in compact.

e0 / tintInformation. No shadow, no border.
e1 / cardClickable card. Hairline + whisper.
e2 / popoverMenus, popovers, call bar.
e3 / modalModals and sheets over the scrim.
ToastInverse surface, e3 shadow.

06 / Focus and density

Visible from across the room

One stroke, never two. A bordered control's own border turns deep violet at 2px. A filled button gets nothing outside it: its fill steps darker or lighter and a 1px stroke of its own hue appears just inside the edge.

See it in use

Approve (tonal forest)
Bordered: the border is the ring
Input on brand tintMember ID
On critical tint
On inverse (toast)
Key, solid: superseded, reference only (data-form="solid")
Key (tonal, Nik's pick): one per screen
RestHoverFocusPressed
Approve (forest)
RestHoverFocusPressed
Primary
Critical (red)
RestHoverFocusPressed
Call (sage)
RestHoverFocusPressed
Tint

Current density

Switch density in the rail. These values come from density.*.json and re-flow everything on the page, including the compositions below.

Search patients

06b / Avatars

Photo first, earth-toned initials after

Patient photos are approved for the chart header. Without one, initials sit on an earthy tint picked from the person's ID, never olive or forest.

See it in use

MRxl 80, photo (initials fallback)
MRlg 56, clay
LBmd 40, sand
JPmd 40, stone
LKsm 32, plum
AOxs 24, indigo

07 / Layers, breakpoints, motion

The quiet tokens

Named layers instead of raw z-index. Breakpoints for the page shell only. Motion values are placeholders until the motion phase.

See it in use

z-index

Breakpoints and containers

Motion (placeholder)

08 / Bento grid

Give it a size, it lays itself out

Tiles declare width and height in grid units. Rows are one unit tall, with one gap both ways, so two 2-unit tiles stack to exactly one 4-unit tile. Below 360px it is one column, in reading order.

See it in use

Each tile prints its live span (columns × row units) for the width it is at now. Resize the window: the spans and column counts change per container tier (narrow 360, medium 560, wide 800, xwide 1040), and the order you read and tab through never changes.

Coverage

columns { narrow: 2 } · copay 1×2 and prior auth 1×2 stack beside deductible 1×4
Copay today
$25
Deductible met
$1,840
of $2,500
$660 to go this year
Prior authorization
None

Today at a glance

columns { narrow: 2, wide: 4, xwide: 6 } · link tiles; widths change per tier, the order never does

Vitals

columns { narrow: 2, medium: 3, xwide: 6 } · blood pressure keeps 2 columns, the rest fill in
Blood pressure
128/82 mmHg
Heart rate
72 bpm
SpO2
98 %
Temp
36.8 °C
Weight
68.2 kg

Schema (generative-UI primitive)

The typed schema and its fixtures live with the component now: Components, BentoGrid. These three mosaics are rendered from it by the build; HarborBento.render stays the page's runtime for generators, with its own validation.

09 / Compositions

The foundations carry real screens

Each shell is built from tokens, plain HTML and the components listed below, and filled with real clinical content.

Patient header

clinical canvas, follows the density toggle

Lab values with reference range and flag

resize the window: the row reflows on its container, not the viewport

Basic metabolic panel + CBC

Collected , resulted
PotassiumLOINC 2823-3
6.2 mmol/L
Critical high
3.5
5.1
Final
HemoglobinLOINC 718-7
10.9 g/dL
Low
12.0
15.5
Final
WBCLOINC 6690-2
7.4 10³/µL
Normal
4.5
11.0
Final
Glucose, fastingLOINC 1558-6
98 mg/dL
Normal
70
99
Prelim
PlateletsLOINC 777-3
410 10³/µL
High
150
400
Final
SodiumLOINC 2951-2
141 mmol/L
Entered in error
136
145
Voided

Insurance card

paper canvas, density pinned to relaxed on this subtree

Your visit today

We checked your plan before you arrived. There is nothing you need to do.

Copay
$25
Deductible met
$1,840
of $2,500
$660 to go this year
Prior auth
None
Why we ask for your card

So the clinic can bill your plan directly. Your card photo is stored with your chart and never shared.

Patient intake question

paper canvas, relaxed subtree, one decision per screen
About youInsuranceConsentReason

How will you pay for today's visit?

We check your coverage before you arrive, so the front desk has nothing to chase.

About 2 minutes left.

10 / Layout and composition

Lines you feel, boxes you don't need

One grid per breakpoint, one spacing ladder, and a short list of things that never break. Space groups, a shared tint gathers, and a hairline encloses only what you can act on.

The grid at this width

live: this page's real margin, columns and gutter

Tier . Resize the window: the margin and column count change at the breakpoints (600, 900, 1200, 1600); the gutter follows density, so it matches the gap between cards.

Page grid, per breakpoint

to scale, from grid.columns, layout.margin, space.gutter, rail and pane widths

Spacing ladder

each rung separates what the rung below it groups

Do and don't

proximity, alignment, wrapping, truncation, enclosure, reading order

Stacks, clusters, grids, bento

pick by what has to line up

Sticky and scrolling

regions numbered in reading order, the same order on every device

11 / Control sizes

Six sizes, one formula

md is the control you already know. Each tier up adds one 4px grid step of height; everything inside grows at a fixed fraction of that step. The touch floor never changes the drawing: it is an invisible hit area.

See it in use

v(tier) = max(floor, v(md) + rate × 4px × (tier − md))

  • height rate 1, no floor: every tier draws its formula height in both densities and on any pointer
  • padding-x and icon rate 0.5; the drawn width hugs the label, with no minimum
  • label size, gap and radius rate 0.25; label floor 12px
  • padding-y derived, so the label is hugged and centered
  • hit area max(drawn, size.target-min) on each axis, centered and invisible: 24 in compact, 44 in relaxed and on a coarse pointer
  • space.touch-gap 2 × the xs hit extension, the closest two controls may sit; space.touch-inset half of it, the least padding a surface gives its controls

The dashed boxes below are a docs-only overlay of each control's real hit area; on a product surface it draws nothing. Focus stays on the drawn box. resolveControlSize('auto', context) picks lg under a 360 pane. Tokens: --harbor-control-<tier>-* and --harbor-space-touch-*, generated from src/size.ts.

Compact, fine pointer

The target is 24 (WCAG 2.5.8), which every tier already draws: the hit area is the drawn box.

xs drawn 24, hit 24
sm drawn 28, hit 28
md drawn 32, hit 32
lg drawn 36, hit 36
xl drawn 40, hit 40
2xl drawn 44, hit 44
Compact (the default), px. Touch gap 0 fine, 20 coarse; touch inset 0 fine, 10 coarse.
Tokenxssmmdlgxl2xl
drawn height242832364044
hit area, fine (per side)24 (+0)28 (+0)32 (+0)36 (+0)40 (+0)44 (+0)
hit area, coarse (per side)44 (+10)44 (+8)44 (+6)44 (+4)44 (+2)44 (+0)
padding-x81012141618
padding-y57891011
label size121213141516
icon101214161820
gap67891011
radius456789

Compact, coarse pointer (preview)

The drawing stays compact; each hit area grows to 44 × 44, and the samples step apart by space.touch-gap.

xs drawn 24, hit 44
sm drawn 28, hit 44
md drawn 32, hit 44
lg drawn 36, hit 44
xl drawn 40, hit 44
2xl drawn 44, hit 44

Relaxed

xs and sm are drawn at 36 and 40, smaller than md; their hit areas are 44 tall. A coarse pointer changes nothing more here.

xs drawn 36, hit 44
sm drawn 40, hit 44
md drawn 44, hit 44
lg drawn 48, hit 48
xl drawn 52, hit 52
2xl drawn 56, hit 56
Relaxed (patient-facing frames), px. Touch gap 8 fine, 8 coarse; touch inset 4 fine, 4 coarse.
Tokenxssmmdlgxl2xl
drawn height364044485256
hit area, fine (per side)44 (+4)44 (+2)44 (+0)48 (+0)52 (+0)56 (+0)
hit area, coarse (per side)44 (+4)44 (+2)44 (+0)48 (+0)52 (+0)56 (+0)
padding-x161820222426
padding-y111213141516
label size121314151617
icon121416182022
gap67891011
radius8910111213

12 / Content rules

Two dates, one clock

A date reads as a sentence where a patient reads it, and as a column where a clinician scans it. Machines always get ISO.

RoleRendersFormatter (src/format.ts)Where
Display dateformatDate(iso, 'display')Patient-facing surfaces, cards, calendars, headings
Body dateformatDate(iso, 'body')Practice-facing tables, patient data, charts, dense lists
TimeformatTime(iso)12-hour with AM/PM by default; 24-hour is an org setting
Date and time, displayformatDateTime(iso, 'display')A patient-facing moment
Date and time, bodyformatDateTime(iso, 'body')A practice-facing timestamp
  • <time datetime> always holds the ISO value; the words are for people.
  • Shown as written, in the clinic's wall clock: the formatter never converts zones, so every machine prints the same thing.
  • Never breaks inside: a date or time moves whole to the next line (DS-P1 §15.7).
  • In a view model a date is a run, { date, role }, { time } or { dateTime, role }, never a preformatted string. The clock defaults to 12h.

Where the roles are applied in this specimen

Collected by the build from every date in the site sources: patient-facing surfaces use the display date, practice-facing ones the body date.

SourceRoleRenders
site/demo/approval.htmlbody date and time10/23/2026, 10:30 AM
site/demo/labs.htmlbody date and time10/23/2026, 7:40 AM
site/demo/patient-hero.htmldisplay dateOct 23, 2026
site/foundations/13-compositions.htmlbody date and time10/22/2026, 7:40 AM
site/foundations/foundations.jsbody date and time10/22/2026, 7:40 AM
site/layout/layout.jsbody date and time10/23/2026, 10:30 AM
site/layout/layout.jsdisplay dateOct 23, 2026
site/shells/insurance-card.htmldisplay dateJan 1, 2026
site/shells/patient-header.htmlbody date03/09/1985