Internal reference · not part of the public site. This page loads apps/pwa/src/styles.css directly — every component below is rendered with the app's real, live CSS, not a copy. It's a reference to check new/edited screens against, not a spec for how things should change. If a screen doesn't match what's shown here, that's drift worth fixing; if you deliberately change a convention in styles.css, this page updates automatically since it's the same stylesheet.

Colors

Every color in the app is a CSS custom property on :root. Two groups: fixed brand/pillar accents, and theme-computed tokens that change per-user (page tint, panel appearance).

Fixed accents (pillar palette)
--teal — #2AD8C8 — Custom / Health --violet — #7C6CFF — Mind --amber — #FFBB5C — Money --green — #4ADE80 — Business --danger — #FF6B6B — destructive / over-budget

Defined once in presets.ts's PALETTE and mirrored as CSS vars in styles.css's :root — except the Habit pillar's rose accent (#FF6B9D), which today only exists as a JS value (applied inline per pillar), not a root CSS var. A pillar's color always comes from this set; don't pick an arbitrary hex for a new preset.

Page background
--bg1 — #0B1B3A — gradient start --bg2 — #241246 — gradient end

background: linear-gradient(160deg, var(--bg1) 0%, var(--bg2) 100%) on html and body — this very page's background is that exact rule, inherited from the linked stylesheet. In Settings → Appearance the user can pick a "Solid" background instead — theme.ts repoints both vars to the same solid color, so nothing that reads --bg1/--bg2 needs to know which mode is active.

Theme-computed (per-user, set by theme.ts)
--panel-color — user's chosen accent --panel --panel-hi --line

--panel-color is the user's customizable accent (set via the color picker in Settings) — it drives primary buttons, active tab rings, progress fills, links. Never hardcode a hex where this should be used; it's the one color in the app that's meant to change per user. --panel-text / --panel-muted / --btn-ink aren't swatches so much as computed contrast answers: theme.ts checks whether --panel-color (for --btn-ink) or the effective panel background (for --panel-text/--panel-muted) is light or dark, and picks black or white text accordingly. Use these vars for text on accent surfaces instead of assuming white.

Typography

Two typefaces, loaded once, with a strict division of labor.

Baloo 2 / 700 — h1, page title, capped 18px

Dashboard

Baloo 2 / 700 — large numerals (.stat-value)
$1,284.50
Nunito / 800 — .btn, .brand, tab labels
Nunito / 700 — .pill, .subnav-link, placeholders
Category
Nunito / 600 — body copy, inputs (.input/.textarea/.select)
Nunito / 400 — rare, long-form prose only

Used sparingly; most UI text is 600–800 weight, never 400.

Rule of thumb: Baloo 2 is for things that should feel like a heading or a big number (page titles, section titles, stat values, calendar day numbers). Everything else — buttons, body text, nav — is Nunito. Don't apply Baloo 2 to a button label or body paragraph.

Spacing & radius

A named scale, not pixel values — the class tells you the gap without doing math.

Margin scale
.mt-sm / .mb-sm — 8px .mt-md / .mb-md — 12px .mt-lg / .mb-lg — 16px .mb-none — cancels bottom margin
Row gaps
.row — 10px gap .row-tight — 6px gap
Radius
var(--radius) — 12px — panels, cards, inputs, stat-tiles 100px — buttons, pills, tags, tabstrip 50% — icon buttons, avatars

Only three radius values in the whole app: var(--radius) — 12px, used everywhere (panels, cards, inputs, stat-tiles, date-groups). 100px (fully round) for buttons, pills, tags, tabstrip. 50% for icon buttons and avatars. Don't introduce a fourth value.

Buttons

Pick the variant for the job instead of building a bespoke style.

The choice is purely about context, not emphasis: a button sitting directly on the page background (outside any .panel/.card) is .btn — the one filled, --panel-color button per screen. A button nested inside a .panel/.card/.modal-panel is always .btn-ghost, except a destructive action, which is always .btn-danger. There's no third color variant — don't reach for a bespoke style for a button on a colored surface.

Panels & cards

The two surface containers everything else sits inside.

.panel

Default content container — forms, detail screens, settings groups. 20px padding.

