Brand  /  Design system · v3.1 · WCAG 2.1 AA

One brand. Five products.
Each with its own signal.

Every ExpiWell application shares one core — void slates, chalk text, cream intermissions, and the lime call-to-action. Each product leads with a single dominant accent: the signal color that identifies which app you're in. The other four accents stay in the room as sub-colors — for data categories, charts, and semantic states — so five apps always feel like one company.

  AA build — every text pairing ≥ 4.5:1 · UI boundaries ≥ 3:1 · WCAG 2.1

Research
Practice
Polling
Pharma
Wellbeing
Core palette

The constant layer

Shared by every application and the marketing site. These never change per product — they're what makes five apps feel like one company. Click any swatch to copy its hex.

Semantic layer

Same tokens, both modes

Components never reference raw hexes — they reference the semantic layer, and the layer re-scopes per band. Dark bands map to the void slates; light bands map to cream and white. The descending ink hierarchy — ink → ink-2 → ink-3 → mute — creates visual weight without reaching for color.

--bgPage background
#F7F6EC
#232C37
--surfaceCards, panels, modals
#FFFFFF
#2B3542
--surface-2Selected row, secondary surface
#FAFAF3
#39434F
--inkPrimary text, primary buttons
#232C37
#FBFCF8
--ink-2Body text (softer than ink)
#2B3542
#C2C7CB
--ink-3Secondary text, helper copy
#5B6572
#9AA3AD
--muteDe-emphasized labels, mono meta
#667079
#8A94A0
--lineDefault hairline borders
#E4E2D4
#414B58
--line-2Input borders, strong dividers
#8B8672
#7E8996

  Edit one file → every app updates. Brand differences are a handful of CSS-variable overrides; everything here stays constant.

Accent scales

Every accent, five jobs

Each accent exists at five steps. Bright is the original app color — for dots, charts, and UI on dark. Hover is one notch of pressure. Muted is the globe-toned sibling for large fills on cream. Tint is the pale wash behind pills and banners. Deep passes AA contrast for text on light backgrounds and on its own tint. Each accent also names its solid — the button fill tuned to pass AA with that accent's ink: the light hues rest on the bright itself with void text; the saturated hues rest one step deeper so chalk text clears 4.5:1.

Accent
Bright · dots & charts
Solid + ink · buttons
Muted · fills
Tint · pill bg
Deep · AA text
Green
#8DC63F
Aa
#8DC63F
#9FBF6B
#EEF6DC
#51791B
Blue
#4285D6
Aa
#3572BC
#5F8FC0
#E7EFF9
#31659E
Gold
#F2C230
Aa
#F2C230
#E2B23F
#FBF1D3
#7E6110
Pink
#E8578C
Aa
#C63A6B
#C4789B
#FBE5EE
#A63A66
Violet
#8A6FC5
Aa
#7A5DB6
#9C8DBE
#EFEBF8
#5F4699

  Rule of thumb — dots & charts: bright. Buttons: solid + ink. Large fills on cream: muted. Pill & banner backgrounds: tint. Text on cream or tint: deep. Every pairing shown here measures ≥ 4.5:1.

Product family

One accent leads, four support

The dominant accent drives CTAs, focus, and identity. The remaining four accents become the app's ordered sub-colors — the palette for data categories, chart series, and secondary highlights. Components, spacing, and type never change between products.

Research

Research Platform

Universities, labs & organizations running EMA and ESM studies.

Bright
Muted
Tint
Deep
Sub-colors —
Action
Live signal
Practice
New

ExpiWell Practice

Clinicians, therapists & coaches using EMA with clients.

Bright
Muted
Tint
Deep
Sub-colors —
Action
Live signal
Polling
Pipeline

Polling

In-the-moment public opinion, captured where it forms.

Bright
Muted
Tint
Deep
Sub-colors —
Action
Live signal
Pharma
Pipeline

Pharmaceutical

