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-smcard, 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
-
Two-face type system. Add Plus Jakarta Sans as a display face (
--font-heading, loaded vianext/font) forh1/h2/h3and 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 gettabular-nums tracking-tightso figures align and read as data. -
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.) -
Soft, slate-tinted elevation. Cards drop the hard
border+ flatshadow-smfor a hairlinering-1 ring-slate-900/5plus 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. -
Tactile buttons.
active:scale-[0.98]with a custom easing curve. Motion is feedback-only and collapses to instant under the existing globalprefers-reduced-motionrule.
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 inglobals.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 usetabular-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.