16 KiB
Design Critique — Goodwalk
Scope: read variables.css end-to-end, ValuesSection.svelte (open in IDE), buttons.css, and surveyed HeroSection.svelte. Citing concrete drift, not generalities.
Issue list
1. Token system has decayed into noise — the design system is the problem
Inconsistency. variables.css defines 7 near-identical grays (--gray #59606d, --text-muted #4c5056, --text-muted-strong #4a4f55, --text-subtle #5f6369, --text-soft #666, --text-softest #6a6d72, --text-heading-soft #34363a) and 6 near-identical off-whites (--surface-panel-soft, --surface-panel-muted, --surface-panel-warm, --surface-panel-cream, --surface-light, --off-white). They differ by 1–3 hex points.
Why it hurts. A design system with this many close-but-different values is functionally no system at all. Every component author picks a slightly different one. This is the engine behind every other inconsistency in the codebase.
Recommendation. Collapse to: 3 grays (heading / body / muted), 2 off-whites (page / panel), 2 brand greens (primary / hover). Run a codemod replacing the deprecated tokens. Lock the file with a comment header forbidding additions without review.
Good looks like. Linear's full text scale is 4 grays. Stripe's surface scale is 3.
Severity: Critical.
2. No radius scale exists — variables.css has zero --radius-* tokens
Inconsistency. Components hand-pick: 40px (button), 28px (photo card), 22px (photo card mobile), 18px (bento + caption), 16px (bento mobile + caption mobile), 11px (point icon), 999px (eyebrow). 7 different radii on one page.
Why it hurts. The eye notices radius mismatches more than almost any other inconsistency — it's the silhouette of every element. A 28px card next to an 18px card next to a 22px card reads as "ported from different templates."
Recommendation. Add tokens: --radius-sm 8, --radius-md 14, --radius-lg 20, --radius-pill 999. Pick one card radius (recommend 20). Buttons stay pill. Eyebrow stays pill. Everything else uses the scale.
Severity: Critical.
3. ValuesSection.svelte bypasses the token system almost entirely
Inconsistency. In ~480 lines of CSS in this single component:
- Hardcoded colors not in tokens:
#000(×3),#0d1a0d(×2),#102010,#5a605f,#3f4348,#fff(×3),#ede4d2,#fcfbf6. - Hardcoded shadows:
0 18px 34px rgba(17,20,24,0.08)(line 227),0 12px 24px rgba(...)(line 262),0 14px 34px rgba(...)(line 295),0 6px 16px rgba(33,48,33,0.18)(line 505) — when--shadow-card,--shadow-panel-strong,--shadow-badgealready exist for these. - Spacing values not on the 4px scale:
padding: 0 50px,padding: 38px 36px,padding: 32px 30px,padding: 13px 0,margin-top: 32px / 36px / 52px / 26px. - One shadow token (
--shadow-panel-elevated, line 479) — out of ~6 shadows. The other 5 are inline.
Why it hurts. This is design system drift in action. The tokens exist; they're being ignored. Every other component will do the same and the system becomes a museum.
Recommendation. Replace every literal in this file with a token. If a token doesn't exist, add it and use it everywhere it applies. Then add a stylelint rule blocking raw hex and raw shadow tuples in component CSS.
Severity: Critical.
4. Two <h3>s in the same section at totally different scales
Inconsistency. .values-contrast-cell h3 is clamp(20px, 1.9vw, 25px). .values-points-title (also <h3>) is clamp(24px, 2.4vw, 32px). They sit in the same section, 200px apart vertically.
Why it hurts. Visual hierarchy breaks. The reader's brain expects the second H3 to be peer-level with the first. Instead it's larger than the first, then .values-point h3 (also h3) drops back to 17px. Three H3s, three scales.
Recommendation. Adopt a real type scale token set: --text-display, --text-h2, --text-h3, --text-h4, --text-body, --text-small. Map every heading to a token. The "values points" introduction is functionally a sub-section header — use the same scale as the contrast cell titles, or promote it to h2-tier visually with proper hierarchy.
Severity: Major.
5. Photo caption layout breaks when detail is empty
Inconsistency. .values-photo-caption uses justify-content: space-between with two spans inside. Three of five photos have detail: ''. The result: an empty right-side gap and a left-pinned name in a half-empty pill. On mobile, the caption switches to display: grid; justify-content: start (line 569) which fixes it — but the desktop is broken.
Why it hurts. Looks accidental. A premium product never ships pills that are half-empty for half their content.
Recommendation. Either (a) make detail required, or (b) when detail is empty, render only the name and center it. Conditional class in Svelte: class:values-photo-caption-solo={!photo.detail}.
Severity: Major.
6. .btn-yellow uses pure black text
Inconsistency. buttons.css:65 — .btn-yellow { color: var(--text-strong); } where --text-strong: #000. Every other text surface in the app uses --gw-green #213021 or --text-heading #1f2421.
Why it hurts. Pure black on the brand yellow is the loudest color combo on the page. It pulls the eye like a warning sign and competes with the actual content. It's also the only place pure black appears outside .values-inner .section-heading.
Recommendation. Use var(--gw-green) on .btn-yellow. Contrast is still well above AA (≈8:1 on #ffd100) and the button immediately feels intentional and on-brand.
Severity: Major.
7. No :focus-visible states defined for .btn
Inconsistency. buttons.css defines :hover and :active only. No :focus-visible. Keyboard users get the browser default outline, which on a custom pill button with border-radius: 40px looks broken.
Why it hurts. Accessibility failure + perceived quality drop for power users. Tab through the page once and the button outlines clip the radius.
Recommendation.
.btn:focus-visible {
outline: 2px solid var(--gw-green);
outline-offset: 3px;
}
.btn-green:focus-visible { outline-color: var(--yellow); }
Severity: Major.
8. .values-inner uses hardcoded padding: 0 50px
Inconsistency. Line 187. The codebase already defines --space-container-x: clamp(24px, 4vw, 48px). The 50px here doesn't match any other section's container padding and breaks the visual gutter rhythm down the page.
Why it hurts. When sections scroll past, the left/right gutters jitter by a few pixels between sections. Reads as cheap on a calibrated monitor.
Recommendation. Replace with padding: 0 var(--space-container-x). Audit every section component and do the same — almost certainly this is repeated elsewhere.
Severity: Major.
9. Mobile eyebrow font-size drops to 11px
Inconsistency. Line 549 — .values-eyebrow { font-size: 11px; } on mobile. Combined with letter-spacing: 0.08em and uppercase, this is functionally close to unreadable on a real phone.
Why it hurts. Mobile readability and accessibility. Apple HIG and Material both recommend 12px minimum for body-adjacent text; uppercase tracked text should be 12–13px+ for legibility.
Recommendation. Mobile eyebrow: 12px, tracking ≤ 0.06em. Or drop uppercase on mobile entirely.
Severity: Major.
10. Featured photo card loses its prominence on mobile
Inconsistency. Lines 558–560 — .values-photo-card-featured { grid-row: auto; min-height: 178px; } on mobile. The whole point of the "featured" card is its larger size; on mobile it becomes a peer to the rest, and the layout decision evaporates.
Why it hurts. The featured card is the strongest brand image. On mobile (where the majority of traffic comes from for local services), it's neutered.
Recommendation. On mobile, make the featured card span both columns of the first row (grid-column: 1 / -1) with a larger min-height (~240px). Visual lead image, not a peer.
Severity: Major.
11. Reveal animation is slow
Inconsistency. Lines 645–647 — opacity 0.55s, transform 0.7s with a translateY(24px) reveal. The cubic-bezier is fine but the durations are dated.
Why it hurts. Premium products (Linear, Apple, Vercel) settle reveal animations at 0.25–0.4s. 0.7s feels like a 2015 marketing site.
Recommendation. opacity 0.3s ease, transform 0.45s cubic-bezier(0.2, 0.8, 0.2, 1). Reduce --reveal-distance to 16px.
Severity: Minor.
12. The "with/without" contrast cell is the page's strongest moment but ends in a quiet brand voice mismatch
Inconsistency. The "good" cell ends with: "That is what people are really buying: peace of mind, routine, and a dog who feels cared for." — yellow text on green, font-size 13px. The footer is the punchline but it's the smallest type in the cell.
Why it hurts. Hierarchy inversion. The most quotable line is treated like a footnote.
Recommendation. Promote the footer to 15–16px, maintain the yellow color and family. Add a small top divider only on the negative cell so the positive cell's footer reads as continuous emphasis.
Severity: Minor.
13. .values-bento uses a 1px gap + ink-tinted background trick for hairlines
Inconsistency. Line 290 — gap: 1px; background: rgba(17, 20, 24, 0.1); to simulate dividers. This is fine technique but the resulting line color (rgba(ink, 0.1)) is darker than the actual borders elsewhere in the system (--border-soft 0.05, --border-muted 0.08).
Why it hurts. Bento dividers read heavier than every other border on the page. The card looks "boxier" than its neighbors.
Recommendation. Drop the gap-background trick to rgba(var(--ink-rgb), 0.06) or a real --border-soft-strong token. Match the rest of the system.
Severity: Polish.
14. .values-contrast-num ("01", "02") in rgba(17, 20, 24, 0.22) is barely visible
Inconsistency. Decorative numbering at ~3:1 contrast.
Why it hurts. If it's decorative, that's fine. But it's currently big enough to look like it's supposed to be read. Either commit to it being a design element (larger, more confident) or make it functional (numbered as a real step indicator).
Recommendation. Either bump to 32px+ with the same low opacity (treats it as a visual mark, like editorial step numbering) or remove entirely. Half-measures are the worst outcome.
Severity: Polish.
Global Design System Drift
- Tokens written but not consumed.
--shadow-card,--shadow-panel-strong,--surface-panel-warm,--text-muted,--border-softexist, butValuesSection.sveltere-implements all of them inline. Likely repeated in every section component. - Spacing scale (
--space-1…--space-14) is ignored. Every padding/margin inValuesSectionis a literal pixel value off the 4px grid (50px,38px,36px,30px,26px,22px,18px,13px,9px). - No radius scale at all. Every component invents its own.
- No type scale. Headings use
clamp()with different formulas everywhere. - Seven grays, six off-whites, three greens in the token file — the surface area of the system exceeds what any human can use consistently.
- Hardcoded shadows. The shadow tokens are a complete shadow system that nobody is calling.
- Hover states use
translateY(-2px) scale(1.012)everywhere on.btn— but card hovers inValuesSectionusetransform: scale(1.06). No shared motion language.
Fast Wins
- Find/replace every raw hex color in
ValuesSection.sveltewith a token. ~30 minutes. Immediate consistency lift. - Replace
.values-inner { padding: 0 50px }withpadding: 0 var(--space-container-x)and do the same in every section component. ~20 minutes. Fixes the section-to-section gutter jitter. - Add
--radius-sm/md/lg/pilltokens and codemod components to use them. Pick 20px as the universal card radius. ~1 hour. - Switch
.btn-yellowtext tovar(--gw-green). 5 minutes. Big perceived-quality lift — the page stops shouting. - Add
:focus-visibleto.btn. 5 minutes. Accessibility + polish. - Fix the photo caption layout when
detailis empty. 10 minutes. Removes the only broken-looking element on the page. - Bump mobile featured photo to
grid-column: 1 / -1. 2 minutes. Mobile hero moment.
Premiumisation Opportunities
- Collapse the gray palette to 3 values and the off-white palette to 2. Premium brands feel calm because their surface palette is small.
- Speed up all reveal/hover transitions to 0.25–0.4s. Slower motion now reads as a slow site.
- Replace pure-black text (
#000) withvar(--gw-green)orvar(--text-heading)everywhere. True black on warm off-white feels harsh; the dark green keeps it editorial. - Tighten letter-spacing on display headings to
-0.03em. Mid-2020s premium look. Bonus: addtext-wrap: balanceto all h2s. - Replace the bento dividers with
--border-softand add a barely-visible inner highlight (inset 0 1px 0 rgba(255,255,255,0.5)) for the elevated feel Linear uses on dark surfaces. - Promote the "punchline" copy (the contrast-footer lines) to body-lead size. Stop hiding the best writing in 13px.
- Inline social proof under the contrast cells. "Joined by 200+ Auckland owners" or a row of three small testimonial avatars with rating. The section currently asserts emotional benefits without proof — easy E-E-A-T win.
Mobile Audit
- Eyebrow text is 11px with uppercase + 0.08em tracking — sub-legible. Bump to 12px, drop tracking to 0.06em.
- Featured photo loses its purpose — collapses to peer-card. Fix with
grid-column: 1 / -1; min-height: 240px. - Photo caption switches to vertical stack (good) but
.values-photo-detailruns through-webkit-line-clamp: 2even when detail is empty — leaves a weird invisible vertical reserve. Conditionally hide the element when empty. - Container padding uses
--space-container-x-mobile(24px). Other components on the site may use the desktop 50px hardcode plus naive scaling — verify all sections collapse to a consistent 24px gutter. - Tap targets — the photo cards are large (~178px tall) and tappable hover scales aren't useful on touch. Hover scale lifts to
1.06on tap-through devices look glitchy on iOS Safari. Wrap in@media (hover: hover)(already done — good) but verify the photo:hoverrule is also gated (line 242 — good, it is). - No visible scroll affordance between the contrast cells and the values-points header — 30px gap on mobile (line 617). Reads as cramped.
.btnpadding13px 28pxon mobile yields ~44px tall tap targets only if line-height holds. Hero CTA stack untested in this audit but worth verifying tap area ≥48px on the primary booking CTA.
Final Verdict
What makes it feel inconsistent. A real design system exists in variables.css but the components don't use it. Components ship with raw hex, raw pixels, raw shadow tuples, and inconsistent radii. The token file has too many near-duplicate values, which is what enabled the drift in the first place.
What is preventing it from feeling premium. Loud black-on-yellow CTA, slow reveal animations, seven different gray tones in the same viewport, radius drift between cards, missing focus states, and one or two broken elements (empty captions, mobile featured card collapse) that read as accidental rather than intentional.
Top 5 highest-ROI changes:
- Collapse the token palette (3 grays, 2 off-whites, add
--radius-*scale) — this is the precondition for everything else holding. - Codemod
ValuesSection.svelte(and peer components) to consume tokens exclusively — kills 80% of the visual inconsistency in one pass. - Switch
.btn-yellowtext to--gw-greenand add:focus-visible— instant perceived-quality lift on the most-clicked element. - Speed up reveal/hover transitions to 0.25–0.4s — the single biggest "feels modern" lever.
- Fix the photo caption (empty-detail case) and the mobile featured card — two small fixes that remove the only "homemade" moments on the section.
These are systems-level fixes, not redesigns. The CLAUDE.md mandate to preserve the WordPress visual design is respected — none of this changes layout, color brand, or hierarchy. It tightens the existing system into something that holds together.