Real-world evidence and patient-reported outcomes for trials.

Bright
Muted
Tint
Deep
Sub-colors —
Action
Live signal
Wellbeing
Pipeline

Corporate Wellbeing

Employee wellbeing, measured in daily life — not annual surveys.

Bright
Muted
Tint
Deep
Sub-colors —
Action
Live signal
Try it —
Research Platform

Capture experience as it actually happens.

Same components, same layout, same type — only --accent and the sub-color order change.

Signal
Sub 1
Sub 2
Sub 3
Sub 4
Responses · by categoryN=248

Light band — same tokens, inverted

Text stays readable because links use the deep step: an accent link

Semantic roles

The palette does double duty

Status colors aren't a second palette — they're drawn from the family. Warn borrows the gold scale, info borrows the blue scale, success borrows the green scale. Only danger stands apart, reserved for destructive actions and errors. Each role ships its base (icons and dots), an AA solid for buttons, a tint, and a dark text step — and a tint background is always paired with its matching dark text.

Accent

The brand signal — switches with the app. Active states, live indicators, carets.

--accent
--accent-solid
--accent-tint
--accent-deep
ActiveConfirm

Info · from the blue scale

In-progress states, help banners, tip strips.

--info
--info-s
--info-t
--info-d
In progressLearn more

Warn · from the gold scale

Caution and past-due states — never errors.

--warn
--warn-s
--warn-t
--warn-d
Past dueReview

Success · from the green scale

Saved, completed, and healthy states.

--ok
--ok-s
--ok-t
--ok-d
CompletedDone

Danger · stands apart

Destructive only — delete buttons, error icons, failed saves.

--danger
--danger-s
--danger-t
--danger-d
No responseDelete

Approved pairings. Always pair a tint background with its matching dark text variant. Mixing across roles breaks contrast and looks broken.

Accent pill--accent-tint × --accent-deep
Active
Info pill--info-t × --info-d
In progress
Warn pill--warn-t × --warn-d
Past due
Danger pill--danger-t × --danger-d
Failed
Body on surface--surface × --ink
Body text
Secondary on surface--surface × --ink-3
Helper copy
Typography

Three voices, one hierarchy

Display
Fraunces 300–500

Capture experience as it happens.

Body & UI
Geist 400–600

High-fidelity data collection and analytics for scientific discovery — captured in the moment, on any device, anywhere in the world.

Labels & data
Geist Mono 400–500
Photo response uploaded — New York, USA

The text atoms. Nine named variants cover every surface — the same names the <Text> atom exposes, so marketing and product share a vocabulary. Display sizes are fluid and always Fraunces; UI and body are Geist; anything uppercase, tabular, or data-flavored is Geist Mono.

displayFraunces 300–400 · fluid clamp / 1.04 · page hero
Capture experience
titleFraunces 400 · fluid clamp / 1.2 · primary title
Every plan, side by side
headingFraunces 500 · 19–24px / 1.15 · section heading
Event-triggering for EMA & ESM
bodyGeist 400 · 16px / 1.5 · default paragraph
The workhorse style for descriptions, helper text, and long-form content.
bodySmGeist 400 · 14px / 1.6 · dense cells, captions
Card descriptions, captions, and metadata.
labelGeist 500 · 14px / 1.2 · buttons, form field labels
Save changes
eyebrowGeist Mono 500 · 12px / .1em · uppercase
The experience research platform
metaGeist Mono 400 · 12px · tabular timestamps & ids
2026-04-25 14:32:08 · req_8a92b1 · N=248
codeGeist Mono 400 · 13px · inline code
const tokens = { accent: 'var(--accent)' }

  Emphasis in display copy = one italic Fraunces word in the accent's deep step. Bold is for UI, never headlines.

Radius, elevation & motion

Sharp corners, soft light

