Files
gw-svelte/docs/design-audit.md
T
2026-05-26 08:30:05 +12:00

34 KiB
Raw Blame History

Design Audit — Goodwalk (Codebase-wide)

Re-audited beyond ValuesSection. Read sections.css (1130 lines), typography.css, responsive.css, variables.css, and surveyed each major surface (Hero, Intro, PageHeader, Footer, FAQ, Instagram, Mobile Menu, Testimonial card). Findings below cite actual files/lines.


Critical issues

1. Three different "eyebrow" components exist, sharing no DNA

Problem. The same UX element — small uppercase intro label above a heading — is rendered three incompatible ways:

  • .eyebrow (typography.css:100): font-body, 12px, 700, 0.09em, green text, no background.
  • .hero-kicker (sections.css:69): font-body, 12px, 700, 0.08em, yellow text in a yellow pill with yellow border.
  • .intro-kicker (sections.css:408): font-body, 12px, 600 (weight differs), 0.18em (tracking differs — over 2× the others), white-58% text with a yellow rule prefix.

Plus .values-eyebrow, .booking-eyebrow, .footer-col-label, .intro-meta are all variants.

Root cause. No eyebrow primitive in the system. Each section author re-implemented from scratch. Perception. Reader senses different sections were designed at different times. Brand voice fragments. Fix. One <Eyebrow variant="default|inverse|accent" /> component. Three variants only. Migrate all six existing implementations. Class: System debt.


2. Six different primary heading scales

Problem.

Selector Scale Weight Tracking
.section-heading clamp(30, 4.6vw, 44) 800 -0.035em
.hero-text h1 clamp(34, 4vw, 56) 800 -0.045em
.intro-headline clamp(30, 4.4vw, 54) 500 -0.02em
.ph-title clamp(34, 4vw, 56) 800 -0.04em
.info-block h2 clamp(28, 2.4vw, 32) 700 -0.02em
#instagram h2 clamp(30, 3vw, 36) 700 -0.02em

Six clamp formulas. Five tracking values. Weights ranging 500→800. The base h2 { font-weight: 700 } (typography.css:58) conflicts with .section-heading { font-weight: 800 } (typography.css:79) — actual rendered weight depends on whether the heading author remembered to apply the class.

Root cause. No --text-display, --text-h1, --text-h2 tokens. Every author authors a fresh clamp. Perception. Headings jump in size and weight as you scroll. The 500-weight .intro-headline reads as a different brand from the 800-weight .hero h1 directly above it. Fix. Define type tokens: --text-display (52/800/-0.04), --text-h1 (44/800/-0.035), --text-h2 (32/700/-0.02), --text-h3 (22/700/-0.02), --text-body-lead (17/500/0), --text-body (16/400/0). Map every heading. Delete the per-section clamps. Class: Design debt + System debt.


3. The Instagram section is a chromatic anomaly

Problem. Section background sequence on the homepage: #hero (green) → #intro (green) → #promise/#values (off-white) → #services (white) → #testimonials (white) → #info (white) → #newlead (white) → #instagram (solid #ffd100 full-bleed) → footer (green).

The Instagram block is the only place brand yellow is used as a page section. It's also where pure black-on-yellow body text appears at rgba(0, 0, 0, 0.6) (sections.css:738).

Root cause. Brand yellow was treated as both an accent (CTAs, highlights) AND a surface (this section). Premium products pick one. Perception. Reads as advertising, not editorial. Breaks the calm green/off-white rhythm. The user scrolls into a marketing banner mid-page. Fix. Either (a) demote to a green section with yellow accents inside, or (b) shrink to a narrow card-on-off-white block. Reserve var(--yellow) for accents only. Class: Design debt + Conversion debt (it sits right before footer — possibly the last impression).


4. Hero animation system runs on a different clock than the rest of the page

Problem. Hero entry: image 1.6s, text rise 0.85s, underline draw 0.9s @ 900ms delay, star pop 0.45s starting at 820ms. Total animation completes ~1.7s after load.

Rest of page: reveal-block now 0.3s opacity / 0.45s transform. Mobile menu: 180220ms. Button hover: 0.160.22s.

