Impressions
Design Review · 2026-05-01
Eleven prioritized fixes from the foundation audit, shown side by side. Each "after" follows the rules captured in PRODUCT.md and DESIGN.md (restrained color, no side-stripe, no hero-metric template, paired display font, motion under 250ms with exponential ease-out).
The 3px border-left is an impeccable absolute ban. Replaced with a full-width tinted pill at brand-primary. Active state is unmistakable, the chrome is quieter, and the rule lives in one place.
DashboardLayout.tsx:137: border-l-[3px] on active. Side-stripe pattern is in active use across the codebase.
Full-width pill at --brand-primary, white label and icon. No left edge accent. Reads as "selected", not "decorated".
Four identical cards in grid-cols-2 lg:grid-cols-4 is the SaaS cliché impeccable specifically bans. The "after" elevates one anchor metric (Spend, with sparkline), groups the rest into a dense companion strip, and rolls the smaller diagnostics into an inline row. Same data, real hierarchy.
Total Spend
Conversions
Impressions
Clicks
OverviewPage.tsx:379. Four equal cards. No anchor, no rhythm, no story. Same layout reused on ReportingPage.
Total Spend · MTD
Conversions
Revenue
Impressions
Clicks
CTR
One anchor metric earns the size. Two supporting metrics ride alongside. Diagnostics fold into one row. Visual hierarchy carries the answer to "are we on track" in one glance.
Inter everywhere is the #1 AI-slop font tell. Pair it with a distinctive display face for headings (sample: Fraunces, a humanist serif with optical sizing). Body stays Inter. Hierarchy now carries character, not just scale. If the user does not have Fraunces installed locally, the system fallback (Iowan Old Style on macOS, Georgia elsewhere) still demonstrates the contrast.
Spend is up 12% vs last period. Two clients are pacing behind. One campaign is over budget by 8%.
Inter at all weights. Hierarchy via size only. Reads as default-ish, indistinguishable from any 2024 SaaS dashboard.
Spend is up 12% vs last period. Two clients are pacing behind. One campaign is over budget by 8%.
Display heading in a humanist serif (Fraunces / Iowan Old Style fallback). Body remains Inter. Heading carries voice; body stays neutral. The two together feel intentional.
Two motion gaps in one. Top: 9 modals snap in with no enter / exit; "after" uses a 200ms scale + fade with exponential ease-out (the existing dropdown-in keyframe at index.css:61-64). Bottom: ProgressDot color flashes between threshold states with no transition; "after" tweens fill / stroke over 320ms.
Connect Google Ads
You will be redirected to authorize Agency OS to read your account.
Modal pops in instantly · 0ms transition
9 modals (Connect, Welcome, CreateTarget, ManageMembers, InviteClient, CustomKpiBuilder, GenerateReport, ScheduleReport, SetOrgTargets) and ProgressDot SVG fill changes are all instant.
Connect Google Ads
You will be redirected to authorize Agency OS to read your account.
200ms scale (0.96 → 1) + fade · ease-out-expo
Modal: scale(0.96) → scale(1) + fade-in over 200ms. ProgressDot: transition: fill 320ms, stroke 320ms on the SVG primitives. Both watch the loop animation above to compare.
Most pages have one h1, then everything below is a styled div. Screen readers can't navigate by headings; SEO does not see structure; Reader Mode breaks. Same visual look, semantic markup.
Section titles are divs with title classes. Outline view (browser dev tools): one h1, no h2, no h3.
Same visual treatment via Tailwind classes; semantic h2/h3 underneath. Reader Mode works. Skip-to-section keyboard navigation works.
Currently the Reporting page reads as "Overview with filters". Same 4-col KPI grid, same density, same composition. The "after" introduces variation: a wide hero band on top, a 3-1 split row, then a 1-2 row. The page now has its own shape.
Four equal columns × four equal rows. Monotone. Indistinguishable from Overview.
Hero row (2-1-1) anchors the page. Wide chart band (3-1) follows. Filter strip (1-2) introduces a different scan rhythm. Reads as a different page.
Sidebar collapse uses p-1.5, ≈ 24×24px including the icon. Below WCAG 2.5.5 minimum and frustrating on tablet. The "after" pads the hit area to 44×44 without changing the icon size.
DashboardLayout.tsx:107: p-1.5. Misses on every other tap on iPad. Coral dashed line shows the visible target; user taps land outside.
Hit area expanded to 44×44 via padding. Icon stays at 14px. Apply same fix to nav items at narrow breakpoint.
Code-only fix; no visual change for end users, but a structural one. #f87171, #fbbf24, #4CAF50 appear inline in ProgressDot.tsx and elsewhere. Centralizing them lets dark mode, white-label, and accessibility overrides all swap in one place.
// ProgressDot.tsx:17-20 const dotColor = (pct, brand) => { if (pct < 50) return '#f87171'; if (pct < 75) return '#fbbf24'; return brand; };
Hex literals in three components. A status color change requires editing every site of use.
// index.css --color-status-behind: #F87171; --color-status-near: #FBBF24; --color-status-ahead: var(--color-primary); // ProgressDot.tsx const dotVar = (pct) => { if (pct < 50) return 'var(--color-status-behind)'; if (pct < 75) return 'var(--color-status-near)'; return 'var(--color-status-ahead)'; };
One source of truth. Dark mode and per-deployment color tweaks now possible without code changes.
Current rule covers custom keyframes (shimmer, bar-bounce, dropdown-in) but not the transition-* Tailwind utilities sprinkled on every button, chevron, and modal. A vestibular-sensitive user still sees animation.
/* index.css:81-87 */ @media (prefers-reduced-motion: reduce) { .animate-dropdown, .animate-toast, .loading-bars span, .skeleton { animation: none !important; } } /* button transitions, chevron rotates, modal backdrop fades all still run */
Partial. A user with motion sensitivity still sees all 150ms hover and chevron animations.
/* index.css */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }
Universal selector neutralizes both keyframes and transitions. The 0.01ms duration (rather than 0) preserves event firing for code that depends on transitionend.
"No team members assigned" is technically correct and emotionally flat. The "after" gives the empty state weight: a tinted icon block, display-font heading, a sentence with character, and a clear next action. Jhey-lens delight without crossing into mascot territory.
No team members assigned
ClientDetailPage.tsx:387-390. Restates the heading. No path forward. No voice.
Assign an account manager so they get the right alerts and pacing reports.
Assign team memberHeading does not restate the title. Sentence explains the consequence. Icon has dimension. CTA is the obvious next step.
After 1 through 10 land, run a polish sweep. The list below is what changes about the product's character, not its features. The score moves from 13 / 20 (Acceptable) toward 17 / 20 (Good).