3px is the default everywhere — almost square, but soft enough not to feel hostile. Two larger steps exist for grouped surfaces; pill (999) is reserved for toggle switches and status dots, never buttons. Depth comes from a quiet elevation scale tinted with void slate — never pure black — and motion uses three durations with one default ease.

--radius-sm: 3pxThe default — cards, inputs, badges, buttons, tables
--radius-md: 4pxGrouped panels and modals
--radius-lg: 6pxLarge containers and media frames
--radius-pill: 999pxToggle switches and signal dots only — never buttons
e0 · flatDefault. Hairline border, no shadow — tables, list rows.
--e1 · restingCards and tiles sitting on cream.
--e2 · raisedDropdowns, sticky bars, hover lift.
--e3 · floatingPopovers, toasts, date pickers.
--e4 · modalDialogs and command palettes — heaviest, to overcome backdrop dim.
hover · +1 levelHover me — resting e1 lifts to e2. Nothing harder.
--duration-fast: 0.12sHovers, toggles, color swaps. Hover the track.
--duration-base: 0.15sThe default — lifts, focus rings, reveals.
--duration-slow: 0.25sPanels and modals. Anything longer needs a justification.

  All shadow color = rgba(35,44,55) — the void, diluted. Never rgba(0,0,0). One ease for everything: cubic-bezier(.2,.8,.2,1).

Components

Fields & actions

The canonical form controls and button variants shared by every app on marketing surfaces. Hover any button to feel the one-level lift; click into a field to see the neutral focus ring. Switch the brand in the lab below and the accent variants re-tone too.

Buttons

Primary is always lime — reserved for the one main action. Accent buttons rest on the app's solid step with its ink: void text on the light hues, chalk on the saturated ones. Every resting and hover state measures ≥ 4.5:1. Everything hovers with a one-level lift.

Variants
Sizes

Inputs & selects

Label above, white field, 1px hairline outline. Focus is neutral in every app — the border darkens to ink with a faint slate halo — so a red glow can only ever mean an error. The brand lives in the caret and in toggles, never in the field glow: no accent can collide with danger, in any current or future app.

Enter a valid email address

  Focus = ink outline + 3px slate halo at 12%, identical in all five apps. Caret-color carries the accent. The danger glow is reserved for errors — pink brands never collide with red.

Component lab

Every state, both modes

Each panel forces its own color scheme so you can review parity without toggling anything. Both panels render with the currently selected brand — pick one and watch buttons, pills, banners, and the sub-color chart re-tone in light and dark at once. Click into any field: the focus ring stays neutral while the caret takes the brand color.

Rendering —

Light mode

Buttons
Form states

Enter a valid email address

Status pills
Active
In progress
Past due
No response
Draft
Completed
SOC 2 Compliant
GDPR Compliant
Small
Medium
Banners
Card

Sample card title

Cards live on surface with a hairline border and resting e1. They adapt automatically across modes and brands — nothing here references a raw hex.

Updated 2 min ago · N=248
Sub-colors in data
Responses · by categoryWk 30
Signal
Sub 1
Sub 2
Sub 3
Sub 4

Dark mode

Buttons
Form states

Enter a valid email address

Status pills
Active
In progress
Past due
No response
Draft
Completed
SOC 2 Compliant
GDPR Compliant
Small
Medium
Banners
Card

Sample card title

Cards live on surface with a hairline border and resting e1. They adapt automatically across modes and brands — nothing here references a raw hex.

Updated 2 min ago · N=248
Sub-colors in data
Responses · by categoryWk 30
Signal
Sub 1
Sub 2
Sub 3
Sub 4

  On dark, tints become the role color at 14–16% over the surface, and text lifts to the bright step — same tokens, re-scoped.

System rules

The habits that make it ours

3px corners, everywhere

Cards, buttons, inputs, badges. No pills, no soft rounds. Pill radius is reserved for toggle switches and signal dots.

The dot means live

A small colored circle prefixes mono labels — it marks real signals (responses, status, categories), never decoration.

