Files
gw-svelte/v4-design.md
T

456 lines
34 KiB
Markdown
Raw Normal View History

2026-05-18 09:43:29 +12:00
# 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.**
---
### 7. Footer typography is a four-size jumble
**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:hover``0 8px 40px rgba(0, 0, 0, 0.08)` (sections.css:620). Not a token.
- `.hero-trust-mark``0 2px 8px rgba(0, 0, 0, 0.25)` (line 161). Not a token.
- `.intro-google-mark``0 4px 12px rgba(0, 0, 0, 0.25)` (line 499). Not a token.
- `.ph-media``0 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.**
---
### 16. Footer container padding doesn't share the global gutter system
**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 50px``padding: 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|md" />`. Component owns one shadow, one shape, two size variants. Delete both CSS blocks. |
| 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|accent" />` | Single eyebrow primitive. Replaces 6 implementations. |
| 6 | `<Card variant="default|quiet|elevated" />` | Single card primitive. Migrates testimonial, FAQ, photo, booking field, point. |
| 9 | `<GoogleTrustMark size="sm|md" />` | Single Google "G" mark. Replaces hero + intro duplicates. |
## 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.