This document reflects the current UI baseline in the codebase and was re-verified on March 24, 2026.
Use this as the visual source of truth for product-facing design decisions. For token definitions (primitive, semantic, component), see design-tokens.md. The bridge layer lives in src/app/globals.css.
Visual Direction
Pharos ships as a light-default financial dashboard:
- Dense data presentation
- Conservative card-and-table surfaces
- Small, meaningful color accents (risk, status, category)
- Heavy use of monospace for numeric trust and scanability
The default theme on load is light, with a user toggle for dark mode.
Light mode keeps the same hierarchy as dark mode, but status/accent text is calibrated one step darker to preserve readability on pale surfaces (typical pattern: text-*-700 dark:text-*-400).
Typography carve-out
Newsreader serif is reserved for the Daily Digest editorial surfaces: the /digest/** route and the homepage DailyDigest preview card. The detail-page AiSummary component uses Georgia serif (font-serif) for its AI-authored narrative paragraph — this is a second intentional carve-out. The root error boundary (page-error-editorial.tsx) shares the Georgia register for its kicker and title — deliberately not Newsreader, because error.tsx sits in every route's preload graph and importing the digest font there preloaded its CSS app-wide. The Cemetery obituary plaques (cemetery-tombstones.tsx) are a third Newsreader serif carve-out (Design Council B11), allow-listed in the design-invariants test alongside AiSummary. The /timeline/ route is a further carve-out: the mono token dominates the wire-service event stream (see ### Tape (Special) below). Every other dashboard panel on Pharos — including the homepage Market Snapshot, Core Monitoring band, Research Surfaces band, and all stablecoin-detail cards — uses the sans token at all weights. Do not introduce new serif usage outside the Digest, AiSummary, and Cemetery obituary carve-outs, and do not extend the Tape mono treatment to general analytics surfaces; a Vitest invariant in src/lib/__tests__/design-invariants.test.ts currently guards component-level drift under src/components/**, while route-level files still require manual review.
Masthead tagline
The SiteHeader tagline reads Every tracked stablecoin: backing, freeze risk, liquidity, and peg stress. It is exposed from md upward (not lg+-only as before) as a single whitespace-nowrap line — there is no line-clamp. Mobile (<md) shows the stat-pill card only: the sticky site chrome directly above already carries the brand lockup, so the masthead h1 is sr-only below md (the page still ships exactly one raw h1).
Hero signals rail (stablecoin detail)
On lg+, the detail hero's right column surfaces a four-pill HeroSignalsRail (Safety / Peg / Liquidity / DEWS) that quick-jumps to #report-card and #liquidity. It replaces the duplicated SafetyGradeHero block that used to sit opposite the Safety Score card. Mobile (<lg) continues to render SafetyGradeHero because the Safety Score card is far down scroll on narrow screens.
Breadcrumbs
src/components/breadcrumb.tsx is the shared visual breadcrumb primitive for bespoke deep-route surfaces such as stablecoin detail. Most feature and taxonomy routes use FeaturePageShell, which renders its own Dashboard / current page breadcrumb and emits BreadcrumbJsonLd from either the default breadcrumbName/path pair or an explicit breadcrumbItems override for N-level routes. For new standard feature pages, prefer FeaturePageShell; for bespoke deep routes, use Breadcrumb directly or consolidate the two renderers first.
Global App Shell
Root + Fonts
- Body classes:
pharos-font-sans pharos-font-mono antialiased - Sans font token: system-first Geist-style stack
- Mono font token: system-first Geist Mono-style stack
- Default corner radius token:
--radius: .5rem - Body background adds two subtle radial glow layers via
--page-glow-topand--page-glow-bottom
Layout Structure
Public pages use this shell:
<header className="lg:hidden sticky top-[3px] z-[56] border-b border-border/80 bg-background" style={{ boxShadow: "var(--elevation-rest)" }} />
{/* on core pages CoreTopRail (tape + submenu) follows the header in flow */}
<div className="flex min-h-screen">
<aside className="hidden lg:flex flex-col fixed top-[3px] left-0 h-[calc(100vh-3px)] border-r border-border/70 bg-card shadow-[0_0_0_1px_oklch(1_0_0_/0.03),0_20px_35px_oklch(0_0_0_/0.2)] z-[55] transition-all duration-200" />
<div className="hidden lg:block shrink-0 transition-all duration-200 w-[var(--sidebar-width-expanded)]" />
<div className="flex-1 flex flex-col min-w-0">
<main id="main-content" className="pharos-mobile-utility-safe flex-1 container mx-auto px-4 py-6 md:py-7 lg:px-6">
{/* route content */}
</main>
<footer className="border-t border-border/70 py-8 sm:py-10" />
</div>
</div>
Chrome Patterns
- Desktop sidebar widths:
--sidebar-width-expandedand--sidebar-width-collapsed - Mobile header height:
h-14 - Mobile utility dock: fixed bottom-right dock on
<640pxwith shared feedback + scroll-to-top placement; the dock stays hidden until the first scroll so it does not cover top-fold content - Main content and footer reserve bottom safe space via
pharos-mobile-utility-safe+--mobile-utility-safe-offset - Main container padding:
- Mobile:
px-4 - Vertical rhythm:
py-6(md:py-7) - Desktop (
lg):px-6
- Mobile:
- Footer now prioritizes a short list of core routes, keeps category browsing secondary, drops duplicate tagline text beside socials, and lets intro/legal copy breathe across wider lines.
- The footer intro block is not width-capped inside its header row; on larger screens it expands to fill the available column beside the social icons.
Page Shell Variants
Standard Analytics Pages
Most routes use:
- Wrapper:
space-y-6 - Title block:
space-y-2.5 - Breadcrumb:
flex items-center gap-1.5 text-xs text-muted-foreground sm:text-sm - Title row outer layer:
flex max-w-full flex-wrap items-start justify-between gap-x-3 gap-y-3 - Title row inner text/action layer:
flex max-w-4xl flex-wrap items-center gap-x-3 gap-y-2
Longform Pages
- Privacy:
mx-auto w-full space-y-6 max-w-2xl - Methodology:
mx-auto w-full max-w-[76rem] space-y-8 - Digest archive:
mx-auto max-w-4xl - Digest detail shell:
mx-auto max-w-4xl, with editorial body copy constrained tomax-w-[68ch]
Start Here (Special)
The /start/ orientation route keeps the shared breadcrumb/title shell, then shifts into a broader planning-board layout:
Behavioral contract: Start Page
- Wrapper:
mx-auto max-w-6xl space-y-8 - Hero shell: large rounded plotting-board surface with editorial onboarding copy on the left and a route board on the right
- Route board: uniform goal cards in
sm:grid-cols-2 xl:grid-cols-3 - Mobile top fold compresses the hero copy so the first route card stays visible above the fold
- Desktop hero keeps the route board on the right while the left column stacks headline copy and
HeroEscapeHatch - Desktop support content under the headline stays in a clean vertical stack: CTA row first, then the experienced-user note
- Follow-up sections use glossary cards, flattened feature-atlas groups, and shortcut cards instead of one long prose stream
Home Dashboard (Special)
Home keeps a single visible page h1 owned by SiteHeader; the rest of the top fold is composed of:
Behavioral contract: Homepage
- Masthead strip:
pharos-card-shell flex flex-col gap-2 px-3 py-2 sm:gap-2.5 sm:px-4 sm:py-2.5 md:flex-row md:items-center md:justify-between md:gap-6 md:px-5 md:py-3— stacked on mobile, side-by-side frommdupward - Core top rail: the live tape is mounted directly below the global PSI
RegimeBaron core pages, with a horizontal nav strip underneath for Dashboard, Safety Scores, Depeg/DDR, FreezeWatch, Alt-Pegs, Yield Intelligence, Stability Index, and PharosWatchBot. On desktop, the combined tape + nav rail is sticky attop-[3px]withz-50, below the fixed PSI strip (z-[60]) and below the desktop sidebar (z-[55]), so both elements persist while scrolling without covering the sidebar search. On mobile, the sticky site header (top-[3px],z-[56]) renders first in the chrome stack; the nav strip pins beneath it attop-[calc(3px+3.5rem)](z-[55]) while the events tape remains in normal flow and scrolls away. Each item renders its Lucide icon (aria-hidden); the active item's icon is littext-frost-blue, inactive icons aretext-muted-foreground/80. The active item is a self-contained frost-lit pill (.pharos-rail-tab-active) — the same frost wash + hairline inset ring + soft halo recipe as.pharos-nav-active, scaled for a small inline chip — with a.pharos-nav-beamsweep on activation and a faint frost-tinted ground (.pharos-rail-ground) below the tape. No left-edge accent stripe. The tape and nav strip both start at the active sidebar width on desktop and center the nav run within the remaining viewport when it fits ([justify-content:safe_center]), with left and right edge gradient fades for overflow and the active pill scrolled into view on route change. While a core page is active, the sidebar suppresses duplicate core shortcuts and keeps Dashboard as the only core entry there. - Page Discovery module: production-style five-link route board under the digest preview. It keeps the analytics
pharos-card-shelltreatment and the older product-callout tile rhythm, but the content is randomized from Core, Track, Analyze, and Monitor pages. A slimChart your routeheader strip (frost-blue mono kicker + warm subline + route count) titles the board; below it sit one larger spotlight tile with the full route description and four compact tiles with one-line short descriptions, semantic accent blocks, and Lucide route icons. The spotlight kicker is a live-dotSpotlightaccent chip; each compact route carries its nav-group accent as a category chip (text mixed toward--foregroundfor legibility). Compact descriptions may wrap to two lines atxl, where the four minor tiles are narrowest. Hover/focus tints the tile toward its--discovery-accent, lifts the icon tile with a soft accent ring, and reveals anArrowUpRightin the tile's top-right corner (the spotlight tile keeps its trailing inline arrow). The five tiles fade-and-rise on mount viapharos-stagger-entrance(spotlight first, routes cascading), disabled underprefers-reduced-motion. No edge accent stripe. - Snapshot shell: PSI-dominant first card + four supporting desktop KPI panels; mobile and tablet collapse to a 2x2 compact tile grid that includes net mint/burn flow
- Snapshot PSI lead card always renders the three compact delta pills (
24h,7d,30d) beside the score/band lockup - Digest preview: broadsheet split with a mono masthead, hairline
Executive Summarylabel, newspaper-styleNewsreadertitle on the left, and the lead paragraph plus CTA rail on the right at desktop. The lazy boundary may reserve space before mount, but the loaded preview itself stays content-height so the page-discovery board follows without a dead desktop band. - Upcoming horizon module: a server-rendered
On the Horizonpanel below the main stablecoin board (home-alt-upcoming-horizon-constellation.tsx) that summarizes the pre-launch universe. It keeps the analyticspharos-card-shelltreatment with a frost mono kicker and an approach rail, but represents phases with the actual upcoming coin logos instead of count-only badges. Atxl+each stage is a circular constellation on its own phase-colored tinted disc (PHASE_FIELD, hues mirroringPHASE_DOT) so the five zones read as distinct; the disc diameter scales with coin count (apackCirclepacker lays ≤6 coins as a single polygon ring and 7+ as a center star wrapped by concentric rings spaced one logo apart), so Announced is the largest cluster and sparse stages are small pairs. All discs center on the brightening horizon beam, with the frost-lit launching-soon threshold carrying an extra glow; labels sit below with aPHASE_DOTchip and count. Belowxlthe phases stack as full-width labelled lanes whose logos wrap. Each logo links to its detail page, the module links to/upcoming/, and there is no separate nearest-launches strip or edge accent stripe.
Stablecoin Detail (Special)
Active detail pages keep one server-rendered semantic h1 for crawlers and assistive tech, while the visible identity lives in the client hero:
- Server
h1:sr-only - Client HeroCard mobile
h2:text-2xl font-black tracking-tighter - Client HeroCard desktop
h2:text-3xl font-black tracking-tighter - Section and block titles across the detail route use
text-lg font-semibold tracking-tight - Detail metadata badges that qualify a section title (for example liquidity source coverage) sit inline with the title instead of dropping onto a separate row
- The
Contract Deploymentsblock shows a one-row, six-item icon preview on mobile with aShow alltoggle;sm+renders labeled rows (chain logo + name link + truncated address + copy + explorer) in a 1/2/3-column grid with a nine-row preview and its ownShow alltoggle — the bare icon wall was retired in the June 2026 mythos pass (recognition fails past the top-10 chain logos) - A "verification passport" strip docks at the bottom of the hero card behind a hairline
border-t: identity-document fields in two scan clusters — how the token works (Mechanism, Redeemability, Minting, Freeze, Record, Chains), then who stands behind it (Jurisdiction, MiCA, GENIUS, Attestor, Issued) — with the field name in small muted letters above a mono all-caps value, each linking to the section that proves the fact. Flat document fields, not pills; data-driven text tones only (attestor tier ladder viaPOR_TIER_STYLES.textCls, freeze amber/emerald); snap-scroll carousel belowlg, edge-to-edge distributed wrap (justify-betweenat ≥6 facts) onlg+. Seedocs/stablecoin-detail-page.md### Hero passport stripfor the field/anchor contract. LongformScrollspyNavrenders as a sticky horizontal pill banner belowlg; onlg+, the same section model moves into a sticky right-side rail withvariant="rail"- A single
Explore Nexthub at the end of the page, replacing the older stack of repeated research/compare/related link grids with one consolidated crawlable route cluster
This is intentionally denser than standard feature pages.
Digest Article (Special)
Digest entries use a distinctive "intelligence briefing" editorial aesthetic that deliberately departs from the standard Geist-based UI:
h1: Newsreader display face viadigestDisplay.classNamewithtext-[clamp(2.2rem,5vw,3.5rem)] font-semibold leading-[0.92] tracking-[-0.04em]- Executive summary card ahead of body copy
- Editorial prose constrained to
max-w-[68ch] - Homepage digest preview switches to a split desktop layout so the title block and italic executive-summary paragraph can use the full container width; dedicated digest pages keep the
max-w-[68ch]editorial measure.
Editorial Typography System
The digest feature employs a dual-font hierarchy that evokes newspaper headlines over wire-service dispatches:
| Element | Font | Rationale |
|---|---|---|
| Headlines | font-serif + route-local Newsreader usage where needed | Editorial authority — magazine headline gravitas |
| Body copy | Courier New italic | Raw urgency — telegrams, terminals, raw intel |
| Metadata | Courier New upright | Systematic precision — timestamps, edition numbers |
This pairing creates a "broadsheet newspaper" aesthetic that signals both authority and real-time urgency. It is one of three intentional non-Geist text treatments in Pharos, alongside the stablecoin-detail AiSummary Georgia serif paragraph and the /timeline/ wire-service stream documented in ### Tape (Special) below.
Implementation: Import styles from @/lib/digest:
EDITORIAL_BODY_STYLE— Courier italic for proseEDITORIAL_META_STYLE— Courier upright for labels
Tape (Special)
The /timeline/ event stream uses a deliberate wire-service / terminal aesthetic that diverges from the standard pharos-card-shell analytics surface. Where Digest is the broadsheet, Timeline is the syslog: mono-token typography everywhere, hairline dividers in place of card chrome, severity expressed as text color, per-class background tints (hue signals class, text-color signals severity), and row time prefixes that use HH:MM on larger screens and compact relative tokens on mobile.
This is a third intentional non-sans-token treatment alongside the Digest dual-font system (Newsreader serif + Courier italic) and the stablecoin-detail AiSummary Georgia paragraph. Tape is distinct from both: it leans on the mono token as the primary typeface across the stream, not serif for editorial gravitas.
The absence of pharos-card-shell on event rows, day groups, the currently-open band, pinned linked-event block, and the filter row is intentional, not an oversight. The filter row is a flat wire-control surface with hairline border-y dividers and shared control primitives, not a card shell.
The canonical contract — rules, structured row layout, day-separator format, and the Aesthetic Lock against harmonization — lives in tape-page.md under ## Visual Identity and ## Aesthetic Lock. Update both docs together when the wire-service treatment changes.
Cemetery (Special)
The Stablecoin Cemetery (/cemetery/) employs a unique memorial aesthetic that is intentionally divergent from standard Pharos UI patterns:
- Tombstone visualizations: Custom SVG-based tombstones with varying shapes (arch, hammer, cross), sizes (by peak market cap), and weathering effects (by age)
- Theme-aware memorial palette: Uses bespoke stone/zinc accents and cause colors for the memorial atmosphere, while tombstone SVGs and cards still adapt through semantic CSS variables and light/dark Tailwind classes
- Cause-of-death color system: Algorithmic failure (red), counterparty failure (amber), liquidity drain (orange), regulatory (blue), abandoned (zinc)
- Interactive memorial: "Press F to pay respects" with persistent flower accumulation
This is a one-off artistic treatment — the patterns are not intended for reuse on other pages. The bespoke memorial colors and components serve the specific narrative of memorializing failed stablecoins.
Typography
Heading Scale
| Role | Live class pattern |
|---|---|
| Standard page title | min-w-0 text-3xl sm:text-4xl font-extrabold tracking-tight leading-[1.05] |
| Shared page title utility | pharos-page-title |
| Digest article title | Newsreader via digestDisplay.className, text-[clamp(2.2rem,5vw,3.5rem)], font-semibold, leading-[0.92], tracking-[-0.04em] |
| Homepage digest hero | Newsreader, font-semibold, text-[clamp(2.8rem,6vw,5rem)], leading-[0.88], tracking-[-0.045em] |
| Home logotype label | text-sm font-mono font-semibold uppercase tracking-[0.14em] md:text-[1.02rem] md:tracking-[0.16em] |
| Primary section heading | leading-none font-semibold |
| Secondary section heading | text-lg font-semibold or text-lg font-semibold tracking-tight |
| Table/section kicker | text-[12px] sm:text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground |
| Subsection heading | text-foreground font-medium |
Body + Supporting Text
| Role | Live class pattern |
|---|---|
| Standard body copy | text-sm text-muted-foreground |
| Shared lead copy | pharos-lead |
| Small metadata | text-xs text-muted-foreground |
| Shared metadata | pharos-meta |
| Card micro-labels | text-xs uppercase tracking-wide |
| Footer legal group | flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-muted-foreground |
Contrast floor: informational text and meaningful icon affordances never drop below text-muted-foreground/70 — lower alpha lands under the 3:1 contrast ratio on the light theme (WCAG 1.4.3/1.4.11). Alpha below /70 is reserved for decorative aria-hidden separators (·, /, rules), disabled controls, loading skeletons, and chart internals that carry their own theme tuning.
Numeric Language
Numbers are consistently mono/tabular where precision matters:
font-monotabular-nums
Spacing and Layout Rhythm
Common Vertical Rhythm
- Section rhythm:
space-y-6 - Header block rhythm:
space-y-2.5 - Longform rhythm:
space-y-8 - Card prose rhythm:
space-y-6 text-sm text-muted-foreground leading-relaxed
Common Grids
- KPI grid (dense analytics):
grid grid-cols-2 gap-3 sm:gap-5 lg:grid-cols-5 - Home feature grid:
grid grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-5 - Home snapshot desktop partition:
hidden lg:grid grid-cols-[minmax(0,1.1fr)_repeat(4,minmax(0,0.92fr))] divide-x divide-border/30
Chip/Pill Layout
- Chips are frequently wrapped in
flex flex-wrap gap-2 - Category links and peg links prioritize
rounded-fullmicro-surfaces.
Onboarding / Access Surfaces
First-run compare, portfolio, and gated status states now share a structured onboarding surface:
- Large rounded shell with dark gradient backdrop
pharos-kickereyebrow + one decisive title- 3 step explainer cards
- CTA row using rounded-full buttons
- Preview panel shell on the right at desktop, stacked on mobile
- Optional footnote/support panel at the bottom of the text column
The dedicated /start/ route extends the same language into a full-page onboarding pattern:
- large hero shell with route-selection cards instead of a single preview panel
- compact fact blocks embedded in the copy column
- glossary cards beneath the hero
- flattened feature-atlas groups plus shortcut cards for optional progressive discovery
Shared Utility Classes
Live production now leans on a broader shared utility layer for finish-level consistency:
pharos-kickerpharos-focus-ringpharos-card-shellpharos-interactive-cardpharos-page-titlepharos-leadpharos-section-titlepharos-metapharos-control-pill/pharos-control-pill-activepharos-toggle-pillpharos-chart-stagepharos-chart-legend-chippharos-table-shellpharos-table-toolbarpharos-table-sticky-primary/pharos-table-sticky-metricpharos-panel-headerpharos-subtle-bandpharos-empty-note
Current high-use areas:
- Homepage snapshot and explore cards
- Peg filter pills
- CTA links with custom focus treatment
- chart legends, chart stages, and comparison controls
- stablecoin/comparison table wrappers and toolbars
Shared Tables
Use the Pharos-owned table primitives in src/components/table/ for visible product tables instead of raw <table> markup or direct shadcn Table composition. The default @/components/table barrel is server-safe; client-only affordances such as settings menus and source links live under @/components/table/client. The primitives preserve semantic table slots without shadcn's nested overflow wrapper, so table shells, viewport overflow, density classes, striping, sticky headers, test ids, and row naming stay consistent.
Current component roles:
| Component | Use when | Notes |
|---|---|---|
TableFrame | Standard children-first row tables, status/admin tables, embedded detail tables, and static content tables | Keep route-specific rows/cells as children; pass tableId for stable selectors and fallback table naming; data-testid defaults to ${tableId}-table. |
DataTableShell | Sortable/paginated analytics tables that already have column descriptors and custom row children | Compatibility layer over TableFrame; still supports route-owned top slots and pagination. |
MatrixTable | Comparison/coverage matrices with sticky row headers or metric columns | Do not force matrix behavior into row-table APIs. |
VirtualTableFrame | Virtualized large tables such as the homepage stablecoin overview | Keep domain-specific row rendering, preference keys, and actions in the route wrapper; share only the shell, viewport, and table element. |
TableToolbarFrame | Generic table toolbar layout with title/meta/actions | Use this before creating route-local toolbar chrome. |
TableControlsToolbar | Shared density/settings/export toolbar for client tables | Import from @/components/table/client; pass route-specific column pickers through columnsSlot. |
TableSettingsMenu | Optional density, column, or custom table settings popovers | Import from @/components/table/client; column visibility remains caller-owned. |
TableSkeletonRows | Loading rows that preserve table semantics and density classes | Use inside TableFrame/VirtualTableFrame rather than standalone div.pharos-table-shell skeletons. |
TableSourceLink | External source links inside table cells or row details | Import from @/components/table/client; keeps focus, truncation, external-link icon, and optional row-click propagation behavior consistent. |
Content/reference tables should stay static: no sorting, export, toolbar, or pagination unless the route explicitly needs those controls. Tables with captions keep caption-based accessible names; unnamed framed tables derive a fallback label from tableId. Accessibility-only chart data tables may remain specialized.
Contextual Explainability
Computed metrics now use a shared contextual-methodology pattern instead of relying only on page-level intros or /methodology as a separate destination.
Current pattern:
- compact help trigger attached directly to the metric label
- desktop behavior: click-to-open popover with short definition + methodology links (a popover, not a tooltip, so keyboard focus can reach the links)
- mobile behavior: bottom sheet with the same content
- score-card footers may add
View methodologyandVersion historyactions when the surface is interpretation-heavy
Use this on:
- composite scores (
Safety Score,Liquidity Score,PYS,DEWS) - baseline-relative or Pharos-native signals (
Pressure Shift vs 30D,Bank Run Gauge) - opaque sub-dimensions where the label alone is insufficient (
Resilience,Dependency Risk)
Do not use this on every metric indiscriminately. The trigger is reserved for values where local interpretation meaningfully improves user decision-making.
Cards
Base Card Primitive
Default card composition in production:
data-slot="card"bg-card text-card-foreground flex flex-col gap-4 rounded-xl border py-4 shadow-sm- card surfaces now inherit a subtle shell gradient plus top-left highlight through the global token bridge instead of relying on flat fills alone
pharos-card-shellis the promoted authored-surface wrapper for major route modules, tables, and feature cards
Card Header + Title
- Header:
@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-4 - Tight variants add
pb-1,pb-1.5, orpb-2 - Titles: mostly
leading-none font-semibold - Shared route and chart surfaces increasingly use
pharos-panel-headerfor a restrained header band instead of ad-hoc muted strips
Accent Border Palette (Live)
The decorative per-card colored left stripe (border-l-[3px] border-l-*-500) has been retired from analytics, discovery, and methodology surfaces (May 2026 harmonization). KPI stat cards (MetricStatCard with no borderColorClass), feature/section cards (/about, /funding, /methodology and its changelogs), the /status sections, the /liquidity, /depeg, /freezewatch heroes, the /cemetery autopsy box, the digest snapshot cards, and the stablecoin-detail Yield Intelligence + blacklist cards now render as flat homepage-style cards (rounded-xl border bg-card shadow-sm, no colored edge, neutral pharos-kicker eyebrows).
border-l-[3px] (with a semantic color) remains reserved for data-driven indicators, not card chrome:
- depeg table row severity accents (
rowAccentClass→border-l-red-500/border-l-orange-500/border-l-amber-500) - stablecoin-detail hero metric accents (
accentClassinhero-card-metrics) - internal admin status sections (
StatusSectionstill accepts an optionalaccentClassName) - stablecoin-detail per-coin notices (
coin-notice.tsx): the danger/warning/info alert stripe is severity-keyed data, deliberately kept through the June 2026 mythos review and normalized to the same 3px weight
The desktop sidebar navigation active state no longer uses a left stripe (June 2026 "watch column" pass). It is now a frost lit-tab — a frost wash falling from the icon side, a hairline frost inset ring, a soft halo, and a frost-lit icon — defined by .pharos-nav-active in globals.css. See ### Navigation Active vs Inactive below. The mobile drawer (header.tsx) still uses border-l-2 border-l-frost-blue on the active route group as its own treatment.
Interactive Card Pattern
pharos-interactive-card is the richer hover-lift utility used on the about-page feature grid. Homepage callouts currently stay on lighter pharos-card-shell variants without the extra interactive-card class. Following the May 2026 harmonization these surfaces no longer carry a colored left stripe.
className =
"pharos-card-shell pharos-focus-ring pharos-interactive-card group flex flex-col gap-2 bg-gradient-to-b from-background/40 to-transparent p-4";
Logo Containers
Tracked token logos now render inside a shared neutral container:
rounded-full border border-border/60 bg-background/80- subtle inset highlight
- image shrunk slightly inside the wrapper so transparent/low-quality upstream assets do not collapse into the page background
Badges and Chips
Version Badge
Secondary version pill:
bg-background/35 text-muted-foreground border-border/60
Micro Chips
Common chip form:
inline-flex items-center rounded-full border bg-background px-2.5 py-1 text-xs font-medium hover:bg-accent transition-colors
Control Pills
The preferred finish-level control language is now the shared pill system:
- base:
pharos-control-pill - selected:
pharos-control-pill pharos-control-pill-active - used on time-range controls, density toggles, lens pills, and lightweight route context summaries
- pills should feel dense and precise, not marketing-chip playful
pharos-control-pill is the canonical small-control shell for any dense, secondary action surface — defined in src/app/globals.css line 499. Following the May 2026 detail-page pass, this includes the hero tertiary metric chips, per-section freshness stamps, and the longform scrollspy. (The former chains/freezable hero pills were absorbed into the flat hero passport strip in June 2026 — see ### Stablecoin Detail (Special).) New surfaces should reach for this utility before constructing ad-hoc rounded-full button shells.
Proof-of-Reserves Attestor Tier Ladder
POR_TIER_STYLES in shared/lib/classification/badges.ts defines a 5-tier categorical color ladder used for the per-coin proof-of-reserves badge on detail pages. The ladder maps directly to AttestorTier from shared/types/core.ts:
| Tier | Color | Token classes | Meaning |
|---|---|---|---|
big4 | emerald | bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-500/30 | Big-4 firm independent attestation |
regional | blue | bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/30 | Licensed regional CPA / auditor |
niche | muted / neutral | bg-muted/40 text-muted-foreground border-border/60 | Single-jurisdiction or small-practice attestor |
self | amber | bg-amber-500/10 text-amber-700 dark:text-amber-400 border-amber-500/30 | Issuer self-attestation, no third-party signoff |
none | red | bg-red-500/10 text-red-700 dark:text-red-400 border-red-500/30 | No attestation surface published |
This is the canonical 5-tier categorical ladder for evidence-quality badges. Reuse POR_TIER_STYLES rather than redefining the palette inline. The ladder degrades cleanly to a 3-tier emerald / amber / red flatten for severity-style surfaces; do not introduce a competing "audit quality" palette.
Freshness Stamps
FreshnessIndicator from src/components/status/freshness-indicator.tsx is the canonical "Updated X ago" affordance across the dashboard. It computes age client-side from a updatedAtMs prop, switches into a stale tone once staleAfterMs is exceeded, and pauses ticking while the document is hidden. As of the May 2026 detail-page pass it also renders inside the Safety Score card header on the stablecoin detail route, paired with the per-card pharos-control-pill chrome.
When adding a new freshness stamp:
- always pass
updatedAtMsfrom the originating cache snapshot, notDate.now()at render - match
staleAfterMsto the producer cron interval (see CLAUDE.md hook timing rule) - prefer the small inline form inside
CardHeader; avoid stacking a new "last updated" line of body copy on the same surface
Tables
Base Table Styling
- Table:
w-full caption-bottom text-sm - Major table surfaces should prefer
pharos-table-shellover a plain rounded border wrapper - Toolbars should prefer
pharos-table-toolbarwith a brief explanatory line rather than a bare row of buttons - Row:
hover:bg-muted/40 data-[state=selected]:bg-muted border-b transition-colors - Table rows now also take a subtle left-edge accent and a small horizontal nudge on hover; risk rows preserve their own semantic border color on hover
Header Variants
- Standard header:
[&_tr]:border-bwith a theme-aware header band and token-driven shadow - Sticky directory header (peg pages): sticky top header with restrained blur and
--table-header-shadow - Stablecoin detail history tables (depeg + mint/burn) use a rounded bordered shell (
rounded-xl border overflow-hidden) with the muted header treatment and a footer row pairing mono range copy with outlinePrevious/Nextcontrols
Mobile Directory Table Handling
- Toolbar becomes a vertical stack on mobile instead of a cramped inline row
ColumnsandExport CSVkeep large tap targets on mobile; density and range controls also stay pill-based instead of collapsing into tiny tabs- Density controls now include a true
Listmode for ticker-first scanning; in that mode the stablecoin table suppresses the expanded coin name and keeps only the ticker lockup - Table keeps a deliberate horizontal-scroll affordance via helper copy and a dynamic inline min-width: the sum of per-column content minimums (
COLUMN_MIN_WIDTH_PX) for the visible column set, with a 420px floor. The viewport'soverflow-x-autoself-degrades — no scrollbar when the columns fit, horizontal scroll when they don't — so fixed-layout cells never squeeze below content width. The mobile/desktop column boundary isxl(1280px); the 7d sparkline renders from2xlup. Belowxlthe price column is pinned to its content width (w-[88px]) so fixed-layout leftover sharing cannot inflate it past the 390px first viewport and clip the fourth peg-price decimal at rest - Bottom spacing is preserved so the mobile utility dock never sits on the last visible rows
Sortable Head Pattern
Sortable heads consistently include:
cursor-pointerhover:bg-muted/50 transition-colors
Numeric columns remain right-aligned (text-right) and collapse progressively by breakpoint (hidden sm:table-cell, hidden md:table-cell, etc.).
Clickable Rows
Interactive rows use:
group cursor-pointerfocus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none
Charts
Live Chart Container Pattern
- Height:
h-[250px] sm:h-[350px] - Recharts container keeps
min-width: 0; min-height: 0 - Chart-heavy home modules now reserve height through matching skeletons or client-ready mount guards before
ResponsiveContainerrenders - Premium chart framing now uses
pharos-chart-stage: a dedicated bordered stage inside the card, rather than letting charts float directly on the card background - Legends should prefer compact chips (
pharos-chart-legend-chip) when the chart needs persistent series context outside the tooltip
Axis + Grid (Observed)
From production rendered charts:
- Tick text:
font-size: 12,font-family: var(--font-mono, monospace),fill: var(--color-muted-foreground) - X axis keeps extra breathing room through
tickMargin={10} - The shared
MonoYAxisprimitive defaults towidth={68}andtickMargin={8}for cleaner number alignment - Grid lines:
stroke="var(--color-border)",strokeDasharray="2 6", verticals off by default
Area Chart Styling (Observed)
- Areas use gradient fills (e.g.
fill="url(#psiScoreGradient)") - Stroke widths are typically
1.5or2 - Tooltips should use the shared elevated card treatment (
PharosChartTooltip) with uppercase label treatment and mono values
Loading Fallbacks
Common chart skeletons:
rounded-lg bg-muted/30 animate-pulse relative overflow-hidden h-[250px] sm:h-[350px] w-fullbg-accent animate-pulse h-[250px] sm:h-[350px] w-full rounded-xl- Blacklist hero chart uses
h-[220px] sm:h-[280px] - Yield scatter plot uses
h-[420px](compact) orh-[600px] sm:h-[850px](full) inside a bordered chart stage
Interaction and State Patterns
Navigation Active vs Inactive
The desktop sidebar (src/components/sidebar.tsx) frames navigation as a lighthouse "watch column": the active route reads as lit by the beam rather than flagged by a left stripe.
- Active sidebar item:
pharos-nav-active— a frost wash falling from the icon side, a hairline frost inset ring + soft halo (no border stripe),text-foreground, and the Lucide icon littext-frost-blue. The row also mounts a one-shotpharos-nav-beamlight sweep on activation (gated onprefers-reduced-motion: no-preference). - Inactive sidebar item:
text-muted-foreground hover:bg-muted/50 hover:text-foreground(no left border at any state). - When a live signal supplies an
accentClass(the/stability-index/PSI band tint), the band background composes beneathpharos-nav-active, so an active item shows frost light on the icon side with the regime band persisting to the right. - The brand lockup casts a thin frost shaft (
pharos-brand-beam) along the header divider; the Search row is a bordered inset command field, not a nav link, and brightens its border toward frost on hover.
The core top rail (src/components/core-top-rail.tsx) is the horizontal echo of the watch column. Each destination is a .pharos-rail-tab ghost chip; the active item gains .pharos-rail-tab-active, which reuses the exact frost color-mix recipe from .pharos-nav-active — frost wash + hairline frost inset ring + soft halo — scaled for a small inline pill rather than a full-width sidebar row. The active pill also mounts the .pharos-nav-beam one-shot frost sweep on activation (reduced-motion gated), and the nav bar sits on a very faint frost-tinted .pharos-rail-ground to distinguish it visually from the neutral-card live ticker tape directly above it. There is no left-edge accent stripe, consistent with the May 2026 harmonization and the June 2026 watch-column pass.
The mobile drawer (header.tsx) keeps its own active treatment (border-border/70 bg-muted/60 links, border-l-2 border-l-frost-blue group accents) and was intentionally left unchanged in this pass.
Focus Treatment
Two dominant focus patterns:
focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-nonefocus-visible:ring-[3px] focus-visible:ring-ring/50
Loading States
- Skeletons are the default loading surface (
data-slot="skeleton"+animate-pulse) - Page-level loader currently appears as:
flex min-h-[40vh] items-center justify-centerh-10 w-10 rounded-full bg-frost-blue/30 animate-pharos-pulse
Live/Event Indicator
Depeg live indicator uses animated ping:
animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75
Data Availability Banner
When data streams are missing:
rounded-md border px-4 py-2.5 text-sm border-border/60 bg-muted/40 text-muted-foreground
The current pattern is a titled trust banner with dataset-specific copy, for example:
Waiting for initial dataAffected: report cards.Last successful update: Mar 24, 1:01 PM GMT+1.
Responsive Behavior
Breakpoint Behavior in Production
sm:- Compacts/expands table columns
- Converts details/nav patterns
md:- Masthead tagline becomes visible as a single
whitespace-nowrapline. - Snapshot KPI grid expands; other dense-data grids transition between mobile and desktop layouts
- Masthead tagline becomes visible as a single
lg:- Sidebar becomes active (
lg:flex); mobile header / drawer hides (lg:hidden) - Tablet portrait (sub-
lg) intentionally falls back to the mobile drawer because the desktop nav has too many groups to remain legible at that width - Main horizontal padding increases (
lg:px-6) - Larger grid splits and extra table columns
- Sidebar becomes active (
xl:- Additional dense table columns
- Home KPI grid keeps the wide five-panel snapshot module intact
Mobile-Specific UX
- Category browse collapses into
details(sm:hidden) --table-header-top: 56pxis set on mobile for sticky header offset alignment- Bottom utility controls are consolidated into one dock on mobile instead of separate floating widgets
Accessibility Baseline
Live app-wide patterns:
- Skip link present on every page:
sr-only focus:not-sr-only ... - Breadcrumb navigation on content routes
- Focus-visible rings on sidebar links, buttons, table rows, and chips
- Keyboard-ready clickable rows on interactive tables
- Color is reinforced with structure and iconography for key status states
Draw the Metaphor
When a page introduces a metaphor, render it — don't just name it. The Stablecoin Cemetery draws actual tombstones with arched caps, crosses, and flower scatter. The Alt-Peg Atlas draws a starfield with celestial bands and constellation cohorts. The Chains Harbor Chart draws ships with flags, cargo, wakes, and depth lines. On /depeg/, the Depeg Duration Resolver (DDR) draws each open event as a forecast timeline — the deviation path so far (peak → now spark), the verdict at a pulsing NOW marker, and the projected resolution band (median diamond + IQR over the 6h/24h/7d/30d landmark axis) reaching into the future — while its Reviewer (DDRR) draws a track-record timeline where every graded past call seats above the rail (correct) or below it (miss), so accuracy reads at a glance. The kill-vs-anchor tug-of-war and the DDRR calibration ledger remain beneath these as the "why" and the honesty check.
Rules that keep this from drifting into decoration:
- Every shape encodes a data field. No ornamental geometry. If a shape doesn't vary with a number, remove it.
- Inline JSX SVG, semantic CSS variables, hex fallbacks. No external
.svgassets, no runtime SVG libraries. - CSS keyframes only, gated on
@media (prefers-reduced-motion: no-preference). No framer-motion. - Mobile preserves the metaphor, not feature removal. Visualization canvases should fit their container without page-level horizontal scrolling; use simplified lists only when the visual cannot remain legible.
- The underlying data table remains. The metaphor is a hero. The table is the workbench.
Maintenance Rule
If a deployed class pattern changes in production, update this document immediately after release. This file is intended to describe what users currently see, not aspirational or historical styles.