Italic serif = emphasis

One italic Fraunces word per headline, colored in the accent's deep step. Never bold for emphasis in display copy.

Elevation is quiet

Resting surfaces sit at e1; hover lifts exactly one level; focus adds a quiet ink ring — never the accent, so the red error glow stays unmistakable. Shadows are slate-tinted, layered, never black.

Semantic tokens only

Components reference --bg, --surface, --ink, --line, --accent — never raw hexes. Modes and light bands re-scope the same tokens.

One accent leads, four support

Only the dominant accent drives CTAs, focus, and identity. The other accents work as the app's ordered sub-colors — data categories, chart series, semantic states — never competing for CTA duty.

Tint pairs with its own deep

A tint background always carries its matching dark text — accent-tint × accent-deep, warn-tint × warn-dark. Mixing across roles breaks contrast.

Lime is the ask

The lime CTA belongs to the company, not any one app. It marks the primary conversion action on every surface, in every product.

Every solid carries its ink

Buttons rest on the accent's solid step with its ink token — void on green, gold & lime, chalk on blue, pink & violet — tuned so every label measures ≥ 4.5:1 in any brand.

Tokens

Drop-in CSS

What globals.css ships — core stays constant; products opt in with a single data-app attribute on the root. Switching brand changes the accent tokens and the sub-color order — nothing else.

:root {
  /* core — constant across all apps */
  --void: #232C37;  --void-2: #2B3542;  --panel: #2B3542;
  --panel-2: #39434F;  --hair: #414B58;
  --chalk: #FBFCF8;  --chalk-2: #C2C7CB;
  --dust: #9AA3AD;   --dust-d: #5B6572;
  --paper: #F4F6EE;  --cream: #F7F6EC;
  --lime: #C3D93F;   --lime-d: #51791B;

  /* semantic layer — re-scoped per band (dark values shown) */
  --bg: var(--void);  --surface: var(--panel);  --surface-2: var(--panel-2);
  --ink: var(--chalk);  --ink-2: var(--chalk-2);  --ink-3: var(--dust);
  --mute: #8A94A0;  --line: var(--hair);  --line-2: #7E8996;

  /* accent scales — bright / hover / muted / tint / deep */
  --green: #8DC63F;  --green-h: #7CB32F;  --green-m: #9FBF6B;  --green-t: #EEF6DC;  --green-d: #51791B;
  --blue: #4285D6;   --blue-h: #3572BC;   --blue-m: #5F8FC0;   --blue-t: #E7EFF9;   --blue-d: #31659E;
  --gold: #F2C230;   --gold-h: #E0B01F;   --gold-m: #E2B23F;   --gold-t: #FBF1D3;   --gold-d: #7E6110;
  --pink: #E8578C;   --pink-h: #D4467A;   --pink-m: #C4789B;   --pink-t: #FBE5EE;   --pink-d: #A63A66;
  --violet: #8A6FC5; --violet-h: #7A5DB6; --violet-m: #9C8DBE; --violet-t: #EFEBF8; --violet-d: #5F4699;

  /* ink per accent — the text color for that accent's solid fills */
  --green-ink: #232C37;  --lime-ink: #232C37;  --gold-ink: #232C37;
  --blue-ink: #FFFFFF;   --pink-ink: #FFFFFF;  --violet-ink: #FFFFFF;

  /* solid steps — AA button fills (light hues = bright; saturated rest deeper) */
  --green-s: var(--green);      --green-sh: var(--green-h);
  --blue-s: var(--blue-h);      --blue-sh: var(--blue-d);
  --gold-s: var(--gold);        --gold-sh: var(--gold-h);
  --pink-s: #C63A6B;            --pink-sh: var(--pink-d);
  --violet-s: var(--violet-h);  --violet-sh: var(--violet-d);

  /* lifted steps — AA pill text over dark tints */
  --green-lift: var(--green);  --gold-lift: var(--gold);
  --blue-lift: #9DBEE0;  --pink-lift: #F2A9C4;  --violet-lift: #BCA8E3;

  /* roles — the palette doing double duty (danger stands apart) */
  --danger: #E55448;  --danger-h: #C9453A;  --danger-t: #FBE7E4;  --danger-d: #9A2F27;
  --warn: var(--gold-m);  --warn-h: #C99A28;  --warn-t: var(--gold-t);  --warn-d: var(--gold-d);
  --info: var(--blue-m);  --info-h: #4A7BAE;  --info-t: var(--blue-t);  --info-d: var(--blue-d);
  --ok: var(--green);     --ok-h: #679A23;    --ok-t: var(--green-t);   --ok-d: var(--green-d);
  --danger-ink: #FFFFFF;  --warn-ink: #232C37;  --info-ink: #FFFFFF;  --ok-ink: #232C37;
  --danger-s: var(--danger-h);  --danger-sh: #A83228;
  --warn-s: var(--warn);    --warn-sh: var(--warn-h);
  --info-s: #4374A3;        --info-sh: var(--info-d);
  --ok-s: var(--ok);        --ok-sh: var(--green-h);

  /* focus — neutral in every app; accents never glow on text fields */
  --focus-border: var(--ink);
  --focus-ring: 0 0 0 3px color-mix(in srgb, var(--ink) 12%, transparent);

  /* shape · elevation · motion */
  --radius-sm: 3px;  --radius-md: 4px;  --radius-lg: 6px;  --radius-pill: 999px;
  --e1: 0 1px 2px rgba(35,44,55,.05), 0 1px 3px rgba(35,44,55,.08);
  --e2: 0 2px 4px rgba(35,44,55,.05), 0 4px 10px rgba(35,44,55,.08);
  --e3: 0 4px 10px rgba(35,44,55,.06), 0 10px 24px rgba(35,44,55,.10);
  --e4: 0 8px 18px rgba(35,44,55,.08), 0 18px 40px rgba(35,44,55,.12);
  --duration-fast: .12s;  --duration-base: .15s;  --duration-slow: .25s;
  --ease: cubic-bezier(.2, .8, .2, 1);
}