Root cause. Hero was authored before the reveal/motion system existed. Never reconciled. Perception. The hero feels heavy and ceremonial; the rest of the page feels snappy. Two products bolted together. Fix. Cap all hero animations at 0.5s. Reduce delays — first text element by 80ms, each subsequent +60ms. Drop the underline-draw delay from 900ms to 350ms. Keep the elegance, lose the lethargy. Class: Polish debt.


5. --space-container-x-tablet breaks the gutter rhythm

Problem. variables.css:145 — --space-container-x-tablet: 30px. Desktop value is clamp(24px, 4vw, 48px). At 1024px width (the tablet breakpoint), 4vw = 41px. The tablet override drops gutters to 30px, then back up to 4148px on desktop. A user resizing a window or rotating an iPad sees the gutters narrow then widen.

Root cause. A patch fix applied at the tablet breakpoint that doesn't share the desktop formula. Perception. Visible engineering. Anyone who notices the layout shift on resize loses trust. Fix. Either delete --space-container-x-tablet and let the clamp handle the breakpoint, or make tablet = clamp(20px, 3vw, 32px) so the curve is continuous. Class: System debt.


6. Card system doesn't exist — each section invents its own

Problem. Same UX primitive (an elevated content card), shipped four ways:

Component Radius Padding Background Shadow
.testimonial-card 28px 36px 32px gradient panel→panel-soft inset + shadow-md
.faq details 16px 18px 22px surface-page shadow-lg on open only
.values-photo-card 28px → md mobile n/a beige inset + card
.booking-field-card 24px 24px 22px (varies) (varies)

Plus .values-point (no radius, pure surface), .mobile-menu-links a (16px radius), .ph-media (28px), .intro-google (pill). Root cause. No <Card /> primitive. Every author re-decides. Perception. Adjacent sections feel like they were copy-pasted from different Behance projects. Fix. One Card system: --radius-lg (20px) for cards, --space-7 (32px) padding, --shadow-card standard. Variants: card, card-elevated, card-quiet. Migrate all four. Class: System debt + Design debt.


Problem. Within the footer alone: 14px (.footer-brand p, .footer-nav a), 13px (.footer-bottom, .footer-contact-link), 12px (.footer-back-top, .footer-social-invite), 10px (.footer-col-label), plus 14px (footer h4). Five sizes in a quiet auxiliary surface.

Add opacities: .footer-brand p 0.72, .footer-nav a 0.72, .footer-contact-link 0.7, .footer-bottom 0.6, .footer-back-top 0.5, .footer-col-label 0.5.

Root cause. Each footer column was sized independently to hit visual targets, not to scale. Perception. The footer feels busy in a quiet way — many small elements clamoring for slightly different attention. Fix. Two sizes: 14px (links/copy) + 12px (label/legal). Two opacities: 0.85 (active text) + 0.55 (labels). Delete the rest. Class: Polish debt + UX debt.


8. .section-heading color is pure #000, used on warm off-white backgrounds

Problem. typography.css:82 — .section-heading { color: var(--text-strong); } where --text-strong: #000. Promise, Values, Testimonials, Services, Info all render their primary heading in pure black on a warm --off-white #f8f7f2. Same pattern as the .btn-yellow issue I already fixed: pure black against a warm surface reads as harsh and clinical.