.card

Dashboard grid tiles, list items. 18px padding, hover lift.

Heads up.

.notice — smaller text for inline callouts (privacy notices, hints), full contrast since it's content someone actually needs to read.

All three redefine --paper/--muted to --panel-text/--panel-muted internally, so any text inside inherits a contrast-safe color against the panel's own background — this is what keeps text legible whether the panel sits on a dark gradient or a light custom page color. See Common mistakes for what breaks this.

Forms

One shared look for every input type.

No form field carries a separate visible <label> — the field's purpose is the placeholder text (.input/.textarea) or its selected option (.select, which has no placeholder concept). .field still wraps each control for consistent vertical spacing; it just no longer has label text as a child. .input, .textarea, and .select share padding, radius, border, and background — a new field type should reuse one of these classes, not invent new input chrome.

Pills, badges & tags

Small inline status/metadata chips.

🔥 4-day streak Paid Overview Budgets

.pill and .btn-sm deliberately share the same vertical padding/font-size so they sit flush at the same height when placed side by side in a row.

Stats & progress

Numeric summaries — budgets, streaks, time tracking.

Spent this month
$842
Over budget
$120
64%

.progress-fill uses --panel-color normally, and swaps to .over (--danger) only for an explicit over-budget/over-limit state — not as a general "warning" color elsewhere.

Rows & lists

Flex-row compositions used for list items and layout — combine modifiers instead of ad hoc inline flex styles.

Groceries −$64.20
Coffee −$5.50
Petrol −$71.00

Modifiers: .row-between (space-between), .row-end, .row-top (align-items: flex-start), .row-nowrap, .row-tight (6px gap). .entry-line adds the top divider and drops it on the first child.

Common mistakes

The recurring ways a screen drifts from the rest of the app.

Don't

Hardcode hex colors in component files. Every color that isn't a one-off illustration value already exists as a CSS var (--teal, --panel-color, --danger, …). A literal hex bypasses per-user theming and light/dark contrast handling.

Don't

Set text color manually inside a .panel/.card/.modal-panel/.menu-panel/.notice. These already redefine --paper/--muted to the panel-safe versions. Setting a fixed white (or var(--paper) from outside that container) reintroduces the exact white-on-light-panel contrast bug this pattern exists to prevent.

Don't

Assume white text on an accent surface. A user-chosen --panel-color can be light (e.g. pale yellow) or dark. Buttons and colored surfaces must use --btn-ink / --panel-text, which theme.ts computes per-color, not a fixed white or black.

Don't

Apply Baloo 2 to body text, buttons, or labels. Baloo 2 is reserved for headings and big numerals. Everything interactive or body-level (buttons, fields, pills, nav) is Nunito — mixing them in is the fastest way to make a screen feel off-brand.

Don't

Use one-off margin/padding values. Use .mt-sm/md/lg and .mb-sm/md/lg (8/12/16px) instead of inline one-offs — new arbitrary values make vertical rhythm inconsistent screen to screen.

Don't

Invent a new radius value. Only three radius values exist: var(--radius) (12px, panels/cards/inputs), 100px (pills/buttons/tabstrip), or 50% (icon buttons/avatars). A fourth value reads as a mistake, not a design choice.

Don't

Pick a button color by feel. It's purely contextual, not a design choice per button: outside any panel/card, use .btn (filled, primary). Inside a panel/card, use .btn-ghost — except a destructive action, which is always .btn-danger. There's no other variant to reach for.

Don't

Wrap a form control in a visible <label>. No field has separate label text. An .input/.textarea carries its label as its placeholder; a .select has no placeholder equivalent, so it carries none — its selected option is descriptive enough on its own.

Don't

Apply --muted to text someone needs to actually read. --muted (65% opacity) is for genuinely secondary micro-text — tab labels, timestamps, hex codes — not for body copy, notices, or explanations. Low-contrast prose is a legibility bug, not a visual-hierarchy choice; if it's more than a few words of real content, it's --paper.

Do

Compose row/spacing modifiers instead of new flex CSS. Before writing a new flex/justify-content block, check whether .row + a modifier (.row-between, .row-tight, …) already does it — most list/header layouts in the app are this same composition.