/* light bands re-scope the semantic layer — components don't change */
.light-band {
  --bg: var(--cream);  --surface: #FFFFFF;  --surface-2: #FAFAF3;
  --ink: var(--void);  --ink-2: var(--void-2);  --ink-3: var(--dust-d);
  --mute: #667079;  --line: #E4E2D4;  --line-2: #8B8672; /* line-2 = input borders, >=3:1 */
}

/* one dominant accent per application + ordered sub-colors */
[data-app='research'] { --accent: var(--green);  --accent-hov: var(--green-h);
  --accent-t: var(--green-t);  --accent-deep: var(--green-d);  --accent-contrast: var(--green-ink);
  --accent-solid: var(--green-s);  --accent-solid-h: var(--green-sh);  --accent-lift: var(--green-lift);
  --sub-1: var(--blue);  --sub-2: var(--gold);  --sub-3: var(--pink);  --sub-4: var(--violet); }
[data-app='practice'] { --accent: var(--blue);  --accent-hov: var(--blue-h);
  --accent-t: var(--blue-t);  --accent-deep: var(--blue-d);  --accent-contrast: var(--blue-ink);
  --accent-solid: var(--blue-s);  --accent-solid-h: var(--blue-sh);  --accent-lift: var(--blue-lift);
  --sub-1: var(--green);  --sub-2: var(--gold);  --sub-3: var(--violet);  --sub-4: var(--pink); }
[data-app='polling'] { --accent: var(--gold);  --accent-hov: var(--gold-h);
  --accent-t: var(--gold-t);  --accent-deep: var(--gold-d);  --accent-contrast: var(--gold-ink);
  --accent-solid: var(--gold-s);  --accent-solid-h: var(--gold-sh);  --accent-lift: var(--gold-lift);
  --sub-1: var(--blue);  --sub-2: var(--green);  --sub-3: var(--pink);  --sub-4: var(--violet); }