Root cause. --text-strong is a "darkest-possible" token used reflexively for headings instead of a heading-specific token. Perception. Headings shout. The rest of the type — body in var(--text) (#2e3031), muted in green-cast grays — is warm. Headings are not. Fix. .section-heading { color: var(--text-heading); } (which is #1f2421, near-black with a green cast). Or var(--gw-green) for full editorial treatment. The dark-green-on-warm-cream pairing is the brand's most premium register. Class: Design debt.


9. Two Google trust-mark components live as duplicates

Problem. .hero-trust-mark (sections.css:153) and .intro-google-mark (sections.css:491). Same Google "G" circle, same white background, same shadow recipe with slightly different parameters (0 2px 8px rgba(0,0,0,0.25) vs 0 4px 12px rgba(0,0,0,0.25)). Different sizes (28px vs 36px). Different parent chip layouts (.hero-trust-chip vs .intro-google).

Root cause. Copy-paste authoring. The second one was built without abstracting the first. Perception. A user who scrolls hero → intro sees the same trust signal twice in slightly different sizes. Either feels redundant or sloppy depending on attention level. Fix. One <GoogleTrustMark size="sm|md" /> component. Two clean sizes (24, 36). Single shadow token. Class: System debt + Conversion debt (trust signals matter; redundant ones dilute).


10. Letter-spacing on headings has four values

Problem. -0.02em (h2/h3, intro-headline, info-block h2), -0.035em (section-heading), -0.04em (ph-title), -0.045em (hero h1). Four bespoke tracking values.

Root cause. Each heading was tuned visually without a tracking scale. Perception. Headings at similar sizes (.intro-headline 54px at -0.02em vs .hero h1 56px at -0.045em) feel like they're set in different fonts, when they're actually both Unbounded. Fix. Two tracking values: -0.035em for display (36px+), -0.02em for everything else. Lock in the type tokens. Class: Design debt.


11. Hero text relies on hand-numbered nth-child animation delays

Problem. sections.css:272278 — .hero-text > :nth-child(1) { animation-delay: 160ms; } through :nth-child(7). Seven hardcoded children. If the hero content shape changes (e.g., remove the kicker, add a phone CTA), the choreography breaks silently.

Root cause. Animation was authored against current markup, not against generic children. Perception. Probably invisible to most users until it breaks — at which point an element pops in without animation while others slide. Fix. Use CSS counter or a Svelte action that applies --reveal-delay per child. Or accept staggered delays via :nth-child(n) formula: animation-delay: calc(160ms + (var(--i, 0)) * 100ms). Class: System debt + Polish debt.


12. Mobile hero CTA shrinks to 12px font, 9px horizontal padding under 480px

Problem. responsive.css:767774 — primary .btn-yellow becomes padding: 13px 9px; font-size: 12px; line-height: 1.1 on iPhone SE-class widths. This is the primary booking CTA — the conversion target.

Root cause. Two CTAs side-by-side in a row at very narrow widths. Author chose to squeeze both rather than stack. Perception. On a 320px screen, the booking button reads as cramped, low-confidence. CTA hierarchy collapses. Fix. Keep buttons stacked under 480px (already done at 768px — extend the rule). Primary CTA: full-width, 14px font, 14px vertical padding. Secondary link below. Class: Conversion debt.


13. Hardcoded shadows everywhere despite a complete shadow token set

Problem. Beyond ValuesSection, the same pattern repeats:

  • .testimonial-card:hover0 8px 40px rgba(0, 0, 0, 0.08) (sections.css:620). Not a token.
  • .hero-trust-mark0 2px 8px rgba(0, 0, 0, 0.25) (line 161). Not a token.
  • .intro-google-mark0 4px 12px rgba(0, 0, 0, 0.25) (line 499). Not a token.
  • .ph-media0 16px 40px rgba(var(--ink-rgb), 0.08) (line 1027). Not a token.
  • .intro-google — uses inset shadow tuples directly (lines 470, 482).

Root cause. Authors didn't know which token to grab from 18 shadow options. Perception. Elevations feel inconsistent — some cards lift more, some less, even when visually they're the same depth. Fix. Five tokens, not eighteen: --shadow-flat, --shadow-card, --shadow-elevated, --shadow-floating, --shadow-modal. Migrate everything. Class: System debt.


14. The .intro-kicker heading-overline pattern is design-y; nothing else on the page uses it

Problem. .intro-kicker (sections.css:408) uses a horizontal rule + uppercase text pattern (line + word). It's stylish. But it appears nowhere else. Adjacent sections use pill eyebrows, naked uppercase, or nothing.

Root cause. Section author drew inspiration that didn't propagate. Perception. The intro feels visually distinct in a way that disconnects it from the rest of the page — over-designed compared to its neighbors. Fix. Pick one: either commit and use the rule pattern in 2-3 more sections (rhythm); or drop it and use the standard .eyebrow. Class: Design debt.


15. FAQ details radius (16px) doesn't match testimonial card (28px), values bento (20px), or photo card (28px) — they all sit on the same page

Problem. Cumulative effect of issue #6. The Info section has FAQ cards next to other cards with completely different silhouettes. Perception. Visual rhythm dissolves. Fix. All cards at --radius-lg (20px). Period. Class: Polish debt.


Problem. sections.css:600 — footer { padding: 60px 50px 32px; }. The 50px is hardcoded. The mobile override at responsive.css:678 uses var(--space-container-x-mobile) — correct. But desktop is a literal. Same problem at #instagram { padding: 60px 50px; } (sections.css:732) and .ph-inner { padding: 0 50px; } (line 949). Perception. Footer/instagram/page-header gutters jitter against section gutters by a few pixels — the same issue I documented for ValuesSection, repeated across the codebase. Fix. Replace every 50px desktop gutter with var(--space-container-x). Class: System debt.


17. Section vertical rhythm collapses on mobile to a single value

Problem. variables.css:172178 — on mobile, all four section padding tiers (--space-section-featured-y, --space-section-support-y, --space-section-form-y, --space-section-page-y) collapse to --space-section-mobile-y (40px). On desktop, featured sections breathe more than supporting. On mobile, every section has identical vertical padding.

Root cause. Pragmatic — mobile space is scarce. But the intent (featured sections feel more important) disappears. Perception. Mobile users get a "flat" page where every section has equal visual weight. Hero → Intro → Promise → Services all feel like peers. Fix. Two mobile tiers: --space-section-featured-y-mobile: 56px, --space-section-mobile-y: 40px. Featured sections get 16px more breathing room. Still respects mobile constraints. Class: Design debt.


18. #hero::after gradient mask has a 4-stop gradient with literal RGB

Problem. sections.css:3245 — a four-stop gradient transparent 18% → 26% brand → 78% brand → solid brand 86%. Plus a mobile-only override at lines 293303 with different stops (22% → 45% → 88% → 78%). The math doesn't read as deliberate; it reads as tuned by trial and error.

Root cause. Hero image overlay was fine-tuned to one image asset. Different images won't behave the same. Perception. Probably invisible. But if the hero photo ever changes (winter scene, different dog), the overlay won't land right. Fix. Reduce to 2 stops: transparent 30% → solid brand 95%. Test against multiple photos. Class: Polish debt.


19. Hero kicker mobile color is rgba(white, 0.48) on dark green

Problem. responsive.css:358 — color: rgba(var(--white-rgb), 0.48) for the kicker on mobile. At 10px uppercase 0.13em on a dark green-tinted gradient, that's somewhere around 45:1 contrast against the gradient. Borderline AA.

Root cause. Designer wanted the kicker quiet against the loud headline. But mobile users get a small device + outdoor sunlight + decreased contrast. Perception. Some users won't read the kicker. Those who do will strain. Fix. rgba(white, 0.72) at 11px on mobile. Still quiet, properly accessible. Class: UX debt.


20. Three styling paradigms cohabit

Problem. The codebase uses:

  1. Global utility classes (.btn, .eyebrow, .section-heading, .testimonial-card, .faq) defined in src/lib/styles/*.css.
  2. Component-scoped styles inside Svelte <style> blocks (ValuesSection, BookingSection, etc.).
  3. :global() overrides inside component-scoped styles that effectively re-globalize CSS.

Root cause. Migration in progress between approaches, no policy decision. Perception. Invisible to users — but the codebase resists consistent change. Every refactor has to reckon with three rule systems. Fix. Pick one. Recommend: component-scoped for component-specific styles, global utility classes for cross-component primitives (.btn, .eyebrow, .card, .section-heading). Forbid :global() overrides except for action-injected classes (reveal-*). Class: System debt.


Systemic Drift Map (grouped by root cause)

Root cause A — No token-driven scales for type, radius, motion, shadow. Issues: #2, #6, #10, #13, #15. Authors invent each value fresh because there's no canonical "use this."

Root cause B — Primitive components were never extracted. Issues: #1, #9, #6 (cards), #14 (intro-kicker). Same UX element re-implemented in multiple places.

Root cause C — Container padding system is incomplete on desktop. Issues: #5, #16. Mobile uses tokens; desktop hardcodes 50px and tablet hardcodes 30px.

Root cause D — Hero was authored on a separate timeline than the rest of the page. Issues: #4, #11, #18, #19, #12. The Hero composition predates the reveal system, predates current motion language.

Root cause E — Pure black #000 is treated as "the heading color." Issues: #8, plus the original .btn-yellow issue I already fixed. The system has a warmer near-black available; it's not the default.

Root cause F — Brand yellow is used both as accent and surface. Issues: #3 (Instagram section). Premium brands choose one role per color.

Root cause G — Mobile breakpoints collapse intent. Issues: #17, #12, #7. Mobile flattens hierarchy that desktop carefully built.


Premiumisation Roadmap

To increase calmness:

  1. Collapse shadows to 5 tokens. Use them everywhere.
  2. Replace #000 with --text-heading or --gw-green across all headings.
  3. Demote the Instagram section from full-yellow to a green section with yellow accents.
  4. Section vertical rhythm: two tiers on mobile, not one (Issue #17).

To increase trust:

  1. Single Google trust-mark component (Issue #9).
  2. Consolidate footer typography to 2 sizes (Issue #7).
  3. Fix mobile hero kicker contrast (Issue #19).
  4. Add last-updated and author byline to comparison and any FAQ-style pages.

To increase sophistication:

  1. Adopt a type scale (Issue #2). Six display sizes is amateur; three is grown-up.
  2. One letter-spacing scale, two values (Issue #10).
  3. Replace .intro-kicker rule pattern with the unified .eyebrow — or commit to it and repeat it in three other places. No one-offs (Issue #14).
  4. Hero animation: cap everything at 0.5s. Restraint reads as confidence (Issue #4).

To increase conversion confidence:

  1. Mobile hero CTA: stack and grow, never shrink (Issue #12).
  2. Trust mark consolidation reinforces — not repeats — social proof (Issue #9).
  3. Instagram section: if removed/demoted, the path from #newlead → footer is uninterrupted. Currently #newlead (the booking form) bleeds into a yellow billboard, which dilutes the form's gravity (Issue #3).

To increase perceived engineering quality:

  1. Solve the tablet gutter shift (Issue #5).
  2. Eliminate the nth-child animation hardcode (Issue #11).
  3. Eliminate hardcoded shadows in global CSS (Issue #13).
  4. Pick one styling paradigm (Issue #20).

Design System Refactor Priority

Now (this week):

  1. Type scale tokens. --text-display/h1/h2/h3/body-lead/body/small with weight + tracking. Migrate .section-heading, .hero h1, .intro-headline, .ph-title, .info-block h2, #instagram h2 to consume them. This single change subsumes issues #2 and #10.
  2. Eyebrow primitive. One Svelte component, three variants. Migrate .eyebrow, .hero-kicker, .intro-kicker, .values-eyebrow, .booking-eyebrow, .footer-col-label. Issue #1.
  3. Replace --text-strong with --text-heading in .section-heading. One-line change, large perceived impact. Issue #8.
  4. Container gutter cleanup. Find/replace padding: 0 50pxpadding: 0 var(--space-container-x) across footer, #instagram, .ph-inner. Re-evaluate --space-container-x-tablet. Issues #5, #16.

Next (this month): 5. Card primitive. <Card variant="default|quiet|elevated" />. Migrate testimonial, FAQ, photo, booking field. Issue #6. 6. Shadow consolidation. 18 tokens → 5. Migrate all hardcoded shadows. Issue #13. 7. Hero animation pass. Cap durations, dynamic delays. Issues #4, #11. 8. Instagram section redesign. Either demote or recompose. Issue #3.

Eventually (next quarter): 9. Google trust-mark component. Issue #9. 10. Footer typography pass. Two sizes, two opacities. Issue #7. 11. Mobile vertical rhythm tiers. Issue #17. 12. Pick one styling paradigm. Document. Issue #20.


Visual Cohesion Score

Scored against what a principal-designer-led product (Linear, Stripe, Apple marketing) would ship.

Axis Score Note
Typography 4/10 Six heading scales, four trackings, weight conflicts between base h2 and .section-heading. The font choices (Unbounded + Readex Pro) are good; their application is uneven.
Spacing 5/10 A 4px scale exists and is partly respected, but desktop container padding is hardcoded (50px) and the tablet override breaks the curve. Section vertical rhythm collapses on mobile.
Colour 5/10 The brand palette (green + yellow + cream) is strong. Execution: too many near-identical grays/off-whites in tokens, pure black where warmer would serve, yellow used as both accent and surface.
Motion 4/10 Three speed regimes coexist (180220ms, 300450ms, 8501600ms). Hero choreography is heavy compared to a snappy rest-of-page. Mobile menu motion is the best-tuned.
Hierarchy 6/10 Within each section, hierarchy generally reads. Across the page, section weights compete — Instagram outshouts the booking form that precedes it. Headings have inconsistent weight signaling.
Responsiveness 6/10 Mobile is genuinely handled (good iOS-zoom-on-focus awareness, 44px tap targets, sticky book bar). But mobile flattens hierarchy that desktop built, and the <480px CTA squeeze undermines the conversion target.
Premium feel 4/10 Several premium gestures (warm beige photo frames, Google trust mark, restrained eyebrows in some places) are undone by harsh black headings, the yellow Instagram billboard, the 1.6s hero entry, and visible engineering drift between sections.

Overall: 4.9 / 10.


Final read

This product is two design languages glued together. The first is editorial-warm: cream surfaces, dark-green accents, photographic cards with beige frames, restrained typography. The second is marketing-loud: yellow billboards, black-on-yellow CTAs, 1.6s hero animations, sub-12px footer captions, hand-numbered choreography.

The brand voice (CLAUDE.md says "preserve the WordPress visual design") implicitly rewards both. But premium products converge. Right now Goodwalk is asking the visitor to switch reading modes every two scrolls.

The single highest-leverage move: adopt a real type-scale token set this week. Issues #2, #8, #10, and large parts of #1 and #7 dissolve. Headings stop fighting each other. The page begins to feel like one product.

The second-highest: abolish the yellow Instagram section as a section. Yellow becomes a pure accent. The path from hero → booking → footer becomes uninterrupted green-and-warm. The site immediately reads more grown-up.

Everything else is downstream of those two.


Changelog

Concrete from→to changes, grouped by file. Each row references the issue it resolves.

src/lib/styles/variables.css

# From To
2 (no type tokens) Add --text-display: clamp(40px, 5vw, 56px), --text-h1: clamp(34px, 4vw, 44px), --text-h2: clamp(28px, 3vw, 36px), --text-h3: clamp(20px, 2vw, 24px), --text-body-lead: 17px, --text-body: 16px, --text-small: 13px
2 (no weight tokens) Add --weight-display: 800, --weight-heading: 700, --weight-body: 400, --weight-emphasis: 600
10 (no tracking tokens) Add --tracking-display: -0.035em, --tracking-heading: -0.02em, --tracking-eyebrow: 0.08em
5 --space-container-x-tablet: 30px Delete; rely on the desktop clamp at tablet widths
13 18 shadow tokens (--shadow-xs/sm/md/lg/xl/2xl/float/press/panel/panel-strong/panel-soft/card/card-hover/badge/badge-hover/menu/menu-soft/...) Collapse to 5: --shadow-flat, --shadow-card, --shadow-elevated, --shadow-floating, --shadow-modal. Alias old names to the 5 during migration.
17 --space-section-mobile-y: var(--space-8) (single mobile tier) Add --space-section-featured-y-mobile: var(--space-10) (56px); keep --space-section-mobile-y: var(--space-8) (40px) for support tiers

src/lib/styles/typography.css

# From To
8 .section-heading { color: var(--text-strong); } (#000) .section-heading { color: var(--text-heading); } (#1f2421)
2 .section-heading { font-size: var(--heading-section-size); font-weight: 800; letter-spacing: -0.035em; } .section-heading { font-size: var(--text-h1); font-weight: var(--weight-display); letter-spacing: var(--tracking-display); }
2 .hero-text h1 { font-size: clamp(34px, 4vw, 56px); font-weight: 800; letter-spacing: -0.045em; } .hero-text h1 { font-size: var(--text-display); font-weight: var(--weight-display); letter-spacing: var(--tracking-display); }
2 h2 { font-weight: 700 } (conflicts with .section-heading 800) h2 { font-weight: var(--weight-heading); } — and remove the duplicate weight from .section-heading (token already encodes it)
2 .info-block h2 { font-size: clamp(28px, 2.4vw, 32px); } .info-block h2 { font-size: var(--text-h2); }
2 #instagram h2 { font-size: clamp(30px, 3vw, 36px); } #instagram h2 { font-size: var(--text-h2); }
10 Four tracking values (-0.02 / -0.035 / -0.04 / -0.045) across headings Two: var(--tracking-display) for h1/display, var(--tracking-heading) for h2/h3
1 .eyebrow { font-size: 12px; font-weight: 700; letter-spacing: 0.09em; color: var(--gw-green); } Becomes the single source of truth. Same selector, but letter-spacing: var(--tracking-eyebrow). Add .eyebrow--inverse (white) and .eyebrow--accent (yellow) modifiers.

src/lib/styles/sections.css

# From To
1 .hero-kicker { ... yellow pill, yellow text, font-size 12px, weight 700, tracking 0.08em ... } Replace with <span class="eyebrow eyebrow--accent">. Delete the .hero-kicker rules.
1 .intro-kicker { ... yellow rule prefix, weight 600, tracking 0.18em, white-58% text ... } Replace with <span class="eyebrow eyebrow--inverse">. Delete .intro-kicker-rule and the .intro-kicker rules. (Issue #14 — the rule pattern is a one-off; remove it.)
16 footer { padding: 60px 50px 32px; } footer { padding: var(--space-11) var(--space-container-x) var(--space-7); }
16 #instagram { padding: 60px 50px; } #instagram { padding: var(--space-section-support-y) var(--space-container-x); }
16 .ph-inner { padding: 0 50px; } .ph-inner { padding: 0 var(--space-container-x); }
3 #instagram { background: var(--yellow); } with .instagram-blurb { color: rgba(0, 0, 0, 0.6); } #instagram { background: var(--gw-green); color: var(--text-inverse); }. #instagram .btn { background: var(--yellow); color: var(--gw-green); }. Yellow becomes accent inside the green section, not the surface.
13 .testimonial-card:hover { box-shadow: ..., 0 8px 40px rgba(0,0,0,0.08); } .testimonial-card:hover { box-shadow: ..., var(--shadow-elevated); }
13 .hero-trust-mark { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); } .hero-trust-mark { box-shadow: var(--shadow-card); }
13 .intro-google-mark { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); } .intro-google-mark { box-shadow: var(--shadow-card); } (then deleted — see #9)
13 .ph-media { box-shadow: 0 16px 40px rgba(var(--ink-rgb), 0.08); } .ph-media { box-shadow: var(--shadow-elevated); }
9 .hero-trust-mark (28px circle) + .intro-google-mark (36px circle) as separate rule blocks Both replaced by `<GoogleTrustMark size="sm
6, 15 .testimonial-card { border-radius: 28px; padding: 36px 32px; background: linear-gradient(180deg, var(--surface-page), var(--surface-panel-soft)); } .testimonial-card { border-radius: var(--radius-lg); padding: var(--space-7); background: var(--surface-panel); }
6, 15 .faq details { border-radius: 16px; }, .faq summary { border-radius: 16px; padding: 18px 22px; } .faq details { border-radius: var(--radius-lg); }, .faq summary { border-radius: var(--radius-lg); padding: var(--space-5) var(--space-6); }
6 .ph-media { border-radius: 28px; } .ph-media { border-radius: var(--radius-lg); }
18 #hero::after four-stop gradient (transparent 18% → 26% → 78% → 86%) Two-stop: linear-gradient(to bottom, transparent 30%, var(--surface-brand) 95%)
4, 11 .hero-text > :nth-child(1..7) { animation-delay: 160ms..760ms; } (seven hardcoded) Single rule: .hero-text > * { animation-delay: calc(120ms + var(--i, 0) * 80ms); }. Inject --i via Svelte style:--i={index}. Max stagger reduced from 760ms to ~500ms.
4 .hero-text > * { animation: heroRise 0.85s cubic-bezier(0.22, 1, 0.36, 1) forwards; } animation: heroRise 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards;
4 .hero-img img { animation: heroImageEnter 1.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; } animation: heroImageEnter 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
4 .hero-title-highlight::after { animation: heroUnderlineDraw 0.9s ...; animation-delay: 900ms; } animation: heroUnderlineDraw 0.4s ...; animation-delay: 350ms;
7 Five footer text sizes (14/13/12/10) and six opacities (0.5/0.6/0.7/0.72/0.8/0.85) Two sizes: 14px (links/copy) + 12px (labels/legal). Two opacities: 0.85 (active) + 0.55 (labels). Apply across .footer-brand p, .footer-nav a, .footer-bottom, .footer-contact-link, .footer-back-top, .footer-col-label.

src/lib/styles/responsive.css

# From To
19 .hero-kicker { color: rgba(var(--white-rgb), 0.48); font-size: 10px; letter-spacing: 0.13em; } (mobile) .hero-kicker { color: rgba(var(--white-rgb), 0.72); font-size: 11px; letter-spacing: var(--tracking-eyebrow); } (mobile)
12 @media (max-width: 480px) { .hero-buttons { flex-direction: row; gap: 8px; } .hero-buttons .btn-yellow { padding: 13px 9px; font-size: 12px; line-height: 1.1; flex: 1; width: 0; } } Delete the entire @media (max-width: 480px) .hero-buttons block. Let the 768px stacked layout extend down — primary CTA stays full-width, 14px font, 14px vertical padding.
5 @media (max-width: 1024px) { .services-inner, .values-inner, ... { padding-left: var(--space-container-x-tablet); } } Delete the tablet padding override entirely. Components use var(--space-container-x) which already clamps appropriately.

src/lib/components/Header.svelte and section components

# From To
1 <span class="eyebrow values-eyebrow">…</span> (Values + Booking + others) <Eyebrow>…</Eyebrow> Svelte component, default variant. Delete .values-eyebrow, .booking-eyebrow rules.
9 Inline .hero-trust-mark markup in HeroSection.svelte and .intro-google-mark markup in IntroStrip.svelte <GoogleTrustMark size="sm" /> and <GoogleTrustMark size="md" /> in their respective parents.
11 <div class="hero-text"> <p class="hero-kicker">…</p> <h1>…</h1> … </div> (children animate via nth-child) <div class="hero-text"> {#each items as item, i} <div style:--i={i}>…</div> {/each} </div> — explicit index per child, animation reads --i.
3 <section id="instagram">…</section> background defined in CSS via #instagram { background: var(--yellow); } Same markup, but section now reads green (CSS-only change in sections.css above).

New components to create

# Component Purpose
1 `<Eyebrow variant="default inverse
6 `<Card variant="default quiet
9 `<GoogleTrustMark size="sm md" />`

Migration table — what consumes the new tokens

Token Consumers (must migrate)
--text-display .hero-text h1
--text-h1 .section-heading, .ph-title
--text-h2 .intro-headline, .info-block h2, #instagram h2
--text-h3 .service-card h3, .info-block h3, .values-points-title, .values-point h3
--tracking-display All h1-tier headings
--tracking-heading All h2/h3-tier headings
--radius-lg All cards (testimonial, FAQ, photo, booking field, ph-media)
--shadow-card Hero/intro trust marks, trust chip elevations
--shadow-elevated Testimonial hover, ph-media, FAQ open state
--shadow-floating Buttons on hover, intro-google on hover

Out of scope (deliberately not in this changelog)

  • Collapsing the 7 gray tokens to 3 — separate codebase-wide codemod; ship after type scale lands.
  • Collapsing the 6 off-white tokens to 2 — same reasoning.
  • Stylelint rule blocking raw hex / raw shadow tuples — adds after the migration is complete; otherwise too noisy.
  • Picking one styling paradigm (Issue #20) — architectural decision, separate PR.