Skip to main content

48. Visual system refinement — display type + soft elevation

Date: 2026-06-27

Status

Accepted

Builds on the ADR-0027 visual layer. No behaviour, IA, route, or copy changes — this is a primitive-level visual pass, so every page inherits it without per-page edits.

Context

A taste + high-end-design audit found the UI structurally sound (reserved status colours, mobile table-stacking, WCAG-AA focus, single-column forms) but visually flat and undifferentiated:

  • One typeface (Open Sans) for everything, so headings, body, and headline numbers read at the same visual weight. Dashboard widget titles used the text-xs uppercase tracking-wide "eyebrow" pattern, a recognisable templated tell.
  • Every surface used the identical border-slate-200 + shadow-sm card, so elevation never communicated hierarchy and the app read as a wireframe rather than a finished product.
  • Buttons changed colour on press but gave no tactile feedback.

These are quality gaps, not defects (the P0 a11y/token defects shipped separately).

Decision

  1. Two-face type system. Add Plus Jakarta Sans as a display face (--font-heading, loaded via next/font) for h1/h2/h3 and headline metrics. Open Sans stays the body face — it is the established brand body type, so this adds hierarchy without re-branding. Headline metrics also get tabular-nums tracking-tight so figures align and read as data.

  2. De-eyebrow content headings. Dashboard widget titles move from uppercase micro-labels to sentence-case text-sm font-semibold. (Sidebar group labels keep their uppercase treatment — that is functional nav grouping, not a decorative eyebrow above a headline.)

  3. Soft, slate-tinted elevation. Cards drop the hard border + flat shadow-sm for a hairline ring-1 ring-slate-900/5 plus a diffused, slate-tinted ambient shadow (--shadow-card). No pure-black drop shadows, no double-bezel nesting — restraint appropriate to a dense, trust-first product, not a marketing page.

  4. Tactile buttons. active:scale-[0.98] with a custom easing curve. Motion is feedback-only and collapses to instant under the existing global prefers-reduced-motion rule.

Explicitly rejected from the high-end-design playbook as wrong for this product: dark/OLED themes, mesh-gradient backgrounds, serif display, Awwwards-style asymmetric chaos, and status-colour-as-branding. Light mode, 60/30/10, and reserved status colours are unchanged.

Consequences

  • Tokens (--font-heading, --shadow-card, --shadow-card-hover) live in globals.css; primitives (Card, Button, PageHeader, DashboardWidget) consume them, so the change propagates app-wide from a handful of files.
  • New headings should use font-heading; new monetary/metric displays should use tabular-nums.
  • Deferred to follow-ups (not in this pass): shrinking the 128px app header and reallocating the brand-gradient budget, a shared <Money>/<Stat> provenance primitive, and an icon family to replace emoji glyphs.