[data-app='pharma'] { --accent: var(--pink);  --accent-hov: var(--pink-h);
  --accent-t: var(--pink-t);  --accent-deep: var(--pink-d);  --accent-contrast: var(--pink-ink);
  --accent-solid: var(--pink-s);  --accent-solid-h: var(--pink-sh);  --accent-lift: var(--pink-lift);
  --sub-1: var(--violet);  --sub-2: var(--blue);  --sub-3: var(--gold);  --sub-4: var(--green); }
[data-app='wellbeing'] { --accent: var(--violet);  --accent-hov: var(--violet-h);
  --accent-t: var(--violet-t);  --accent-deep: var(--violet-d);  --accent-contrast: var(--violet-ink);
  --accent-solid: var(--violet-s);  --accent-solid-h: var(--violet-sh);  --accent-lift: var(--violet-lift);
  --sub-1: var(--pink);  --sub-2: var(--blue);  --sub-3: var(--green);  --sub-4: var(--gold); }

/* dark-mode toggle — set data-theme='dark' on <html> (header ThemeToggle) to
   force the cream light-bands back onto the dark semantic layer, so the whole
   page renders uniformly dark. Only [data-theme='dark'] carries overrides; the
   default (light) render is untouched. */
html[data-theme='dark'] .light-band {
  --bg: var(--void);  --surface: var(--panel);  --surface-2: var(--panel-2);
  --ink: var(--chalk);  --ink-2: var(--chalk-2);  --ink-3: var(--dust);
  --line: var(--hair);  --accent-ink: var(--accent); /* …the full :root dark scope */
}
/* .theme-lock opts a band out of the flip — this page keeps its cream
   mode-parity panes (and their AA deep accents) even in dark mode. */
html[data-theme='dark'] .theme-lock .light-band { /* …the cream scope, re-asserted */ }

Using tokens in code — the house patterns

// Typography — only via the <Text> atom (variants in typography-tokens.ts)
<Text variant="heading" as="h3">Event-triggering for EMA & ESM</Text>

// Buttons — only via <AppButton>: primary | cta | accent | ghost | danger | text
<AppButton variant="cta" withArrow>Get Started</AppButton>

// Direct CSS var in sx (layout + token references only — never raw hexes)
<Box sx={{ boxShadow: 'var(--e2)', borderRadius: 'var(--radius-sm)' }} />

// Status pill recipe — tint background × matching deep text
<Chip color="success" label="Completed" />

// Text field recipe — neutral focus, brand caret, danger owns the glow
// (already themed globally: MuiOutlinedInput in styles/theme/components.ts)
<FormControl>
  <FormLabel>Email</FormLabel>
  <OutlinedInput placeholder="you@university.edu" />
</FormControl>

// Chart series — dominant signal first, then the sub-color order
const SERIES = ['var(--accent)', 'var(--sub-1)', 'var(--sub-2)', 'var(--sub-3)', 'var(--sub-4)'];
Logo assets

One wordmark, two inks

The ExpiWell wordmark ships in two ready-to-place variants — download the one built for your background and place it as-is: never recolor it, stretch it, or rebuild it from type.

Light mode

Near-black ink on transparent — for white, cream, and other light surfaces.

ExpiWell wordmark — light mode variant
PNG · 399×97 · transparentDownload .png

Dark mode

White on transparent — for the void slates and other dark surfaces.

ExpiWell wordmark — dark mode variant
PNG · 399×97 · transparentDownload .png

  Both files serve same-origin from /brand/. The light variant doubles as the site's Organization JSON-LD logo — its path never changes.

Built once. Themed five ways.

  ExpiWell brand system · v3.1 · WCAG 2.1 AA · 2026