Design Language tweaks

This commit is contained in:
2026-05-15 01:28:10 +12:00
parent baafafabdb
commit 580d600c47
52 changed files with 3465 additions and 1548 deletions
+302
View File
@@ -0,0 +1,302 @@
# Homepage Refinement Change Log
This file tracks the conversion and design-system refinement pass applied to the Goodwalk marketing site.
The aim is not a brand reset. The aim is to preserve the current warmth and premium positioning while moving the interface toward quieter confidence.
## Goals
- improve clarity
- improve trust pacing
- reduce decision friction
- improve conversion flow
- keep the existing brand emotionally recognizable
## Implemented
### Homepage sequence
Proposed:
- move the founder story later so visitors understand the offer before the personal narrative
Implemented:
- reordered the homepage to `Hero -> Services -> How It Works -> Testimonials -> Founder -> Values -> Booking -> Info`
Files:
- `src/routes/+page.svelte`
Motive:
- improves speed of understanding
- makes the founder section reinforce trust instead of carrying early explanation
### Hero simplification
Proposed:
- reduce competing proof and CTA layers
- keep one dominant action and one quieter secondary action
Implemented:
- removed the floating pill from the rendered hero
- turned proof chips into a visible trust row
- softened the review CTA treatment
- changed the secondary CTA from a full outline button to a quieter text action
- softened the hero overlay for a calmer premium tone
Files:
- `src/lib/components/HeroSection.svelte`
- `src/lib/styles/sections.css`
- `src/lib/styles/responsive.css`
Motive:
- clearer hierarchy
- less visual competition
- more premium restraint
### Service card decision reduction
Proposed:
- reduce the two-CTA pattern on each service card
- help users qualify the right service faster
- make the section feel like guidance toward the right care relationship, not a pricing grid
Implemented:
- added “best for” qualification copy to core services
- made the service title the detail path
- removed the explicit secondary detail CTA from the action stack
- kept one dominant CTA: `Book a Meet & Greet`
- added softer exploratory CTAs tailored to each service
- introduced a subtle featured treatment for Pack Walks as the signature Tiny Gang offer
- softened pricing so it reads as supporting information rather than the main focal point
- added a small in-card `What to expect` disclosure for progressive exploration
- rewrote service support copy to sound more experiential and more recognisably Goodwalk
- shifted the card reading pattern toward Apple-like editorial hierarchy: centred service titles, left-aligned supporting copy, left-aligned cues, and left-aligned disclosure content
- increased the presence of Goodwalk green and yellow in badges and icon treatments so the section feels more ownable
- tightened internal spacing so the left-aligned content reads more like an editorial card than a converted centred layout
- fixed the icon treatment so the icons inside the green bubbles render in Goodwalk yellow consistently
- changed the desktop card group to behave more like a connected service gateway surface, with clearer tile-level affordance toward the service pages and a separate booking action beneath
- added subtle per-card tinting so the joined desktop group still gives each service its own identity
- changed `What to expect` into a desktop popover-style disclosure so it no longer disturbs the surrounding card layout, while keeping a normal stacked disclosure on mobile
Files:
- `src/lib/components/ServicesSection.svelte`
Motive:
- lowers micro-decision fatigue
- improves service comprehension
- keeps the cards calmer and more conversion-oriented
### Testimonials simplification
Proposed:
- replace the busier carousel treatment with calmer editorial proof
- remove decorative elements that weaken quiet confidence
Implemented:
- replaced the carousel structure with one featured testimonial plus two supporting testimonial cards
- removed arrows and the `WOOF` decorative motif
- kept the Google reviews CTA as the primary proof action
- demoted Instagram to a lower-emphasis follow-up link
Files:
- `src/lib/components/TestimonialsSection.svelte`
Motive:
- trust should feel effortless, not performative
- calmer proof improves credibility
- simpler structure improves readability on desktop and mobile
### Founder section reframing
Proposed:
- frame the founder section around trust value instead of biography
Implemented:
- changed the kicker from `Founder story` to `Why owners trust Aless`
- adjusted the mobile supporting caption toward relationship-led care
Files:
- `src/lib/components/FounderStorySection.svelte`
Motive:
- preserves warmth
- makes the section more conversion-relevant
### Booking step-one friction reduction
Proposed:
- make step one feel lighter
- avoid presenting general enquiry as equal-weight complexity
- reduce repeated service selection when intent is already known
Implemented:
- replaced the prominent enquiry-type radio group with lighter inline switching
- added a simpler general-enquiry alternate path
- added a locked service summary state when a service is preselected from a service card
- added a `Change` action instead of forcing the full selector immediately
Files:
- `src/lib/components/BookingSection.svelte`
- `src/lib/styles/forms.css`
- `src/lib/styles/responsive.css`
Motive:
- lowers perceived effort
- improves continuity from service card to form
- keeps flexibility without overloading first-time visitors
### Navigation ribbon simplification
Proposed:
- reduce the ribbon from multiple simultaneous trust claims to one calm reassurance line
Implemented:
- replaced the three-part ribbon with one single message
- simplified ribbon spacing and mobile behavior
Files:
- `src/lib/components/Header.svelte`
- `src/lib/styles/layout.css`
Motive:
- reduces visual noise
- improves header calmness
- supports the “quiet confidence” direction
### Sticky mobile CTA softening
Proposed:
- keep the mobile CTA behavior
- make the visual treatment less app-like
Implemented:
- reduced blur
- reduced shadow weight
- slightly reduced CTA scale
- softened spacing and prominence
Files:
- `src/lib/components/MobileBookBar.svelte`
Motive:
- preserves conversion utility
- reduces mobile pressure
## Proposed But Not Implemented In This Pass
### Full design-token normalization
Proposed:
- formalize card families
- normalize radius and shadow tokens site-wide
- standardize section spacing more broadly
Status:
- not fully implemented
Reason:
- requires a broader system sweep than this homepage-focused refinement pass
### Values section copy rewrite
Proposed:
- make every value explicitly outcome-led for owners
Status:
- not implemented
Reason:
- content should be reviewed carefully rather than inferred too aggressively in code
### FAQ and areas structural split
Proposed:
- separate service-area information from FAQs into clearer blocks
Status:
- not implemented
Reason:
- lower impact than upper-funnel and booking refinements
### Deeper founder copy restructuring
Proposed:
- compress narrative into shorter paragraphs and trust-led bullets
Status:
- partially implemented through framing changes only
Reason:
- needs a more deliberate copy pass
## Verification Notes
Verified in this pass:
- homepage section order
- changed component structure
- style/class consistency for edited sections
Not fully verified in this pass:
- unrestricted `npm run check`
Reason:
- previous full check attempts were interrupted by sandbox and permission-related environment issues rather than template-level errors
## Summary
This pass moves the site toward:
- calmer hierarchy
- stronger service comprehension
- better trust pacing
- lower-friction action
- a more premium and less over-instrumented feel
It does that without redesigning the brand from scratch.
+32
View File
@@ -30,6 +30,7 @@
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^22.10.5",
"@types/pg": "^8.11.10",
"embla-carousel-svelte": "^8.6.0",
"jsdom": "^29.1.1",
"svelte": "^5.16.0",
"svelte-check": "^4.1.1",
@@ -2505,6 +2506,37 @@
"dev": true,
"license": "MIT"
},
"node_modules/embla-carousel": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"dev": true,
"license": "MIT"
},
"node_modules/embla-carousel-reactive-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz",
"integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"embla-carousel": "8.6.0"
}
},
"node_modules/embla-carousel-svelte": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel-svelte/-/embla-carousel-svelte-8.6.0.tgz",
"integrity": "sha512-ZDsKk8Sdv+AUTygMYcwZjfRd1DTh+JSUzxkOo8b9iKAkYjg+39mzbY/lwHsE3jXSpKxdKWS69hPSNuzlOGtR2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"embla-carousel": "8.6.0",
"embla-carousel-reactive-utils": "8.6.0"
},
"peerDependencies": {
"svelte": "^3.49.0 || ^4.0.0 || ^5.0.0"
}
},
"node_modules/entities": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+1
View File
@@ -34,6 +34,7 @@
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^22.10.5",
"@types/pg": "^8.11.10",
"embla-carousel-svelte": "^8.6.0",
"jsdom": "^29.1.1",
"svelte": "^5.16.0",
"svelte-check": "^4.1.1",
+10
View File
@@ -391,12 +391,21 @@
padding: 60px 0;
}
.about-eyebrow {
display: block;
width: fit-content;
margin-left: auto;
margin-right: auto;
text-align: center;
}
.about-section-grid {
gap: 28px;
}
.about-copy h2 {
font-size: 28px;
text-align: center;
}
.about-copy p {
@@ -415,6 +424,7 @@
.about-founder-copy h2 {
font-size: 26px;
line-height: 1.02;
text-align: center;
}
.about-founder-heading-desktop {
+56 -60
View File
@@ -21,10 +21,10 @@
messagePlaceholder: string;
}
> = {
'Pack Walks': {
'Tiny Gang Pack Walks': {
intro:
'Tell us about your dog, your area, and how they feel around other dogs so we can see if Pack Walks are the right fit.',
messageLabel: 'Pack Walks fit',
'Tell us about your dog, your area, and how they feel around other dogs so we can see if Tiny Gang Pack Walks are the right fit.',
messageLabel: 'Tiny Gang Pack Walks fit',
messagePlaceholder:
'How old is your dog, how do they feel in groups, and is there anything about confidence, recall, or social behaviour we should know?'
},
@@ -75,6 +75,7 @@
let submitted = false;
let showErrorModal = false;
let submitErrorDetail = '';
let showServicePicker = false;
function validateEmail(raw: string): string {
const value = raw.trim();
@@ -134,6 +135,7 @@
? 'Tell us if this is feedback, a complaint, a business enquiry, or anything else we should know.'
: activeServicePrompt?.messagePlaceholder || 'Describe your pet, any special needs, or anything we should know.';
$: successPetName = petName.trim() || 'your dog';
$: serviceChoiceLocked = !isGeneralEnquiry && selectedServices.length === 1 && !showServicePicker;
onMount(() => {
const now = Date.now();
@@ -241,6 +243,7 @@
}
selectedServices = [requestedService];
showServicePicker = false;
try {
window.sessionStorage.removeItem(requestedServiceStorageKey);
@@ -283,6 +286,7 @@
petName = '';
location = '';
selectedServices = [];
showServicePicker = false;
}
errors = {};
}
@@ -438,7 +442,7 @@
{/if}
<div class="booking-header">
<span class="booking-eyebrow">{bookingEyebrow}</span>
<span class="eyebrow booking-eyebrow">{bookingEyebrow}</span>
<h2 class="booking-title">
<span class="booking-title-plain">{headingParts.plain}</span>{' '}
<span class="booking-title-highlight">{headingParts.highlight}</span>
@@ -510,38 +514,12 @@
class:booking-card-grid-with-banner={Boolean(detailsStepIntro)}
class="booking-card-grid booking-card-grid-dog"
>
{#if allowGeneralEnquiry}
<div class="booking-field-card booking-field-card-full">
<label>
<Icon name="fas fa-comments" />&nbsp;Enquiry type
</label>
<div class="booking-toggle-group" role="radiogroup" aria-label="Enquiry type">
<label class="booking-toggle-option">
<input
type="radio"
name="enquiryType"
value="booking"
checked={enquiryType === 'booking'}
on:change={() => setEnquiryType('booking')}
/>
<span class="booking-toggle-indicator" aria-hidden="true"></span>
<span>Book a Meet &amp; Greet</span>
</label>
<label class="booking-toggle-option">
<input
type="radio"
name="enquiryType"
value="general"
checked={enquiryType === 'general'}
on:change={() => setEnquiryType('general')}
/>
<span class="booking-toggle-indicator" aria-hidden="true"></span>
<span>General enquiry</span>
</label>
</div>
<p class="booking-help-text">
General enquiries cover feedback, complaints, business enquiries, and other non-booking messages.
</p>
{#if allowGeneralEnquiry && !isGeneralEnquiry}
<div class="booking-inline-switch booking-field-card-full">
<span>Need help with something else?</span>
<button type="button" class="booking-inline-link" on:click={() => setEnquiryType('general')}>
Send a general enquiry
</button>
</div>
{/if}
@@ -594,6 +572,40 @@
{/if}
</div>
{#if hasServices}
<div class="booking-field-stack booking-field-stack-full">
<div class="booking-selected-service-row">
<span class="booking-service-label"><Icon name="fas fa-paw" />&nbsp;Service</span>
{#if serviceChoiceLocked}
<button type="button" class="booking-inline-link" on:click={() => (showServicePicker = true)}>
Change
</button>
{/if}
</div>
{#if serviceChoiceLocked}
<div class="booking-selected-service-chip">{selectedServices[0]}</div>
{:else}
<div class="booking-service-options">
{#each booking.serviceOptions as service}
<label class="booking-check-option">
<input
type="checkbox"
name="services"
value={service}
checked={selectedServices.includes(service)}
on:change={(event) =>
toggleService(service, (event.currentTarget as HTMLInputElement).checked)}
/>
<span class="booking-check-box" aria-hidden="true"></span>
<span>{service}</span>
</label>
{/each}
</div>
{/if}
</div>
{/if}
<div
class="booking-field-stack booking-field-stack-full"
class:booking-field-stack-invalid={errors.message}
@@ -617,33 +629,17 @@
</p>
{/if}
</div>
{#if hasServices}
<div class="booking-field-stack booking-field-stack-full">
<span class="booking-service-label"><Icon name="fas fa-paw" />&nbsp;Services</span>
<div class="booking-service-options">
{#each booking.serviceOptions as service}
<label class="booking-check-option">
<input
type="checkbox"
name="services"
value={service}
checked={selectedServices.includes(service)}
on:change={(event) =>
toggleService(service, (event.currentTarget as HTMLInputElement).checked)}
/>
<span class="booking-check-box" aria-hidden="true"></span>
<span>{service}</span>
</label>
{/each}
</div>
</div>
{/if}
</div>
</div>
{/if}
{#if isGeneralEnquiry}
<div class="booking-inline-switch booking-field-card-full">
<span>Want to book a Meet &amp; Greet instead?</span>
<button type="button" class="booking-inline-link" on:click={() => setEnquiryType('booking')}>
Switch back to booking
</button>
</div>
<div
class="booking-field-card booking-field-card-full"
class:booking-field-card-invalid={errors.message}
@@ -673,7 +669,7 @@
</div>
<div class="booking-actions booking-actions-next">
<button type="button" class="btn btn-yellow booking-next-button" on:click={goToOwnerStep}>
<button type="button" class="btn btn-yellow btn-with-arrow booking-next-button" on:click={goToOwnerStep}>
Next: {ownerStepLabel.toLowerCase()}
<Icon name="fas fa-arrow-right" />
</button>
@@ -784,7 +780,7 @@
>
Back
</button>
<button type="submit" class="btn btn-yellow booking-submit-button" disabled={submitting}>
<button type="submit" class="btn btn-yellow btn-with-arrow booking-submit-button" disabled={submitting}>
{#if submitting}
Sending…
{:else if isGeneralEnquiry}
+4 -4
View File
@@ -16,7 +16,7 @@ async function fillOwnerStep() {
}
async function fillDogStep() {
await fireEvent.click(screen.getByLabelText('Pack Walks'));
await fireEvent.click(screen.getByLabelText('Tiny Gang Pack Walks'));
await fireEvent.click(screen.getByLabelText('Other Services'));
await fireEvent.input(screen.getByLabelText(/Dog's Name/i), {
target: { value: 'Maya' }
@@ -24,7 +24,7 @@ async function fillDogStep() {
await fireEvent.input(screen.getByLabelText(/Location/i), {
target: { value: 'Kingsland' }
});
await fireEvent.input(screen.getByLabelText(/Pack Walks fit/i), {
await fireEvent.input(screen.getByLabelText(/Tiny Gang Pack Walks fit/i), {
target: { value: 'Loves small group walks.' }
});
}
@@ -103,7 +103,7 @@ describe('BookingSection', () => {
petName: 'Maya',
location: 'Kingsland',
message: 'Loves small group walks.',
services: ['Pack Walks', 'Other Services'],
services: ['Tiny Gang Pack Walks', 'Other Services'],
website: '',
referrer: 'https://www.google.com/',
stepChanges: 1,
@@ -139,7 +139,7 @@ describe('BookingSection', () => {
await fireEvent.click(screen.getByLabelText(/General enquiry/i));
expect(screen.queryByLabelText(/Dog's Name/i)).not.toBeInTheDocument();
expect(screen.queryByText('Pack Walks')).not.toBeInTheDocument();
expect(screen.queryByText('Tiny Gang Pack Walks')).not.toBeInTheDocument();
await fireEvent.click(container.querySelector('.booking-next-button')!);
expect(screen.getByText('Please tell us how we can help')).toBeInTheDocument();
+4 -3
View File
@@ -10,10 +10,11 @@
const desktop = logoDesktop as Picture;
export let preview = false;
$: onboardingPageHref = preview ? '/?preview=onboarding' : '/';
const ownerEmail = 'info@goodwalk.co.nz';
const ownerPhone = '(022) 642 1011';
const services = ['Pack Walks', '1:1 Walks', 'Puppy Visits'];
const services = ['Tiny Gang Pack Walks', '1:1 Walks', 'Puppy Visits'];
const visitStartedStorageKey = 'goodwalk_visit_started_at';
const draftStorageKey = 'goodwalk_contract_draft';
@@ -422,7 +423,7 @@
<div class="journey-bar">
<div class="contract-shell journey-bar-inner">
<a href="/?preview=onboarding" class="journey-stage" class:journey-done={onboardingCompleted} class:journey-current={!onboardingCompleted}>
<a href={onboardingPageHref} class="journey-stage" class:journey-done={onboardingCompleted} class:journey-current={!onboardingCompleted}>
<span class="journey-stage-icon">
{#if onboardingCompleted}
<Icon name="fas fa-check" />
@@ -465,7 +466,7 @@
<div>
<strong>You haven't completed your onboarding form yet.</strong>
Aless will need this before your service can start.
<a href="/?preview=onboarding">Complete it now →</a>
<a href={onboardingPageHref}>Complete it now →</a>
</div>
</div>
</div>
+7 -3
View File
@@ -18,7 +18,10 @@
<span class="cta-card__eyebrow">{eyebrow}</span>
<h2>{title}</h2>
<p class="cta-card__desc">{description}</p>
<a class="btn btn-yellow btn-mobile-center cta-card__btn" href={ctaHref}>{ctaLabel}</a>
<a class="btn btn-yellow btn-mobile-center btn-with-arrow cta-card__btn" href={ctaHref}>
{ctaLabel}
<Icon name="fas fa-arrow-right" />
</a>
{#if email || phone}
<div class="cta-card__links">
{#if email}
@@ -52,8 +55,9 @@
margin-bottom: 14px;
padding: 7px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
color: #fff;
background: rgba(255, 209, 0, 0.12);
box-shadow: inset 0 0 0 1px rgba(255, 209, 0, 0.2);
color: var(--yellow);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
+7 -1
View File
@@ -14,6 +14,10 @@
const aboutLink: LinkItem = { label: 'About Us', href: '/about' };
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function withAboutLink(links: LinkItem[]) {
if (links.some((link) => link.href === aboutLink.href || link.label === aboutLink.label)) {
return links;
@@ -105,6 +109,8 @@
<a href="/terms-and-conditions">Terms &amp; Conditions</a>
<a href="/privacy-policy">Privacy Policy</a>
</nav>
<a href="#" class="footer-back-top" aria-label="Back to top">↑ Back to top</a>
<button type="button" class="footer-back-top" aria-label="Back to top" on:click={scrollToTop}>
↑ Back to top
</button>
</div>
</footer>
+185 -193
View File
@@ -1,254 +1,246 @@
<script lang="ts">
import { reveal } from '$lib/actions/reveal';
import Icon from '$lib/components/Icon.svelte';
import { getEnhancedImage } from '$lib/enhanced-images';
import type { FounderStoryContent } from '$lib/types';
export let founderStory: FounderStoryContent;
const founderStoryParagraphs = [
'Goodwalk started with my own little dog and the kind of relationship I have always had with animals. Growing up in Italy with a German Shepherd, I saw early on how much joy, comfort, personality, and companionship dogs bring into a home. They are not just pets. They become part of your family and your daily life.',
'When I moved to Auckland, I noticed a lot of dog walking felt rushed, overcrowded, or impersonal, especially for smaller dogs. So I built Goodwalk around the kind of care I would want for my own dog: familiar faces, safe and social little groups, lots of fun, and genuine relationships with every dog we walk.',
'The Tiny Gang is built around routine, trust, and dogs having the absolute best part of their day together. Older dogs help younger ones settle in, nervous dogs build confidence, and playful dogs get to burn energy with their friends.',
'You know exactly who is caring for your dog. Your dog knows who is at the door. And you come home to a happy, fulfilled dog that has had a proper adventure. Ready to join the Tiny Gang?'
];
$: founderStoryEnhanced = getEnhancedImage(founderStory.imageUrl);
</script>
<section id="promise">
<div class="promise-inner">
<div class="promise-text">
<span class="promise-kicker">Founder story</span>
<div class="promise-mobile-intro">
<div class="promise-mobile-avatar">
<section id="promise" use:reveal={{ delay: 20 }} class="reveal-block">
<div class="founder-inner">
<article class="founder-note">
<span class="eyebrow founder-kicker">A note from Aless</span>
<h2 class="founder-heading">
<span class="founder-heading-main">Hi, Welcome to Goodwalk.</span>
</h2>
<div class="founder-body">
{#each founderStoryParagraphs as paragraph}
<p>{paragraph}</p>
{/each}
</div>
<div class="founder-signoff">
<div class="founder-avatar">
{#if founderStoryEnhanced}
<enhanced:img
class="founder-avatar-img"
src={founderStoryEnhanced}
alt={founderStory.imageAlt}
loading="lazy"
decoding="async"
/>
{:else}
<img src={founderStory.imageUrl} alt={founderStory.imageAlt} loading="lazy" decoding="async" />
<img
class="founder-avatar-img"
src={founderStory.imageUrl}
alt={founderStory.imageAlt}
loading="lazy"
decoding="async"
/>
{/if}
</div>
<p class="promise-mobile-caption">Auckland Central walks led personally by Aless.</p>
<div class="founder-signoff-text">
<span class="founder-name">Aless</span>
<span class="founder-role">Goodwalk founder</span>
</div>
</div>
<h2 class="promise-heading">
<span class="promise-heading-desktop">
<span class="promise-title-main">{founderStory.title}</span>
<br />
<span class="promise-title-highlight">{founderStory.subtitle}</span>
</span>
<span class="promise-heading-mobile">
<span class="promise-title-main">{founderStory.title}</span>
<span class="promise-title-highlight">{founderStory.subtitle}</span>
</span>
</h2>
<a class="founder-contact-note" href="mailto:info@goodwalk.co.nz" aria-label="Email Aless at Goodwalk">
<span class="founder-contact-wave" aria-hidden="true">👋</span>
<span>If you are unsure about anything, feel free to email me anytime.</span>
</a>
{#each founderStory.body as paragraph, idx}
<p>
{paragraph}
{#if idx === founderStory.body.length - 1}
<strong>{founderStory.emphasis}</strong>
{/if}
</p>
{/each}
<a href={founderStory.cta.href} class="btn btn-green">{founderStory.cta.label}</a>
</div>
<div class="promise-img">
<div class="promise-img-frame">
{#if founderStoryEnhanced}
<enhanced:img
src={founderStoryEnhanced}
alt={founderStory.imageAlt}
loading="lazy"
decoding="async"
/>
{:else}
<img src={founderStory.imageUrl} alt={founderStory.imageAlt} loading="lazy" decoding="async" />
{/if}
</div>
</div>
<a href={founderStory.cta.href} class="btn btn-green btn-with-arrow founder-cta">
{founderStory.cta.label}
<Icon name="fas fa-arrow-right" />
</a>
</article>
</div>
</section>
<style>
.promise-kicker {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0 0 14px;
padding: 8px 14px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.07);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
/* A quiet, letter-style sign-off — left-aligned, one clean panel, no ornament. */
.founder-inner {
max-width: var(--max-w);
margin: 0 auto;
padding: 0 50px;
}
.promise-heading {
margin: 0 0 22px;
max-width: 14ch;
.founder-note {
max-width: 680px;
margin: 0 auto;
padding: 42px 52px 34px;
background: #fff;
border: 1px solid rgba(17, 20, 24, 0.08);
border-radius: 28px;
box-shadow: var(--shadow-panel-elevated);
}
.promise-mobile-intro {
display: none;
}
.promise-heading-desktop {
.founder-kicker {
display: block;
letter-spacing: 0.14em;
}
.promise-heading-mobile {
display: none;
.founder-heading {
margin: 0 0 26px;
}
.promise-heading-mobile .promise-title-main,
.promise-heading-mobile .promise-title-highlight {
display: block;
}
.promise-title-main {
display: block;
margin-bottom: 8px;
color: rgba(13, 26, 13, 0.68);
font-family: var(--font-head);
font-size: clamp(15px, 1.3vw, 18px);
font-weight: 700;
letter-spacing: 0.01em;
line-height: 1.15;
}
.promise-title-highlight {
.founder-heading-main {
display: block;
color: #0d1a0d;
font-size: clamp(42px, 5.2vw, 64px);
font-family: var(--font-head);
font-size: clamp(32px, 3.6vw, 46px);
font-weight: 800;
letter-spacing: -0.05em;
line-height: 0.96;
text-wrap: balance;
letter-spacing: -0.04em;
line-height: 1;
}
.promise-text {
position: relative;
z-index: 2;
.founder-body {
display: grid;
gap: 16px;
}
.founder-body p {
margin: 0;
color: #4c5056;
font-size: var(--body-copy-size);
line-height: 1.75;
}
.founder-signoff {
display: flex;
align-items: center;
gap: 14px;
margin-top: 28px;
padding-top: 24px;
border-top: 1px solid rgba(17, 20, 24, 0.08);
}
.founder-avatar {
flex: 0 0 auto;
width: 58px;
height: 58px;
overflow: hidden;
border-radius: 50%;
background: #ede4d2;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.07);
}
.founder-avatar-img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 20%;
}
.founder-signoff-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.founder-name {
color: #0d1a0d;
font-family: var(--font-head);
font-size: 16px;
font-weight: 700;
line-height: 1.2;
}
.founder-role {
color: var(--gray);
font-size: 13px;
line-height: 1.3;
}
.founder-cta {
margin-top: 28px;
}
.founder-contact-note {
display: inline-flex;
align-items: center;
gap: 10px;
margin-top: 20px;
padding: 12px 16px;
border-radius: 18px;
background: rgba(33, 48, 33, 0.05);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
color: var(--gw-green);
font-size: 14px;
font-weight: 600;
line-height: 1.5;
text-decoration: none;
transition: background 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
}
.founder-contact-wave {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: #fff;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.07);
font-size: 16px;
flex: 0 0 auto;
}
@media (hover: hover) {
.founder-contact-note:hover {
background: rgba(33, 48, 33, 0.08);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.08),
0 10px 22px rgba(17, 20, 24, 0.05);
transform: translateY(-1px);
}
}
@media (max-width: 768px) {
#promise {
padding-top: 42px;
padding-bottom: var(--space-section-featured-y);
.founder-inner {
padding: 0 var(--space-container-x-mobile);
}
.promise-kicker {
margin-bottom: 12px;
padding: 7px 12px;
font-size: 11px;
.founder-note {
padding: 26px 24px 24px;
border-radius: 24px;
}
.promise-text {
width: 100%;
margin-top: 0;
padding: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.promise-heading {
max-width: none;
margin-bottom: 22px;
.founder-heading {
margin: 0 0 22px;
text-align: center;
}
.promise-heading-desktop {
display: none;
.founder-body p {
font-size: var(--body-copy-size-mobile);
line-height: 1.7;
}
.promise-heading-mobile {
.founder-contact-note {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.promise-heading-mobile .promise-title-main,
.promise-heading-mobile .promise-title-highlight {
display: inline-block;
width: fit-content;
margin-left: auto;
margin-right: auto;
}
.promise-mobile-intro {
display: flex;
align-items: center;
gap: 14px;
margin: 0 0 16px;
padding: 14px 16px;
border-radius: 22px;
background: linear-gradient(180deg, #fbf6e8 0%, #efe4c8 100%);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.65),
0 10px 22px rgba(17, 20, 24, 0.06),
inset 0 0 0 1px rgba(242, 191, 47, 0.12);
}
.promise-mobile-avatar {
flex: 0 0 auto;
width: 68px;
height: 68px;
overflow: hidden;
border-radius: 20px;
box-shadow: 0 8px 18px rgba(17, 20, 24, 0.08);
}
.promise-mobile-avatar img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 20%;
font-size: 13px;
padding: 11px 14px;
text-align: center;
}
.promise-mobile-caption {
margin: 0;
color: #34363a;
font-family: var(--font-head);
font-size: 15px;
font-weight: 600;
line-height: 1.4;
}
.promise-title-main {
margin-bottom: 0;
font-size: 14px;
letter-spacing: 0.02em;
}
.promise-title-highlight {
font-size: clamp(36px, 11vw, 54px);
line-height: 0.98;
}
.promise-text p,
.promise-text .btn {
margin-left: 0;
margin-right: 0;
}
.promise-text p {
text-align: left;
}
.promise-text .btn {
.founder-cta {
width: 100%;
justify-content: center;
}
.promise-img {
display: none;
}
}
</style>
+50 -28
View File
@@ -12,6 +12,10 @@
export let navigation: NavigationContent;
// Desktop nav is split either side of the centre logo.
$: leftLinks = navigation.desktopLinks.slice(0, 2);
$: rightLinks = navigation.desktopLinks.slice(2);
let mobileMenuOpen = false;
let headerElement: HTMLElement;
let mobileMenuTop = 0;
@@ -44,6 +48,7 @@
if (href === '/pack-walks') return 'fas fa-paw';
if (href === '/dog-walking') return 'fas fa-person-walking';
if (href === '/puppy-visits') return 'fas fa-dog';
if (href === '/testimonials') return 'fas fa-star';
if (href === '/our-pricing') return 'fas fa-tags';
if (href === '/about') return 'fas fa-heart';
if (href === '/contact-us') return 'fas fa-envelope';
@@ -130,8 +135,8 @@
<header bind:this={headerElement}>
<nav>
<ul class="nav-links">
{#each navigation.desktopLinks as link, i}
<ul class="nav-links nav-links-left">
{#each leftLinks as link, i}
<li class:has-mega={i === 0 && navigation.megaMenuServices?.length}>
<a
href={link.href}
@@ -201,27 +206,43 @@
</picture>
</a>
<a href={`tel:${mobilePhoneHref}`} class="mobile-phone" aria-label={`Call Goodwalk on ${mobilePhoneDisplay}`}>
<Icon name="fas fa-phone" />
<span>{mobilePhoneDisplay}</span>
</a>
<div class="nav-end">
<ul class="nav-links nav-links-right">
{#each rightLinks as link}
<li>
<a
href={link.href}
target={linkTarget(link.external)}
rel={linkRel(link.external)}
aria-current={ariaCurrent(link.href)}
class:nav-link-active={isActiveLink(link.href)}
>
{link.label}
</a>
</li>
{/each}
</ul>
<div class="nav-right">
{#if navigation.instagram}
<a
href={navigation.instagram.href}
target={linkTarget(navigation.instagram.external)}
rel={linkRel(navigation.instagram.external)}
class="instagram-icon"
aria-label="Instagram"
>
<Icon name="fab fa-instagram" />
<div class="nav-right">
<a href={`tel:${mobilePhoneHref}`} class="mobile-phone" aria-label={`Call Goodwalk on ${mobilePhoneDisplay}`}>
<Icon name="fas fa-phone" />
</a>
{/if}
<a
href={navigation.cta.href}
class="btn btn-yellow"
>{navigation.cta.label}</a>
{#if navigation.instagram}
<a
href={navigation.instagram.href}
target={linkTarget(navigation.instagram.external)}
rel={linkRel(navigation.instagram.external)}
class="instagram-icon"
aria-label="Instagram"
>
<Icon name="fab fa-instagram" />
</a>
{/if}
<a
href={navigation.cta.href}
class="btn btn-yellow"
>{navigation.cta.label}</a>
</div>
</div>
<button
@@ -238,11 +259,7 @@
{#if $page.url.pathname === '/'}
<div class="nav-ribbon">
<span class="nav-ribbon-item"><Icon name="fas fa-paw" />Small &amp; Medium Dog Specialists</span>
<span class="nav-ribbon-divider"></span>
<span class="nav-ribbon-item"><Icon name="fas fa-handshake" />Free Meet &amp; Greet</span>
<span class="nav-ribbon-divider"></span>
<span class="nav-ribbon-item"><Icon name="fas fa-van-shuttle" />Free Pickup &amp; Drop-off</span>
<span class="nav-ribbon-item"><Icon name="fas fa-handshake" />Free Meet &amp; Greet for Auckland Central dogs</span>
</div>
{/if}
@@ -250,9 +267,14 @@
class:open={mobileMenuOpen}
class="mobile-menu-shell"
style={`--mobile-menu-top: ${mobileMenuTop}px;`}
on:click={closeMenu}
>
<div class="mobile-menu" id="mobile-menu" on:click|stopPropagation>
<button
type="button"
class="mobile-menu-backdrop"
aria-label="Close menu"
on:click={closeMenu}
></button>
<div class="mobile-menu" id="mobile-menu">
<div class="mobile-menu-links">
{#each navigation.mobileLinks as link}
<a
+25 -35
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
import { getEnhancedImage } from '$lib/enhanced-images';
import type { CallToAction, HeroContent } from '$lib/types';
export let hero: HeroContent;
@@ -11,7 +10,7 @@
$: mobileLead = mobileTitle.includes(hero.highlight)
? mobileTitle.slice(0, mobileTitle.lastIndexOf(hero.highlight))
: mobileTitle;
$: heroEnhanced = getEnhancedImage(hero.imageUrl);
$: proofItems = (hero.subtitleChips ?? []).slice(0, 3);
function splitTitle(title: string) {
const trimmed = title.trim();
@@ -56,10 +55,6 @@
</picture>
</div>
{#if hero.floatingPill}
<div class="hero-floating-pill">{hero.floatingPill}</div>
{/if}
<div class="hero-inner">
<div class="hero-text">
{#if hero.kicker}
@@ -84,55 +79,50 @@
<p class="hero-subtitle hero-subtitle-desktop">{hero.subtitle}</p>
{/if}
{#if hero.subtitleChips && hero.subtitleChips.length}
<div class="hero-chips">
{#each hero.subtitleChips as chip}
{#if proofItems.length || reviewCta}
<div class="hero-chips" aria-label="Why owners choose Goodwalk">
{#each proofItems as chip}
<span class="hero-chip">
<Icon name={chip.icon} />
{chip.label}
</span>
{/each}
{#if reviewCta}
<a
class="hero-trust-chip"
href={reviewCta.href}
target={reviewCta.external ? '_blank' : undefined}
rel={reviewCta.external ? 'noopener' : undefined}
aria-label="Read our five-star Google reviews"
>
<img
class="hero-trust-logo"
src="/images/google-g-logo.svg"
alt=""
width="18"
height="19"
/>
<span>{reviewCta.label}</span>
</a>
{/if}
</div>
{/if}
{#if reviewCta}
<a
class="hero-trust-chip"
href={reviewCta.href}
target={reviewCta.external ? '_blank' : undefined}
rel={reviewCta.external ? 'noopener' : undefined}
aria-label="Read our five-star Google reviews"
>
<img
class="hero-trust-logo"
src="/images/google-g-logo.svg"
alt=""
width="18"
height="19"
/>
<span class="hero-trust-stars" aria-hidden="true">
{#each Array(5) as _}
<Icon name="fas fa-star" />
{/each}
</span>
<span>{reviewCta.label}</span>
</a>
{/if}
<div class="hero-buttons">
<a
href={hero.primaryCta.href}
target={linkTarget(hero.primaryCta.external)}
rel={linkRel(hero.primaryCta.external)}
class="btn btn-yellow"
class="btn btn-yellow btn-with-arrow"
>
{hero.primaryCta.label}
<Icon name="fas fa-arrow-right" />
</a>
<a
href={hero.secondaryCta.href}
target={linkTarget(hero.secondaryCta.external)}
rel={linkRel(hero.secondaryCta.external)}
class="btn btn-outline"
class="hero-secondary-link"
>
{hero.secondaryCta.label}
<Icon name="fas fa-arrow-down" className="hero-cta-arrow" />
+100 -43
View File
@@ -4,19 +4,34 @@
import type { HowItWorksContent } from '$lib/types';
export let content: HowItWorksContent;
const journeyChips = [
{ icon: 'fas fa-handshake', label: 'Free Meet & Greet' },
{ icon: 'fas fa-clipboard-check', label: 'Assessment walks' },
{ icon: 'fas fa-calendar-check', label: 'A regular weekly rhythm' }
];
</script>
<section id="how-it-works" use:reveal={{ delay: 30 }} class="reveal-block">
<div class="hiw-inner">
<div class="hiw-header">
<span class="hiw-eyebrow">Getting started</span>
<div class="section-header hiw-header">
<span class="eyebrow hiw-eyebrow">Getting started</span>
<h2 class="section-heading">{content.title}</h2>
{#if content.intro}
<p class="hiw-intro">{content.intro}</p>
<p class="section-intro hiw-intro">{content.intro}</p>
{/if}
</div>
<div class="hiw-journey-bar" aria-label="How getting started works">
{#each journeyChips as chip}
<span class="hiw-journey-pill">
<Icon name={chip.icon} className="hiw-journey-icon" />
{chip.label}
</span>
{/each}
</div>
<div class="hiw-steps">
{#each content.steps as step, index}
<div class="hiw-step">
@@ -40,7 +55,10 @@
</div>
<div class="hiw-cta">
<a href="#newlead" class="btn btn-green btn-mobile-center">Book your free Meet &amp; Greet</a>
<a href="#newlead" class="btn btn-green btn-mobile-center btn-with-arrow">
Book your free Meet &amp; Greet
<Icon name="fas fa-arrow-right" />
</a>
<p class="hiw-cta-note">Free, no-obligation. We reply within 24 hours.</p>
</div>
@@ -61,30 +79,19 @@
/* ── Header ── */
.hiw-header {
text-align: center;
margin-bottom: 56px;
margin-bottom: 36px;
}
.hiw-eyebrow {
display: inline-block;
margin-bottom: 14px;
padding: 7px 12px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.08);
color: var(--gw-green);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
}
.hiw-intro {
max-width: 580px;
margin: 16px auto 0;
color: #4c5056;
font-size: 16px;
line-height: 1.65;
color: var(--text);
}
/* ── Steps grid ── */
@@ -92,6 +99,19 @@
display: grid;
grid-template-columns: repeat(3, 1fr);
position: relative;
gap: 14px;
margin-top: 28px;
}
.hiw-steps::before {
content: '';
position: absolute;
top: 32px;
left: 13%;
right: 13%;
height: 1px;
background: linear-gradient(90deg, rgba(33, 48, 33, 0.16), rgba(242, 191, 47, 0.4), rgba(33, 48, 33, 0.16));
pointer-events: none;
}
.hiw-step {
@@ -100,22 +120,15 @@
align-items: center;
text-align: center;
padding: 40px 40px 36px;
background: #fff;
background:
radial-gradient(circle at top center, rgba(255, 209, 0, 0.12), transparent 34%),
#fff;
border: 1px solid rgba(17, 20, 24, 0.06);
box-shadow: 0 4px 16px rgba(17, 20, 24, 0.04);
transition: box-shadow 0.22s ease, transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
}
.hiw-step:first-child {
border-radius: 28px 0 0 28px;
}
.hiw-step:last-child {
border-radius: 0 28px 28px 0;
}
.hiw-step + .hiw-step {
border-left: none;
border-radius: 28px;
overflow: hidden;
z-index: 1;
}
@media (hover: hover) {
@@ -171,14 +184,14 @@
.hiw-icon-wrap :global(.hiw-step-icon) {
font-size: 26px;
color: #fff;
color: var(--yellow);
}
/* ── Content ── */
.hiw-title {
margin: 0 0 14px;
font-family: var(--font-head);
font-size: 20px;
font-size: var(--heading-card-size);
font-weight: 700;
line-height: 1.2;
color: #0d1a0d;
@@ -225,6 +238,38 @@
font-size: 13px;
}
.hiw-journey-bar {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
margin: 14px auto 0;
max-width: 880px;
}
.hiw-journey-pill {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 34px;
padding: 0 14px;
border-radius: 999px;
background: var(--gw-green);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.04),
0 10px 22px rgba(17, 20, 24, 0.06);
color: #fff;
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
}
.hiw-journey-pill :global(.hiw-journey-icon) {
color: var(--yellow);
font-size: 11px;
}
/* ── Mobile ── */
@media (max-width: 768px) {
.hiw-inner {
@@ -232,31 +277,43 @@
}
.hiw-header {
margin-bottom: 32px;
margin-bottom: 22px;
}
.hiw-intro {
font-size: 15px;
line-height: 1.55;
max-width: 34ch;
}
.hiw-journey-bar {
justify-content: center;
gap: 8px;
margin-top: 14px;
}
.hiw-journey-pill {
min-height: 32px;
padding: 0 12px;
font-size: 11px;
}
.hiw-steps {
grid-template-columns: 1fr;
gap: 12px;
margin-top: 20px;
}
.hiw-steps::before {
display: none;
}
.hiw-step {
align-items: flex-start;
text-align: left;
padding: 28px 24px;
border-radius: 24px !important;
border-radius: 24px;
border: 1px solid rgba(17, 20, 24, 0.06);
}
.hiw-step + .hiw-step {
border-left: 1px solid rgba(17, 20, 24, 0.06);
}
.hiw-step-meta {
justify-content: flex-start;
margin-bottom: 20px;
@@ -274,16 +331,16 @@
}
.hiw-title {
font-size: 18px;
font-size: var(--heading-card-size-mobile);
}
.hiw-body {
font-size: 14px;
font-size: var(--body-copy-size-mobile);
line-height: 1.6;
}
.hiw-cta {
margin-top: 36px;
margin-top: 28px;
}
}
+67 -3
View File
@@ -16,9 +16,12 @@
</script>
<section id="info">
<div class="info-inner">
<div class="info-inner">
<div class="info-block">
<h2><Icon name="fas fa-location-dot" /> {info.title}</h2>
<h2>
<span class="info-heading-icon"><Icon name="fas fa-location-dot" /></span>
{info.title}
</h2>
<p class="info-lead">{info.intro}</p>
<p class="info-support">Regular walks across the inner-west and nearby suburbs.</p>
@@ -47,7 +50,10 @@
</div>
<div class="info-block">
<h2><Icon name="fas fa-circle-question" /> {info.faqTitle}</h2>
<h2>
<span class="info-heading-icon"><Icon name="fas fa-circle-question" /></span>
{info.faqTitle}
</h2>
<div use:accordion class="faq">
{#each info.faqs as faq}
<details>
@@ -61,6 +67,24 @@
</section>
<style>
.info-heading-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
margin-right: 10px;
border-radius: 12px;
background: var(--gw-green);
box-shadow: 0 10px 22px rgba(33, 48, 33, 0.16);
vertical-align: middle;
}
.info-heading-icon :global(.icon) {
color: var(--yellow);
font-size: 16px;
}
.info-lead {
margin-bottom: 8px;
}
@@ -158,7 +182,37 @@
}
@media (max-width: 768px) {
.info-block {
text-align: left;
padding: 24px 20px;
border-radius: 24px;
background: #fff;
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 14px 30px rgba(17, 20, 24, 0.05);
}
.info-lead,
.info-support {
margin-left: 0;
margin-right: 0;
}
.info-block h2 {
text-align: left;
white-space: nowrap;
font-size: clamp(24px, 6.3vw, 28px);
}
.info-heading-icon {
width: 32px;
height: 32px;
margin-right: 8px;
border-radius: 11px;
}
.info-suburb-chips {
justify-content: flex-start;
gap: 8px;
margin-bottom: 22px;
}
@@ -172,11 +226,21 @@
.info-nearby-card {
flex-direction: column;
align-items: flex-start;
text-align: left;
padding: 20px 18px;
}
.info-nearby-cta {
width: 100%;
}
.info-hours-card {
text-align: left;
}
.faq summary,
.faq details p {
text-align: left;
}
}
</style>
+4 -1
View File
@@ -27,7 +27,10 @@
</a>
<div class="intro-trust-copy">
<p>{intro.text}</p>
<p>
<span class="intro-trust-copy-desktop">{intro.text}</span>
<span class="intro-trust-copy-mobile">30+ 5-star Google reviews. Small &amp; medium dog specialists in Auckland.</span>
</p>
<div class="intro-trust-meta">
<div class="intro-trust-stars" aria-label="5 star rating">
+17 -2
View File
@@ -242,8 +242,9 @@
}
.loc-hero-eyebrow {
background: rgba(255, 255, 255, 0.14);
color: #fff;
background: rgba(255, 209, 0, 0.12);
box-shadow: inset 0 0 0 1px rgba(255, 209, 0, 0.2);
color: var(--yellow);
}
/* ── Hero ── */
@@ -714,6 +715,20 @@
gap: 12px;
}
.loc-eyebrow,
.loc-hero-eyebrow {
display: block;
width: fit-content;
margin-left: auto;
margin-right: auto;
text-align: center;
}
.loc-section-header h2,
.loc-section-intro {
text-align: center;
}
.loc-highlights {
margin-top: -24px;
padding-bottom: 60px;
+8 -8
View File
@@ -142,11 +142,11 @@
z-index: 50;
display: flex;
justify-content: center;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(10px);
padding: 8px 14px calc(8px + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(6px);
border-top: 1px solid rgba(17, 20, 24, 0.08);
box-shadow: 0 -10px 28px rgba(17, 20, 24, 0.1);
box-shadow: 0 -8px 22px rgba(17, 20, 24, 0.08);
opacity: 0;
transform: translateY(110%);
@@ -168,17 +168,17 @@
justify-content: center;
gap: 10px;
width: 100%;
max-width: 460px;
padding: 13px 22px;
max-width: 420px;
padding: 12px 18px;
border-radius: 999px;
background: var(--yellow);
color: #000;
font-family: var(--font-head);
font-size: 15px;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.01em;
text-decoration: none;
box-shadow: 0 8px 18px rgba(255, 209, 0, 0.4);
box-shadow: 0 6px 14px rgba(255, 209, 0, 0.24);
transition:
background 0.18s ease,
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
+5 -4
View File
@@ -3,6 +3,7 @@
import Icon from '$lib/components/Icon.svelte';
export let context: 'onboarding' | 'contract' = 'onboarding';
$: flowLabel = context === 'contract' ? 'contract' : 'onboarding';
const dispatch = createEventDispatcher<{ authenticated: { email: string; profile: Record<string, string>; draft: Record<string, unknown> } }>();
@@ -93,7 +94,7 @@
{#if stage === 'email'}
<h2>Sign in to continue</h2>
<p>Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code.</p>
<p>Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code to continue your {flowLabel}.</p>
<div class="auth-field">
<label for="auth-email">Email address</label>
@@ -112,7 +113,7 @@
<div class="auth-error">{error}</div>
{/if}
<button class="btn btn-yellow auth-btn" on:click={requestCode} disabled={loading}>
<button type="button" class="btn btn-yellow auth-btn" on:click={requestCode} disabled={loading}>
{#if loading}Sending…{:else}Send code <Icon name="fas fa-arrow-right" />{/if}
</button>
@@ -141,11 +142,11 @@
<div class="auth-error">{error}</div>
{/if}
<button class="btn btn-yellow auth-btn" on:click={verifyCode} disabled={loading}>
<button type="button" class="btn btn-yellow auth-btn" on:click={verifyCode} disabled={loading}>
{#if loading}Verifying…{:else}Verify code <Icon name="fas fa-arrow-right" />{/if}
</button>
<button class="auth-back" on:click={goBack}>
<button type="button" class="auth-back" on:click={goBack}>
<Icon name="fas fa-arrow-left" /> Use a different email
</button>
{/if}
+14 -1
View File
@@ -32,7 +32,10 @@
<Icon name="fas fa-arrow-left" />
Back to main site
</a>
<button class="ob-footer-logout" on:click={logout} disabled={loggingOut}>
{#if email}
<span class="ob-footer-email">Signed in as {email}</span>
{/if}
<button type="button" class="ob-footer-logout" on:click={logout} disabled={loggingOut}>
<Icon name="fas fa-right-from-bracket" />
{loggingOut ? 'Signing out…' : 'Sign out'}
</button>
@@ -72,6 +75,16 @@
color: #fff;
}
.ob-footer-email {
margin-left: auto;
min-width: 0;
color: rgba(255, 255, 255, 0.62);
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ob-footer-logout {
display: inline-flex;
align-items: center;
+3 -2
View File
@@ -12,10 +12,11 @@
const mobile = logoMobile as Picture;
export let preview = false;
$: contractPageHref = preview ? '/contract?preview=contract' : '/contract';
const ownerEmail = 'info@goodwalk.co.nz';
const ownerPhone = '(022) 642 1011';
const services = ['Pack Walks', '1:1 Walks', 'Puppy Visits', 'Unsure yet'];
const services = ['Tiny Gang Pack Walks', '1:1 Walks', 'Puppy Visits', 'Unsure yet'];
const visitStartedStorageKey = 'goodwalk_visit_started_at';
const draftStorageKey = 'goodwalk_onboarding_draft';
@@ -453,7 +454,7 @@
<div class="journey-connector" class:journey-connector-done={onboardingCompleted}></div>
<a href="/contract?preview=contract" class="journey-stage" class:journey-done={contractCompleted}>
<a href={contractPageHref} class="journey-stage" class:journey-done={contractCompleted}>
<span class="journey-stage-icon">
{#if contractCompleted}
<Icon name="fas fa-check" />
+9 -1
View File
@@ -269,6 +269,8 @@
.sh-copy {
order: 2;
padding: 44px 24px 48px;
align-items: center;
text-align: center;
}
.sh-title {
@@ -277,11 +279,17 @@
.sh-subtitle {
font-size: 15px;
margin-left: auto;
margin-right: auto;
}
.sh-chips {
justify-content: center;
}
.sh-actions {
flex-direction: column;
align-items: flex-start;
align-items: center;
gap: 12px;
}
+11 -8
View File
@@ -55,14 +55,18 @@
return typeof window !== 'undefined' && window.innerWidth <= 768;
}
function benefitCardScrollLeft(card: HTMLElement) {
return Math.max(0, card.offsetLeft - 8);
}
async function scrollBenefits(direction: -1 | 1) {
if (!benefitScroller || !isMobileViewport()) return;
const nextIndex = Math.max(0, Math.min(activeBenefitIndex + direction, benefitCards.length - 1));
await scrollBenefitTo(nextIndex);
await scrollBenefitTo(nextIndex, 'smooth');
}
async function scrollBenefitTo(index: number) {
async function scrollBenefitTo(index: number, behavior: ScrollBehavior = 'smooth') {
if (!benefitScroller || !isMobileViewport()) return;
const cards = benefitScroller.querySelectorAll<HTMLElement>('.service-benefit-card');
@@ -71,10 +75,9 @@
activeBenefitIndex = index;
await tick();
targetCard.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'start'
benefitScroller.scrollTo({
left: benefitCardScrollLeft(targetCard),
behavior
});
}
@@ -117,7 +120,7 @@
benefitScroller.scrollTo({ left: 0, behavior: 'auto' });
}
bindMobileBenefitObserver();
void scrollBenefitTo(activeBenefitIndex);
void scrollBenefitTo(activeBenefitIndex, 'auto');
} else {
mobileBenefitObserver?.disconnect();
}
@@ -252,7 +255,7 @@
aria-label={`Go to benefit ${index + 1}`}
aria-pressed={index === activeBenefitIndex}
on:click={() => scrollBenefitTo(index)}
/>
></button>
{/each}
</div>
<button
+496 -110
View File
@@ -4,69 +4,524 @@
import type { IconCard } from '$lib/types';
export let services: IconCard[];
export let heading = 'What we do';
export let heading = 'Choose the walk style that suits your dog best.';
export let intro =
'Choose the walk style that fits your dog best, then book a free Meet & Greet when you are ready.';
'Dogs are social creatures. The Tiny Gang gives them their own little friendship group — older dogs guide the younger ones, playful dogs burn energy together, and everyone comes home happy, tired, and fulfilled. All the fun of doggy daycare, without the huge groups or price tag.';
const requestedServiceStorageKey = 'goodwalk_requested_service';
const sharedPromises = [
'Familiar walkers',
'Small-scale care',
'Reliable pickup & drop-off',
'Updates you will actually want'
];
function bookingHref() {
return '#newlead';
}
function primeBookingService(serviceTitle: string) {
try {
window.sessionStorage.setItem(requestedServiceStorageKey, serviceTitle);
} catch {
// Ignore storage failures and continue with the link target.
// Lightweight presentation metadata — the card only needs to say *what*
// each service is before the visitor opens the full service page.
const serviceMeta: Record<
string,
{
eyebrow: string;
featured?: boolean;
featuredLabel?: string;
imageUrl: string;
imageAlt: string;
lead: string;
cues: string[];
}
> = {
'Tiny Gang Pack Walks': {
eyebrow: 'Good Walk Signature',
featured: true,
featuredLabel: 'Most loved',
imageUrl: '/images/auckland-pack-walk-small-dogs-group.jpg',
imageAlt: 'Small dogs together on a Tiny Gang pack walk',
lead: 'The Tiny Gang is built for dogs who love company, big adventures, and coming home happily worn out!',
cues: ['4-8 dogs', 'Pickup & drop-off', 'Tiny Gang matching']
},
'1:1 Walks': {
eyebrow: 'Tailored support',
imageUrl: '/images/one-on-one-dog-portrait-1.jpg',
imageAlt: 'Dog enjoying a one-on-one walk',
lead: 'For nervous dogs, senior dogs, and little personalities who do better with extra attention.',
cues: ['Solo focus', 'Custom pace', 'Confidence building']
},
'Puppy Visits': {
eyebrow: 'Building Blocks For The Tiny Gang',
imageUrl: '/images/auckland-puppy-home-visit.jpg',
imageAlt: 'Puppy during a calm home visit',
lead: 'Early puppy visits designed to build confidence, routine, and good habits before Tiny Gang adventures begin!',
cues: ['Home visits', 'Routine support', 'Play & company']
}
};
window.dispatchEvent(
new CustomEvent('goodwalk:service-selected', {
detail: { service: serviceTitle }
})
);
}
$: orderedServices = services
.map((service, index) => ({ service, index }))
.sort((a, b) => {
const aFeatured = serviceMeta[a.service.title]?.featured ? 0 : 1;
const bFeatured = serviceMeta[b.service.title]?.featured ? 0 : 1;
if (aFeatured !== bFeatured) {
return aFeatured - bFeatured;
}
return a.index - b.index;
})
.map(({ service }) => service);
</script>
<section id="services" use:reveal={{ delay: 20 }} class="reveal-block">
<div class="services-inner">
<h2 class="section-heading">{heading}</h2>
<p class="services-intro">{intro}</p>
<div class="section-header">
<h2 class="section-heading">{heading}</h2>
<p class="section-intro services-intro">{intro}</p>
</div>
<div class="services-grid">
{#each services as service}
<div class="service-card">
<div class="service-icon-bubble">
<Icon name={service.icon} className="service-card-icon" />
{#each orderedServices as service}
{@const meta = serviceMeta[service.title]}
<a
href={service.href}
class:service-card-featured={meta?.featured}
class="service-card"
aria-label={`${service.title} — view service page`}
>
<div class="service-card-media">
{#if meta}
<img src={meta.imageUrl} alt={meta.imageAlt} loading="lazy" decoding="async" />
{/if}
{#if meta?.featuredLabel}
<span class="service-card-badge">{meta.featuredLabel}</span>
{/if}
</div>
<h3>{service.title}</h3>
<p>{service.body}</p>
{#if service.priceFrom}
<p class="service-card-price">{service.priceFrom}</p>
{/if}
<div class="service-card-body">
<span class="service-card-emblem">
<Icon name={service.icon} className="service-card-emblem-glyph" />
</span>
{#if service.href}
<div class="service-card-actions">
<a href={bookingHref()} class="btn btn-green" on:click={() => primeBookingService(service.title)}>
<span>Book {service.title}</span>
<Icon name="fas fa-arrow-right" />
</a>
{#if meta?.eyebrow}
<span class="service-card-eyebrow">{meta.eyebrow}</span>
{/if}
<h3>{service.title}</h3>
<p>{meta?.lead ?? service.body}</p>
<a href={service.href} class="service-card-link">
View details &amp; pricing
</a>
</div>
{/if}
</div>
{#if meta?.cues?.length}
<div class="service-card-cues">
{#each meta.cues as cue}
<span class="service-card-cue">{cue}</span>
{/each}
</div>
{/if}
<span class="service-card-cta">
View service page
<Icon name="fas fa-arrow-right" className="service-card-cta-arrow" />
</span>
</div>
</a>
{/each}
</div>
</div>
</section>
<style>
/* ── Section intro ── */
.services-intro {
max-width: 700px;
}
/* ── Overview band ── */
.services-overview {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(300px, 0.8fr);
gap: 18px;
align-items: stretch;
margin-top: 28px;
}
.services-overview-copy,
.services-overview-panel {
border-radius: 28px;
background: linear-gradient(180deg, #fff 0%, #fbf8f2 100%);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 12px 28px rgba(17, 20, 24, 0.05);
}
.services-overview-copy {
padding: 28px 30px;
}
.services-overview-kicker,
.services-overview-panel-label {
display: inline-flex;
align-items: center;
min-height: 30px;
width: fit-content;
padding: 0 11px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.08);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.services-overview-copy h3 {
margin: 14px 0 10px;
color: #0d1a0d;
font-size: clamp(26px, 2.6vw, 34px);
line-height: 1.02;
letter-spacing: -0.04em;
}
.services-overview-copy p,
.services-overview-panel {
color: #4c5056;
font-size: 15px;
line-height: 1.65;
}
.services-overview-copy p {
max-width: 44ch;
margin: 0;
}
.services-overview-panel {
display: grid;
align-content: center;
gap: 16px;
padding: 24px 24px 22px;
background:
radial-gradient(circle at top right, rgba(255, 209, 0, 0.22), transparent 36%),
linear-gradient(180deg, #fff 0%, #f8f3e7 100%);
}
.services-overview-pills {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.services-overview-pill {
display: inline-flex;
align-items: center;
min-height: 36px;
padding: 0 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
box-shadow: inset 0 0 0 1px rgba(33, 48, 33, 0.08);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 12px;
font-weight: 700;
line-height: 1.2;
}
/* ── Service cards ── */
.services-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 22px;
margin-top: 30px;
}
.service-card {
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
text-align: left;
border-radius: 24px;
background: #fff;
border: 1px solid rgba(17, 20, 24, 0.07);
box-shadow: 0 6px 20px rgba(17, 20, 24, 0.05);
text-decoration: none;
color: inherit;
transition:
box-shadow 0.28s ease,
transform 0.24s cubic-bezier(0.22, 1, 0.36, 1),
border-color 0.28s ease;
}
.service-card-featured {
border-color: rgba(242, 191, 47, 0.45);
box-shadow:
inset 0 0 0 1px rgba(255, 209, 0, 0.22),
0 10px 26px rgba(17, 20, 24, 0.07);
}
@media (hover: hover) {
.service-card:hover {
transform: translateY(-6px);
border-color: rgba(33, 48, 33, 0.16);
box-shadow: 0 22px 46px rgba(17, 20, 24, 0.13);
filter: none;
}
.service-card:hover .service-card-media img {
transform: scale(1.06);
}
.service-card:hover .service-card-cta-arrow {
transform: translateX(4px);
}
.service-card:hover .service-card-emblem::after,
.service-card:focus-visible .service-card-emblem::after,
.service-card:active .service-card-emblem::after {
animation: serviceEmblemShine 0.9s cubic-bezier(0.22, 1, 0.36, 1) 1;
}
}
.service-card:active {
transform: translateY(-3px);
}
.service-card-media {
position: relative;
aspect-ratio: 4 / 3;
overflow: hidden;
background: #ede4d2;
}
.service-card-media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.service-card-badge {
position: absolute;
top: 14px;
right: 14px;
padding: 6px 11px;
border-radius: 999px;
background: var(--gw-green);
color: #fff6cf;
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.service-card-body {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
padding: 42px 26px 26px;
}
/* Brand emblem straddles the photo / body seam */
.service-card-emblem {
position: absolute;
top: 0;
left: 24px;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 16px;
background: var(--gw-green);
box-shadow: 0 10px 22px rgba(33, 48, 33, 0.26);
overflow: hidden;
}
.service-card-emblem :global(.service-card-emblem-glyph) {
font-size: 22px;
color: var(--yellow);
}
.service-card-emblem::after {
content: '';
position: absolute;
top: -20%;
left: -85%;
width: 60%;
height: 140%;
background: linear-gradient(
120deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.18) 35%,
rgba(255, 255, 255, 0.65) 50%,
rgba(255, 255, 255, 0.18) 65%,
rgba(255, 255, 255, 0) 100%
);
transform: rotate(14deg);
pointer-events: none;
opacity: 0;
}
.service-card-eyebrow {
margin-bottom: 8px;
color: var(--gw-green);
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.service-card-body h3 {
margin: 0 0 8px;
font-family: var(--font-head);
font-size: 21px;
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.02em;
color: #0d1a0d;
}
.service-card-body p {
margin: 0;
color: #4c5056;
font-size: 15px;
line-height: 1.6;
}
.service-card-cues {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 16px;
}
.service-card-cue {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 4px 11px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
box-shadow: inset 0 0 0 1px rgba(33, 48, 33, 0.07);
color: var(--gw-green);
font-size: 11px;
font-weight: 700;
line-height: 1.2;
}
.service-card-cta {
display: flex;
align-items: center;
gap: 8px;
margin-top: auto;
padding-top: 20px;
border-top: 1px solid rgba(17, 20, 24, 0.08);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 13px;
font-weight: 700;
}
.service-card-cta-arrow {
font-size: 11px;
transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
/* ── Mobile ── */
@media (max-width: 768px) {
.services-intro {
max-width: 34ch;
}
.services-overview {
grid-template-columns: 1fr;
gap: 12px;
margin-top: 22px;
}
.services-overview-copy,
.services-overview-panel {
border-radius: 24px;
}
.services-overview-copy {
padding: 22px 18px;
}
.services-overview-copy h3 {
font-size: clamp(24px, 8vw, 31px);
line-height: 1.06;
}
.services-overview-copy p,
.services-overview-panel {
font-size: 14px;
line-height: 1.55;
}
.services-overview-panel {
padding: 18px;
gap: 12px;
}
.services-overview-pills {
gap: 8px;
}
.services-overview-pill {
min-height: 32px;
padding: 0 12px;
font-size: 11px;
}
.services-grid {
grid-template-columns: 1fr;
gap: 16px;
margin-top: 24px;
}
.service-card-body {
padding: 40px 22px 24px;
}
.service-card-emblem {
width: 48px;
height: 48px;
border-radius: 15px;
}
.service-card-emblem :global(.service-card-emblem-glyph) {
font-size: 20px;
}
.service-card-body h3 {
font-size: 20px;
}
}
@keyframes serviceEmblemShine {
0% {
left: -85%;
opacity: 0;
}
18% {
opacity: 1;
}
82% {
left: 130%;
opacity: 0;
}
100% {
left: 130%;
opacity: 0;
}
}
/* ── Reveal ── */
:global(.reveal-ready.reveal-block) {
opacity: 0;
transform: translate3d(0, var(--reveal-distance, 24px), 0);
@@ -80,73 +535,4 @@
opacity: 1;
transform: translate3d(0, 0, 0);
}
@media (hover: hover) and (min-width: 769px) {
:global(.reveal-visible.reveal-block) .service-card {
animation: service-card-settle 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
}
:global(.reveal-visible.reveal-block) .service-card:nth-child(1) {
animation-delay: 0.02s;
}
:global(.reveal-visible.reveal-block) .service-card:nth-child(2) {
animation-delay: 0.06s;
}
:global(.reveal-visible.reveal-block) .service-card:nth-child(3) {
animation-delay: 0.1s;
}
}
@keyframes service-card-settle {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.services-intro {
max-width: 700px;
margin: 18px auto 0;
text-align: center;
color: #4c5056;
font-size: 17px;
line-height: 1.65;
}
.service-card-actions {
display: grid;
gap: 12px;
margin-top: 18px;
}
.service-card-actions :global(.btn) {
width: 100%;
justify-content: center;
}
.service-card-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 28px;
color: var(--gw-green);
font-size: 14px;
font-weight: 700;
text-decoration: none;
}
@media (hover: hover) {
.service-card-link:hover {
text-decoration: underline;
text-underline-offset: 0.18em;
}
}
</style>
+720
View File
@@ -0,0 +1,720 @@
<script lang="ts">
import BookingSection from '$lib/components/BookingSection.svelte';
import Icon from '$lib/components/Icon.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import { getEnhancedImage } from '$lib/enhanced-images';
import type { SiteSharedContent, TestimonialContent } from '$lib/types';
export let content: SiteSharedContent;
$: testimonials = content.testimonials.filter((testimonial) => testimonial.imageUrl);
$: featuredTestimonial = testimonials[0];
$: supportingTestimonials = testimonials.slice(1);
$: testimonialCards = testimonials.map((testimonial, index) => ({
...testimonial,
enhanced: getEnhancedImage(testimonial.imageUrl),
accentClass: `testimonial-card-accent-${(index % 4) + 1}`
}));
const testimonialHighlights = [
{
quote: 'Amazing with my slightly hyper and anxious dog.',
reviewer: 'Kate',
detail: "Archie's mum"
},
{
quote: 'Great with communication if anything needs to change.',
reviewer: 'Kate',
detail: "Archie's mum"
},
{
quote: 'Basically doubled as a second mum to Monty.',
reviewer: 'Estelle',
detail: "Monty's mum"
},
{
quote: 'Provided feedback and training recommendations.',
reviewer: 'Estelle',
detail: "Monty's mum"
},
{
quote: 'Otis absolutely adores her.',
reviewer: 'Ross',
detail: "Otis's dad"
},
{
quote: 'He always comes back happy and tired.',
reviewer: 'Ross',
detail: "Otis's dad"
},
{
quote: 'She has cared for my pup since she was 10 weeks old.',
reviewer: 'Nina',
detail: "Wallace's mum"
},
{
quote: 'My dog has a great time every walk.',
reviewer: 'Nina',
detail: "Wallace's mum"
}
];
function reviewAlt(testimonial: TestimonialContent) {
const detailLead = testimonial.detail.match(/^([^']+)/)?.[1]?.trim();
return detailLead
? `${detailLead}, a Goodwalk dog in Auckland`
: `${testimonial.reviewer}'s dog with Goodwalk in Auckland`;
}
</script>
<main class="testimonials-page">
<PageHeader
variant="green"
eyebrow="Goodwalk reviews"
title="What our clients say"
subtitle="The same things come up again and again: calmer dogs, smoother routines, and owners who finally stop worrying."
>
<a
class="testimonials-page-trust"
href="https://g.page/r/CUsvrWPhkYrAEB0/"
target="_blank"
rel="noopener"
aria-label="Read our Google reviews"
>
<img
class="testimonials-page-trust-logo"
src="/images/google-g-logo.svg"
alt=""
width="18"
height="19"
/>
<span class="testimonials-page-trust-stars" aria-hidden="true">
{#each Array(5) as _}
<Icon name="fas fa-star" />
{/each}
</span>
<span>30+ five-star Google reviews</span>
</a>
</PageHeader>
<section class="testimonials-page-intro">
<div class="page-inner testimonials-page-intro-grid">
<div class="testimonials-page-copy">
<span class="eyebrow testimonials-page-kicker">Why owners stay</span>
<h2>The dogs are happy. The handoff feels easy. The routine starts working.</h2>
<p>
That is what people are really reviewing. Not just the walk itself, but what it feels
like to trust someone with your dog, your keys, and part of your week.
</p>
</div>
<aside class="testimonials-page-proof">
<span class="eyebrow testimonials-page-proof-label">What people keep saying</span>
<div class="testimonials-page-proof-pills">
<span class="testimonials-page-proof-pill">Reliable communication</span>
<span class="testimonials-page-proof-pill">Dogs genuinely adore Aless</span>
<span class="testimonials-page-proof-pill">Calmer afternoons at home</span>
<span class="testimonials-page-proof-pill">A routine that feels easy</span>
</div>
</aside>
</div>
</section>
{#if featuredTestimonial}
<section class="testimonials-page-featured-section">
<div class="page-inner testimonials-page-featured-grid">
<article class="testimonials-page-spotlight">
<div class="testimonials-page-spotlight-media">
{#if testimonialCards[0]?.enhanced}
<enhanced:img
src={testimonialCards[0].enhanced}
alt={reviewAlt(featuredTestimonial)}
loading="lazy"
decoding="async"
/>
{:else}
<img
src={featuredTestimonial.imageUrl}
alt={reviewAlt(featuredTestimonial)}
loading="lazy"
decoding="async"
/>
{/if}
</div>
<div class="testimonials-page-spotlight-copy">
<span class="eyebrow testimonials-page-card-kicker">Featured review</span>
<blockquote>{featuredTestimonial.quote}</blockquote>
<div class="testimonials-page-author">
<span class="testimonials-page-author-name">{featuredTestimonial.reviewer}</span>
<span class="testimonials-page-author-detail">{featuredTestimonial.detail}</span>
</div>
</div>
</article>
<div class="testimonials-page-stack">
{#each supportingTestimonials as testimonial, index}
<article class={`testimonials-page-mini-card testimonial-card-accent-${((index + 1) % 4) + 1}`}>
<div class="testimonials-page-mini-card-copy">
<span class="testimonials-page-mini-stars" aria-hidden="true">★★★★★</span>
<blockquote>{testimonial.quote}</blockquote>
<div class="testimonials-page-author">
<span class="testimonials-page-author-name">{testimonial.reviewer}</span>
<span class="testimonials-page-author-detail">{testimonial.detail}</span>
</div>
</div>
</article>
{/each}
</div>
</div>
</section>
{/if}
<section class="testimonials-page-highlights-section">
<div class="page-inner">
<div class="section-header testimonials-page-highlights-header">
<span class="eyebrow">Small moments owners mention</span>
<h2 class="section-heading">The same little details keep coming up.</h2>
<p class="section-intro">
These are the lines that show up again and again when people describe what Goodwalk
feels like.
</p>
</div>
<div class="testimonials-page-highlights-grid">
{#each testimonialHighlights as highlight}
<article class="testimonials-page-highlight-card">
<div class="testimonials-page-highlight-top">
<span class="testimonials-page-highlight-mark"></span>
<span class="testimonials-page-highlight-stars" aria-hidden="true">★★★★★</span>
</div>
<p>{highlight.quote}</p>
<div class="testimonials-page-highlight-author">
<span>{highlight.reviewer}</span>
<span>{highlight.detail}</span>
</div>
</article>
{/each}
</div>
</div>
</section>
<section class="testimonials-page-grid-section">
<div class="page-inner">
<div class="section-header testimonials-page-grid-header">
<span class="eyebrow">Full reviews</span>
<h2 class="section-heading">A closer look at what clients say after walking with us.</h2>
</div>
<div class="testimonials-page-grid">
{#each testimonialCards as testimonial}
<article class={`testimonials-page-card ${testimonial.accentClass}`}>
<div class="testimonials-page-card-media">
{#if testimonial.enhanced}
<enhanced:img
src={testimonial.enhanced}
alt={reviewAlt(testimonial)}
loading="lazy"
decoding="async"
/>
{:else}
<img
src={testimonial.imageUrl}
alt={reviewAlt(testimonial)}
loading="lazy"
decoding="async"
/>
{/if}
</div>
<div class="testimonials-page-card-copy">
<span class="testimonials-page-quote-mark">"</span>
<blockquote>{testimonial.quote}</blockquote>
<div class="testimonials-page-author">
<span class="testimonials-page-author-name">{testimonial.reviewer}</span>
<span class="testimonials-page-author-detail">{testimonial.detail}</span>
</div>
</div>
</article>
{/each}
</div>
<div class="testimonials-page-google-cta-wrap">
<a
class="testimonials-page-google-cta"
href="https://g.page/r/CUsvrWPhkYrAEB0/"
target="_blank"
rel="noopener"
>
<img
class="testimonials-page-trust-logo"
src="/images/google-g-logo.svg"
alt=""
width="18"
height="19"
/>
<span>Read all our Google reviews</span>
<Icon name="fas fa-arrow-right" />
</a>
</div>
</div>
</section>
<BookingSection booking={content.booking} />
</main>
<style>
.testimonials-page {
background: #fff;
}
.testimonials-page-trust {
display: inline-flex;
align-items: center;
gap: 12px;
margin-top: 22px;
padding: 10px 18px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.16);
color: #fff;
font-family: var(--font-body);
font-size: 14px;
font-weight: 700;
line-height: 1.2;
text-decoration: none;
}
.testimonials-page-trust-stars {
display: inline-flex;
gap: 2px;
color: var(--yellow);
font-size: 13px;
}
.testimonials-page-trust-logo {
flex: 0 0 auto;
}
.testimonials-page-intro,
.testimonials-page-featured-section,
.testimonials-page-highlights-section,
.testimonials-page-grid-section {
padding: 64px 0;
}
.testimonials-page-featured-section,
.testimonials-page-grid-section {
background: var(--off-white);
}
.testimonials-page-intro-grid {
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(320px, 0.95fr);
gap: 22px;
align-items: stretch;
}
.testimonials-page-copy,
.testimonials-page-proof,
.testimonials-page-spotlight,
.testimonials-page-mini-card,
.testimonials-page-highlight-card,
.testimonials-page-card {
border-radius: 30px;
background: #fff;
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
var(--shadow-panel-elevated);
}
.testimonials-page-copy {
padding: 34px 34px 32px;
}
.testimonials-page-kicker,
.testimonials-page-proof-label,
.testimonials-page-card-kicker {
display: inline-block;
}
.testimonials-page-copy h2 {
margin: 16px 0 12px;
color: #102010;
font-family: var(--font-head);
font-size: clamp(28px, 3vw, 42px);
line-height: 1.04;
letter-spacing: -0.04em;
}
.testimonials-page-copy p {
max-width: 44ch;
margin: 0;
color: #4c5056;
font-size: 17px;
line-height: 1.68;
}
.testimonials-page-proof {
display: grid;
align-content: center;
gap: 16px;
padding: 28px 26px;
background:
radial-gradient(circle at top right, rgba(255, 209, 0, 0.2), transparent 34%),
linear-gradient(180deg, #fffdf8 0%, #f5efe0 100%);
}
.testimonials-page-proof-pills {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.testimonials-page-proof-pill {
display: inline-flex;
align-items: center;
min-height: 36px;
padding: 0 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.84);
box-shadow: inset 0 0 0 1px rgba(33, 48, 33, 0.08);
color: var(--gw-green);
font-family: var(--font-body);
font-size: 12px;
font-weight: 700;
line-height: 1.2;
}
.testimonials-page-featured-grid {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(300px, 0.85fr);
gap: 24px;
align-items: stretch;
}
.testimonials-page-spotlight {
display: grid;
grid-template-columns: minmax(240px, 0.9fr) minmax(0, 1.1fr);
overflow: hidden;
}
.testimonials-page-spotlight-media {
min-height: 100%;
background: #ede4d2;
}
.testimonials-page-spotlight-media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.testimonials-page-spotlight-copy {
display: flex;
flex-direction: column;
justify-content: center;
gap: 18px;
padding: 34px 32px;
background: linear-gradient(180deg, rgba(255, 250, 236, 0.76), #fff);
}
.testimonials-page-spotlight-copy blockquote {
margin: 0;
color: #202226;
font-size: clamp(18px, 1.65vw, 23px);
line-height: 1.6;
}
.testimonials-page-stack {
display: grid;
gap: 16px;
}
.testimonials-page-mini-card {
overflow: hidden;
}
.testimonials-page-mini-card-copy {
display: grid;
gap: 12px;
padding: 24px 24px 22px;
}
.testimonials-page-mini-stars {
color: var(--yellow);
font-size: 13px;
letter-spacing: 0.1em;
}
.testimonials-page-mini-card blockquote {
margin: 0;
color: #2e3031;
font-size: 15px;
line-height: 1.68;
}
.testimonials-page-highlights-header,
.testimonials-page-grid-header {
margin-bottom: 28px;
}
.testimonials-page-highlights-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
.testimonials-page-highlight-card {
display: grid;
gap: 14px;
padding: 22px 20px 20px;
}
.testimonials-page-highlight-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.testimonials-page-highlight-mark {
color: rgba(242, 191, 47, 0.6);
font-family: Georgia, serif;
font-size: 42px;
line-height: 0.8;
}
.testimonials-page-highlight-stars {
color: var(--yellow);
font-size: 12px;
letter-spacing: 0.12em;
}
.testimonials-page-highlight-card p {
margin: 0;
color: #1f2421;
font-size: 15px;
font-weight: 600;
line-height: 1.55;
}
.testimonials-page-highlight-author {
display: grid;
gap: 2px;
color: #687076;
font-size: 13px;
line-height: 1.4;
}
.testimonials-page-highlight-author span:first-child {
color: #102010;
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
}
.testimonials-page-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
}
.testimonials-page-card {
overflow: hidden;
}
.testimonials-page-card-media {
aspect-ratio: 4 / 3;
background: #ede4d2;
}
.testimonials-page-card-media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.testimonials-page-card-copy {
padding: 28px 28px 30px;
}
.testimonials-page-quote-mark {
display: block;
margin-bottom: 12px;
color: var(--yellow-soft);
font-family: Georgia, serif;
font-size: 54px;
line-height: 0.6;
}
.testimonials-page-card blockquote {
margin: 0;
color: #2e3031;
font-size: 16px;
line-height: 1.68;
}
.testimonials-page-author {
display: flex;
align-items: center;
gap: 10px;
margin-top: 18px;
}
.testimonials-page-author-name {
color: #102010;
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
}
.testimonials-page-author-detail {
color: #6b7280;
font-size: 14px;
}
.testimonials-page-author-detail::before {
content: '—';
margin-right: 6px;
}
.testimonial-card-accent-1 .testimonials-page-card-copy,
.testimonial-card-accent-1 .testimonials-page-mini-card-copy {
background: linear-gradient(180deg, rgba(255, 250, 236, 0.72), #fff);
}
.testimonial-card-accent-2 .testimonials-page-card-copy,
.testimonial-card-accent-2 .testimonials-page-mini-card-copy {
background: linear-gradient(180deg, rgba(229, 214, 194, 0.28), #fff);
}
.testimonial-card-accent-3 .testimonials-page-card-copy,
.testimonial-card-accent-3 .testimonials-page-mini-card-copy {
background: linear-gradient(180deg, rgba(33, 48, 33, 0.04), #fff);
}
.testimonial-card-accent-4 .testimonials-page-card-copy,
.testimonial-card-accent-4 .testimonials-page-mini-card-copy {
background: linear-gradient(180deg, rgba(255, 209, 0, 0.12), #fff);
}
.testimonials-page-google-cta-wrap {
display: flex;
justify-content: center;
margin-top: 28px;
}
.testimonials-page-google-cta {
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 50px;
padding: 0 18px;
border-radius: 999px;
background: #fff;
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 10px 24px rgba(17, 20, 24, 0.06);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
text-decoration: none;
}
@media (max-width: 1024px) {
.testimonials-page-intro-grid,
.testimonials-page-featured-grid,
.testimonials-page-highlights-grid,
.testimonials-page-grid,
.testimonials-page-spotlight {
grid-template-columns: 1fr;
}
.testimonials-page-highlights-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.testimonials-page-spotlight-media {
aspect-ratio: 4 / 3;
}
}
@media (max-width: 768px) {
.testimonials-page-intro,
.testimonials-page-featured-section,
.testimonials-page-highlights-section,
.testimonials-page-grid-section {
padding: 52px 0;
}
.testimonials-page-trust {
gap: 10px;
padding: 10px 14px;
font-size: 13px;
}
.testimonials-page-copy {
padding: 24px 20px 22px;
}
.testimonials-page-proof {
padding: 22px 18px;
gap: 12px;
}
.testimonials-page-copy h2 {
font-size: clamp(26px, 8vw, 34px);
}
.testimonials-page-copy p {
font-size: 15px;
line-height: 1.58;
}
.testimonials-page-spotlight-copy,
.testimonials-page-mini-card-copy,
.testimonials-page-card-copy {
padding: 22px 20px 24px;
}
.testimonials-page-spotlight-copy blockquote,
.testimonials-page-card blockquote,
.testimonials-page-mini-card blockquote {
font-size: 15px;
line-height: 1.6;
}
.testimonials-page-highlights-grid,
.testimonials-page-grid {
gap: 18px;
}
.testimonials-page-highlights-grid {
grid-template-columns: 1fr;
}
.testimonials-page-copy,
.testimonials-page-proof,
.testimonials-page-spotlight,
.testimonials-page-mini-card,
.testimonials-page-highlight-card,
.testimonials-page-card {
border-radius: 24px;
}
.testimonials-page-author {
align-items: flex-start;
flex-direction: column;
gap: 4px;
}
.testimonials-page-author-detail::before {
content: '';
margin-right: 0;
}
}
</style>
+93 -43
View File
@@ -8,10 +8,10 @@
export let testimonials: TestimonialContent[];
export let eyebrow = '30+ five-star reviews';
export let heading = 'Proof your dog is in good hands';
export let blurb = 'Peace of mind for busy Auckland dog owners. Happier dogs, smoother routines, and a team owners trust with the important stuff.';
export let instagramHref = 'https://www.instagram.com/goodwalk.nz/';
export let instagramLabel = 'goodwalk.nz';
export let heading = 'What owners notice first';
export let blurb = 'Happier dogs. Calmer evenings. A routine that feels easier to trust and easier to keep.';
export let testimonialsHref = '/testimonials';
export let googleReviewsHref = 'https://g.page/r/CUsvrWPhkYrAEB0/';
export let seedKey = '';
type TestimonialSlide = TestimonialContent & { imageUrl: string };
@@ -265,12 +265,7 @@
</button>
</div>
<a
class="testimonial-google"
href="https://g.page/r/CUsvrWPhkYrAEB0/"
target="_blank"
rel="noopener"
>
<a class="testimonial-google" href={googleReviewsHref} target="_blank" rel="noopener">
<img
class="testimonial-google-logo"
src="/images/google-g-logo.svg"
@@ -296,10 +291,19 @@
</div>
{/if}
<a href={instagramHref} target="_blank" rel="noopener" class="testimonials-instagram-link">
<Icon name="fab fa-instagram" />
<span>{instagramLabel}</span>
</a>
<div class="testimonials-cta-row">
<a href={testimonialsHref} class="testimonials-cta testimonials-cta-primary">
<Icon name="fas fa-comment-dots" />
<span class="testimonials-cta-label-desktop">All testimonials</span>
<span class="testimonials-cta-label-mobile">Testimonials</span>
</a>
<a href={googleReviewsHref} target="_blank" rel="noopener" class="testimonials-cta testimonials-cta-secondary">
<img class="testimonials-cta-logo" src="/images/google-g-logo.svg" alt="" width="16" height="17" />
<span class="testimonials-cta-label-desktop">Google reviews</span>
<span class="testimonials-cta-label-mobile">Google</span>
</a>
</div>
</div>
</section>
@@ -328,45 +332,77 @@
.testimonials-intro p {
margin: 0;
color: #4c5056;
font-size: 17px;
font-size: var(--body-lead-size);
line-height: 1.65;
}
.testimonials-instagram-link {
display: flex;
width: fit-content;
.testimonials-cta-row {
display: grid;
grid-template-columns: repeat(2, max-content);
align-items: center;
gap: 10px;
margin: 18px auto 0;
padding: 10px 16px;
justify-content: center;
width: fit-content;
max-width: 100%;
gap: 12px;
margin: 22px auto 0;
}
.testimonials-cta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
min-height: 42px;
padding: 9px 16px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
color: var(--gw-green);
font-weight: 700;
flex-shrink: 0;
text-decoration: none;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
font-size: 14px;
font-weight: 700;
line-height: 1.2;
white-space: nowrap;
transition:
transform 0.16s cubic-bezier(0.22, 1, 0.36, 1),
background 0.2s ease,
box-shadow 0.2s ease;
}
:global(.testimonials-instagram-link .icon) {
font-size: 18px;
.testimonials-cta-primary {
background: var(--gw-green);
color: #fff;
box-shadow: 0 10px 24px rgba(33, 48, 33, 0.16);
}
.testimonials-cta-secondary {
background: rgba(33, 48, 33, 0.06);
color: var(--gw-green);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
}
.testimonials-cta-logo {
flex: 0 0 auto;
}
.testimonials-cta-label-mobile {
display: none;
}
@media (hover: hover) {
.testimonials-instagram-link:hover {
.testimonials-cta:hover {
transform: translateY(-2px);
}
.testimonials-cta-primary:hover {
box-shadow: 0 14px 30px rgba(33, 48, 33, 0.2);
}
.testimonials-cta-secondary:hover {
background: rgba(33, 48, 33, 0.09);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 10px 22px rgba(17, 20, 24, 0.08);
}
}
.testimonials-instagram-link:active {
transform: translateY(1px) scale(0.985);
}
.testimonials-carousel {
@@ -387,14 +423,28 @@
}
.testimonials-intro p {
font-size: 15px;
font-size: var(--body-lead-size-mobile);
line-height: 1.55;
}
.testimonials-instagram-link {
margin: 14px auto 0;
padding: 9px 14px;
font-size: 15px;
.testimonials-cta-row {
gap: 8px;
margin-top: 16px;
}
.testimonials-cta {
min-height: 38px;
padding: 8px 10px;
font-size: 11px;
gap: 7px;
}
.testimonials-cta-label-desktop {
display: none;
}
.testimonials-cta-label-mobile {
display: inline;
}
}
@@ -672,7 +722,7 @@
@media (max-width: 767px) {
.testimonials-carousel {
margin-top: 32px;
margin-top: 26px;
padding: 0;
}
@@ -708,7 +758,7 @@
.testimonial-photo-wrap {
justify-content: center;
padding: 48px 22px 16px;
padding: 34px 20px 12px;
}
.testimonial-photo-frame {
@@ -720,7 +770,7 @@
}
.testimonial-copy {
padding: 8px 28px 32px;
padding: 4px 24px 24px;
align-self: start;
}
@@ -744,7 +794,7 @@
justify-content: flex-end;
width: 100%;
gap: 12px;
margin-top: 20px;
margin-top: 16px;
}
.testimonial-arrow-inline {
@@ -765,7 +815,7 @@
}
.testimonial-google {
margin-top: 20px;
margin-top: 16px;
font-size: 16px;
gap: 10px;
padding: 10px 14px;
@@ -776,8 +826,8 @@
}
.testimonial-woof {
top: 24px;
right: 22px;
top: 16px;
right: 18px;
}
.testimonial-woof-text {
+584 -341
View File
@@ -1,12 +1,73 @@
<script lang="ts">
import { onMount, tick } from 'svelte';
import { reveal } from '$lib/actions/reveal';
import { getEnhancedImage } from '$lib/enhanced-images';
import Icon from '$lib/components/Icon.svelte';
import type { IconCard } from '$lib/types';
export let values: IconCard[];
let valuesScroller: HTMLDivElement | undefined;
let activeIndex = 0;
let mobileCardsObserver: IntersectionObserver | null = null;
const stakes = [
{
label: 'Without the right routine',
title: 'By the end of the day, everything feels harder than it should.',
body:
'Your dog still has energy to burn, you are carrying guilt through the workday, and home does not feel as calm as it could.',
points: [
'A dog who is restless, wired, or harder to settle at home',
'A workday shaped by guilt, logistics, and wondering how they are doing',
'Too much uncertainty around who is picking up your dog and what the walk will be like'
],
footer: 'The walk is not really the point. The evening is.'
},
{
label: 'With Goodwalk',
title: 'A good walk changes you & your dogs whole evening.',
body:
'Your dog comes home happier, the routine feels lighter, and you are not spending the day second-guessing whether they are okay.',
points: [
'A walker your dog recognises and is happy to see at the door',
'Small-group or one-on-one care that genuinely suits your dog',
'Clear updates, calmer evenings, and one less thing sitting on your mind'
],
footer: 'That is what people are really buying: peace of mind, routine, and a dog who feels cared for.'
}
];
const clientPhotos = [
{
imageUrl: '/images/dog.png',
alt: 'Two happy Goodwalk client dogs out on a walk in Auckland',
name: 'Happy clients',
detail: 'out with Goodwalk'
},
{
imageUrl: '/images/pack-walk-tiny-gang.png',
alt: 'Goodwalk Tiny Gang dogs together on a walk in Auckland',
name: 'Tiny Gang',
detail: 'small group pack walks'
},
{
imageUrl: '/images/tiny-gang-mt-albert-park.png',
alt: 'Goodwalk dogs together at Mt Albert Park in Auckland',
name: 'Mt Albert Park',
detail: 'Goodwalk regulars'
},
{
imageUrl: '/images/otis-auckland-dog-walking-review.jpg',
alt: 'Otis enjoying his Goodwalk routine in Auckland',
name: 'Otis',
detail: 'regular weekly walks'
},
{
imageUrl: '/images/wallace-auckland-dog-walking-review.jpg',
alt: 'Wallace during a Goodwalk puppy-to-pack journey',
name: 'Wallace',
detail: 'from puppy visits to pack walks'
}
];
$: clientPhotoCards = clientPhotos.map((photo) => ({
...photo,
enhanced: getEnhancedImage(photo.imageUrl)
}));
$: orderedValues = values
.map((value, index) => ({ value, index }))
@@ -21,380 +82,562 @@
return a.index - b.index;
})
.map(({ value }) => value);
function isMobileViewport() {
return typeof window !== 'undefined' && window.innerWidth <= 768;
}
function cardScrollLeft(card: HTMLElement) {
return Math.max(0, card.offsetLeft - 8);
}
async function scrollValues(direction: 1 | -1) {
if (!valuesScroller || !isMobileViewport()) {
return;
}
const nextIndex = Math.max(0, Math.min(activeIndex + direction, orderedValues.length - 1));
await scrollToValue(nextIndex, 'smooth');
}
async function scrollToValue(index: number, behavior: ScrollBehavior = 'smooth') {
if (!valuesScroller || !isMobileViewport()) {
return;
}
const cards = valuesScroller.querySelectorAll<HTMLElement>('.value-card');
const targetCard = cards[index];
if (!targetCard) {
return;
}
activeIndex = index;
await tick();
valuesScroller.scrollTo({
left: cardScrollLeft(targetCard),
behavior
});
}
function bindMobileCardObserver() {
mobileCardsObserver?.disconnect();
if (!valuesScroller || !isMobileViewport()) {
return;
}
const cards = valuesScroller.querySelectorAll<HTMLElement>('.value-card');
if (!cards.length) {
return;
}
mobileCardsObserver = new IntersectionObserver(
(entries) => {
const visibleEntry = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (!visibleEntry) {
return;
}
const nextIndex = cards.length ? [...cards].indexOf(visibleEntry.target as HTMLElement) : -1;
if (nextIndex >= 0) {
activeIndex = nextIndex;
}
},
{
root: valuesScroller,
threshold: [0.6, 0.75, 0.9]
}
);
cards.forEach((card) => mobileCardsObserver?.observe(card));
}
onMount(() => {
const handleResize = () => {
if (!valuesScroller) {
return;
}
if (isMobileViewport()) {
if (activeIndex === 0) {
valuesScroller.scrollTo({ left: 0, behavior: 'auto' });
}
bindMobileCardObserver();
void scrollToValue(activeIndex, 'auto');
} else {
mobileCardsObserver?.disconnect();
}
};
if (valuesScroller && isMobileViewport()) {
valuesScroller.scrollTo({ left: 0, behavior: 'auto' });
bindMobileCardObserver();
}
window.addEventListener('resize', handleResize);
return () => {
mobileCardsObserver?.disconnect();
window.removeEventListener('resize', handleResize);
};
});
</script>
<section id="values">
<section id="values" use:reveal={{ delay: 30 }} class="reveal-block">
<div class="values-inner">
<span class="values-eyebrow">Why owners stay</span>
<h2 class="section-heading">Calmer dogs. Clearer routines. Less worry.</h2>
<p class="values-intro">
Everything is designed to make life easier for busy Auckland dog owners and safer, happier for the dogs in our care.
</p>
<div class="section-header">
<span class="eyebrow values-eyebrow">Why people come to us</span>
<h2 class="section-heading">Calmer dogs. Better routines. The Tiny Gang effect.</h2>
<p class="section-intro values-intro">
Goodwalk was created for busy owners who want reliable, relationship-led care their dog genuinely looks forward to.
</p>
</div>
<div class="values-shell">
<div bind:this={valuesScroller} class="values-grid">
{#each orderedValues as value, index}
<div class:active={index === activeIndex} class="value-card">
<div class="value-icon-wrap">
<Icon name={value.icon} className="value-card-icon" />
</div>
<div class="value-text">
<h3>{value.title}</h3>
<p>{value.body}</p>
</div>
</div>
{/each}
</div>
<div class="values-mobile-controls" aria-label="Value cards navigation">
<button
type="button"
class="values-mobile-button"
aria-label="Previous value"
disabled={activeIndex === 0}
on:click={() => scrollValues(-1)}
>
<Icon name="fas fa-chevron-left" />
</button>
<div class="values-mobile-pager" aria-label="Current value">
{#each orderedValues as _, index}
<button
type="button"
class:active={index === activeIndex}
class="values-mobile-dot"
aria-label={`Go to value ${index + 1}`}
aria-pressed={index === activeIndex}
on:click={() => scrollToValue(index)}
<div class="values-photo-grid" aria-label="Goodwalk client dogs">
{#each clientPhotoCards as photo, index}
<figure class:values-photo-card-featured={index === 0} class="values-photo-card">
{#if photo.enhanced}
<enhanced:img
class="values-photo-image"
src={photo.enhanced}
alt={photo.alt}
loading="lazy"
decoding="async"
/>
{/each}
{:else}
<img
class="values-photo-image"
src={photo.imageUrl}
alt={photo.alt}
loading="lazy"
decoding="async"
/>
{/if}
<figcaption class="values-photo-caption">
<span class="values-photo-name">{photo.name}</span>
<span class="values-photo-detail">{photo.detail}</span>
</figcaption>
</figure>
{/each}
</div>
<div class="values-bento values-contrast">
{#each stakes as stake, index}
<article class:values-contrast-cell-good={index === 1} class="values-contrast-cell">
<div class="values-contrast-head">
<span class:values-contrast-label-good={index === 1} class="values-contrast-label">
{stake.label}
</span>
<span class="values-contrast-num">0{index + 1}</span>
</div>
<h3>{stake.title}</h3>
<p class="values-contrast-body">{stake.body}</p>
<ul class="values-contrast-list">
{#each stake.points as point}
<li>
<span class="values-contrast-bullet">
<Icon
name={index === 1 ? 'fas fa-check' : 'fas fa-minus'}
className="values-contrast-glyph"
/>
</span>
<span>{point}</span>
</li>
{/each}
</ul>
<p class="values-contrast-footer">{stake.footer}</p>
</article>
{/each}
</div>
<div class="values-points-header">
<span class="eyebrow values-eyebrow">What we stand for</span>
<h3 class="values-points-title">The values behind every walk</h3>
<p class="values-points-intro">
Kind handling, small groups, proper safety training, and honest communication — not extras, just how Goodwalk works.
</p>
</div>
<div class="values-bento values-points">
{#each orderedValues as value}
<div class="values-point">
<div class="values-point-icon">
<Icon name={value.icon} className="values-point-glyph" />
</div>
<h3>{value.title}</h3>
<p>{value.body}</p>
</div>
<button
type="button"
class="values-mobile-button"
aria-label="Next value"
disabled={activeIndex === orderedValues.length - 1}
on:click={() => scrollValues(1)}
>
<Icon name="fas fa-chevron-right" />
</button>
</div>
{/each}
</div>
</div>
</section>
<style>
/* Minimalist, grid-based layout (hairline "bento" cells) with Goodwalk
brand colour carried through the icons and the "With Goodwalk" cell. */
#values {
position: relative;
color: var(--text);
}
.values-inner {
max-width: var(--max-w);
margin: 0 auto;
padding: 0 50px;
}
.values-inner .section-heading {
color: #000;
text-align: center;
}
.values-eyebrow {
display: block;
width: fit-content;
margin: 0 auto 10px;
padding: 7px 12px;
padding: 6px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: var(--yellow);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
background: rgba(33, 48, 33, 0.06);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.07);
}
.values-intro {
max-width: 760px;
margin: 18px auto 0;
text-align: center;
color: rgba(255, 255, 255, 0.82);
font-size: 17px;
max-width: 720px;
}
/* ── Client photo gallery ── */
.values-photo-grid {
display: grid;
grid-template-columns: 1.25fr 0.9fr 0.9fr;
grid-template-rows: repeat(2, clamp(170px, 18vw, 240px));
gap: 16px;
margin-top: 32px;
max-width: 1120px;
margin-left: auto;
margin-right: auto;
}
.values-photo-card {
position: relative;
overflow: hidden;
min-height: 0;
border-radius: 28px;
background: #ede4d2;
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 18px 34px rgba(17, 20, 24, 0.08);
}
.values-photo-card-featured {
grid-row: 1 / span 2;
}
.values-photo-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) {
.values-photo-card:hover .values-photo-image {
transform: scale(1.06);
}
}
.values-photo-caption {
position: absolute;
left: 16px;
right: 16px;
bottom: 16px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 10px;
padding: 12px 14px;
border-radius: 18px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.92));
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 12px 24px rgba(17, 20, 24, 0.08);
}
.values-photo-name,
.values-photo-detail {
display: block;
}
.values-photo-name {
color: #102010;
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
}
.values-photo-detail {
color: #5a605f;
font-size: 13px;
line-height: 1.3;
text-align: right;
}
/* ── Bento container: hairline grid via 1px gaps over a line-coloured base ── */
.values-bento {
max-width: 1120px;
margin-left: auto;
margin-right: auto;
display: grid;
gap: 1px;
background: rgba(17, 20, 24, 0.1);
border: 1px solid rgba(17, 20, 24, 0.1);
border-radius: 18px;
overflow: hidden;
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.06);
}
/* ── Before / after contrast ── */
.values-contrast {
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 36px;
}
.values-contrast-cell {
display: flex;
flex-direction: column;
padding: 38px 36px;
background: #fff;
}
.values-contrast-cell-good {
background: var(--gw-green);
}
.values-contrast-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 20px;
}
.values-contrast-label {
display: inline-flex;
align-items: center;
padding: 5px 11px;
border-radius: 999px;
background: rgba(17, 20, 24, 0.05);
color: var(--gray);
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.values-contrast-label-good {
background: var(--yellow);
color: #000;
}
.values-contrast-num {
font-family: var(--font-head);
font-size: 12px;
font-weight: 700;
color: rgba(17, 20, 24, 0.22);
letter-spacing: 0.04em;
}
.values-contrast-cell h3 {
margin: 0 0 12px;
font-family: var(--font-head);
font-size: clamp(20px, 1.9vw, 25px);
font-weight: 700;
line-height: 1.22;
letter-spacing: -0.02em;
color: #0d1a0d;
}
.values-contrast-body {
margin: 0 0 20px;
color: #4c5056;
font-size: 15px;
line-height: 1.65;
}
.values-mobile-controls {
display: none;
.values-contrast-list {
display: grid;
margin: 0 0 22px;
padding: 0;
list-style: none;
}
.values-contrast-list li {
display: grid;
grid-template-columns: 20px minmax(0, 1fr);
gap: 12px;
align-items: start;
padding: 13px 0;
color: #3f4348;
font-size: 15px;
line-height: 1.5;
}
.values-contrast-list li + li {
border-top: 1px solid rgba(17, 20, 24, 0.08);
}
.values-contrast-bullet {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-top: 1px;
}
.values-contrast-list :global(.values-contrast-glyph) {
font-size: 10px;
color: var(--gray);
}
.values-contrast-cell-good .values-contrast-bullet {
border-radius: 50%;
background: var(--yellow);
}
.values-contrast-cell-good .values-contrast-list :global(.values-contrast-glyph) {
font-size: 9px;
color: var(--gw-green);
}
.values-contrast-footer {
margin: auto 0 0;
padding-top: 18px;
border-top: 1px solid rgba(17, 20, 24, 0.08);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 13px;
font-weight: 700;
line-height: 1.5;
}
.values-contrast-cell-good .values-contrast-footer {
border-top-color: rgba(255, 255, 255, 0.18);
color: var(--yellow);
}
/* Light text for the gw-green "With Goodwalk" cell */
.values-contrast-cell-good h3 {
color: #fff;
}
.values-contrast-cell-good .values-contrast-num {
color: rgba(255, 255, 255, 0.4);
}
.values-contrast-cell-good .values-contrast-body {
color: rgba(255, 255, 255, 0.82);
}
.values-contrast-cell-good .values-contrast-list li {
color: rgba(255, 255, 255, 0.9);
}
.values-contrast-cell-good .values-contrast-list li + li {
border-top-color: rgba(255, 255, 255, 0.14);
}
/* ── Values points header ── */
.values-points-header {
margin-top: 52px;
text-align: center;
}
.values-points-title {
max-width: 19ch;
margin: 12px auto 0;
font-family: var(--font-head);
font-size: clamp(24px, 2.4vw, 32px);
font-weight: 700;
line-height: 1.14;
letter-spacing: -0.03em;
color: #000;
}
.values-points-intro {
max-width: 560px;
margin: 14px auto 0;
color: #4c5056;
font-size: var(--body-copy-size);
line-height: 1.65;
}
/* ── Values points ── */
.values-points {
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 26px;
box-shadow: var(--shadow-panel-elevated);
}
.values-point {
display: flex;
flex-direction: column;
padding: 32px 30px;
background: #fff;
transition: background 0.18s ease;
}
@media (hover: hover) {
.values-point:hover {
background: #fcfbf6;
}
}
.values-point-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin-bottom: 18px;
border-radius: 11px;
background: var(--gw-green);
box-shadow: 0 6px 16px rgba(33, 48, 33, 0.18);
}
.values-point-icon :global(.values-point-glyph) {
font-size: 17px;
color: var(--yellow);
}
.values-point h3 {
margin: 0 0 9px;
font-family: var(--font-head);
font-size: 17px;
font-weight: 700;
line-height: 1.25;
color: #0d1a0d;
}
.values-point p {
margin: 0;
color: #4c5056;
font-size: 14px;
line-height: 1.6;
}
@media (min-width: 1600px) {
.values-photo-grid,
.values-bento {
max-width: 1180px;
}
}
/* ── Mobile ── */
@media (max-width: 768px) {
.values-shell {
margin-top: 24px;
overflow: hidden;
.values-inner {
padding: 0 var(--space-container-x-mobile);
}
.values-grid {
display: grid;
grid-auto-flow: column;
grid-auto-columns: calc(100% - 64px);
grid-template-columns: none;
align-items: stretch;
gap: 10px;
margin-top: 0;
border-top: none;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scroll-padding-left: 8px;
padding: 0 14px 8px 8px;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
touch-action: pan-x pinch-zoom;
}
.values-grid::-webkit-scrollbar {
display: none;
}
.values-mobile-controls {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-top: 12px;
}
.values-mobile-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 46px;
height: 46px;
border: none;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
color: #fff;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
transition:
background 0.18s ease,
opacity 0.18s ease,
transform 0.18s ease;
}
.values-mobile-button:disabled {
opacity: 0.35;
}
.values-mobile-pager {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-width: 0;
flex: 1;
}
.values-mobile-dot {
width: 9px;
height: 9px;
padding: 0;
border: none;
border-radius: 999px;
background: rgba(255, 255, 255, 0.28);
transition:
width 0.22s ease,
background 0.22s ease,
transform 0.22s ease;
}
.values-mobile-dot.active {
width: 28px;
background: var(--yellow);
}
.value-card {
display: flex;
min-height: clamp(230px, 42svh, 320px);
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 20px 18px 22px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 24px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.07));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 6px 16px rgba(0, 0, 0, 0.06);
scroll-snap-align: start;
scroll-snap-stop: always;
flex-direction: column;
justify-content: flex-start;
gap: 14px;
transition:
background 0.24s ease,
box-shadow 0.24s ease,
border-color 0.24s ease;
touch-action: pan-x;
user-select: none;
-webkit-user-select: none;
}
.value-card.active {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.09));
border-color: rgba(255, 255, 255, 0.16);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 10px 22px rgba(0, 0, 0, 0.08);
}
.value-card:nth-child(odd) {
border-right: none;
}
.value-card:last-child {
margin-right: 2px;
}
.value-icon-wrap {
width: 56px;
height: 56px;
border-radius: 18px;
margin-top: 0;
background: rgba(255, 255, 255, 0.1);
}
.value-card .value-card-icon {
font-size: 23px;
}
.value-text {
max-width: 30ch;
min-width: 0;
margin-top: 0;
}
.value-text h3 {
margin-bottom: 8px;
font-size: 21px;
line-height: 1.08;
}
.value-card p {
font-size: 14px;
line-height: 1.55;
opacity: 0.9;
.values-intro {
max-width: 32ch;
}
.values-eyebrow {
margin-bottom: 8px;
padding: 6px 10px;
font-size: 11px;
}
.values-intro {
margin-top: 14px;
font-size: 15px;
.values-photo-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: auto;
gap: 10px;
margin-top: 22px;
}
.values-photo-card,
.values-photo-card-featured {
grid-row: auto;
min-height: 178px;
border-radius: 22px;
}
.values-photo-caption {
left: 10px;
right: 10px;
bottom: 10px;
padding: 10px 11px;
border-radius: 16px;
}
.values-photo-name {
font-size: 12px;
}
.values-photo-detail {
font-size: 11px;
}
.values-bento {
border-radius: 16px;
}
.values-contrast {
grid-template-columns: 1fr;
margin-top: 26px;
}
.values-contrast-cell {
padding: 26px 22px;
}
.values-contrast-body {
font-size: var(--body-copy-size-mobile);
line-height: 1.6;
}
.values-contrast-list li {
font-size: var(--body-copy-size-mobile);
line-height: 1.45;
}
.values-points-header {
margin-top: 30px;
}
.values-points-title {
max-width: 16ch;
font-size: clamp(22px, 6.4vw, 27px);
}
.values-points-intro {
max-width: 36ch;
font-size: var(--body-lead-size-mobile);
line-height: 1.55;
}
}
@media (hover: hover) {
.values-mobile-button:hover {
background: rgba(255, 255, 255, 0.18);
.values-points {
grid-template-columns: 1fr;
margin-top: 20px;
}
.values-point {
padding: 26px 22px;
}
}
.values-mobile-button:active {
transform: scale(0.95);
/* ── Reveal ── */
:global(.reveal-ready.reveal-block) {
opacity: 0;
transform: translate3d(0, var(--reveal-distance, 24px), 0);
transition:
opacity 0.55s ease,
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
transition-delay: var(--reveal-delay, 0ms);
}
:global(.reveal-visible.reveal-block) {
opacity: 1;
transform: translate3d(0, 0, 0);
}
</style>
+23
View File
@@ -25,6 +25,23 @@ export const dogWalkingContent: ServicePageContent = {
title: 'Built for dogs who need a more individual kind of walk',
imageUrl: '/images/auckland-dogs-outdoor-pack.jpg',
imageAlt: 'Goodwalk dogs gathered together outdoors',
points: [
{
title: 'A quieter setup from the start',
body:
'1:1 walks suit dogs who feel better without the pressure, pace, or unpredictability of a group environment.'
},
{
title: 'Handled around your dog, not a pack routine',
body:
'We can adjust the route, timing, and tempo around your dogs confidence, energy, and what helps them stay settled.'
},
{
title: 'A more considered fit for larger or more sensitive dogs',
body:
'For dogs who need more space, more clarity, or more personal attention, 1:1 walks give us room to do things properly.'
}
],
collageImages: [
{
imageUrl: '/images/one-on-one-dog-portrait-1.jpg',
@@ -65,10 +82,16 @@ export const dogWalkingContent: ServicePageContent = {
features: ['Free pickup and drop-off', 'Longer individual walk', 'More time for movement and engagement', 'Best for dogs needing a fuller outing']
}
],
extras: [
{ label: 'Extra Dog', note: 'Same household', price: '$20' },
{ label: 'Muddy Wash', price: '$35' }
],
scarcityNote: 'A limited number of 1:1 slots are available each week.'
},
benefits: {
title: 'Why some dogs do better on 1:1 walks',
intro:
'For dogs who need more space, steadier handling, or a more personalised pace, one-on-one walks can make the whole week feel easier.',
items: [
{
title: 'They get the walkers full attention',
+19 -17
View File
@@ -10,13 +10,15 @@ export const homepageContent: HomePageContent = {
desktopLinks: [
{ label: 'Our Services', href: '#services' },
{ label: 'Our Pricing', href: '/our-pricing' },
{ label: 'Testimonials', href: '/testimonials' },
{ label: 'About Us', href: '/about' }
],
mobileLinks: [
{ label: 'Home', href: '/' },
{ label: 'Pack Walks', href: '/pack-walks' },
{ label: 'Tiny Gang Pack Walks', href: '/pack-walks' },
{ label: '1:1 Walks', href: '/dog-walking' },
{ label: 'Puppy Visits', href: '/puppy-visits' },
{ label: 'Testimonials', href: '/testimonials' },
{ label: 'Our Pricing', href: '/our-pricing' },
{ label: 'About Us', href: '/about' },
{ label: 'Contact Us', href: '/contact-us' }
@@ -24,7 +26,7 @@ export const homepageContent: HomePageContent = {
cta: { label: 'Contact Us', href: '/contact-us', variant: 'yellow' },
instagram: { href: 'https://www.instagram.com/goodwalk.nz/', external: true },
megaMenuServices: [
{ icon: 'fas fa-paw', label: 'Pack Walks', description: 'Tiny Gang outdoor adventures', href: '/pack-walks' },
{ icon: 'fas fa-paw', label: 'Tiny Gang Pack Walks', description: 'Tiny Gang outdoor adventures', href: '/pack-walks' },
{ icon: 'fas fa-person-walking', label: '1:1 Walks', description: 'Personalised solo walks', href: '/dog-walking' },
{ icon: 'fas fa-dog', label: 'Puppy Visits', description: 'Home visits for young pups', href: '/puppy-visits' }
],
@@ -35,7 +37,7 @@ export const homepageContent: HomePageContent = {
highlight: 'calm, happy dog',
mobileTitle: 'Come home to a\ncalm, happy dog',
subtitle:
'Dog walking for busy Auckland Central professionals who want a reliable, relationship-led team their dog knows by name.',
'Reliable dog walking for busy Auckland owners who want happier dogs, calmer evenings, and a team they can trust.',
primaryCta: { label: 'Book a Meet & Greet', href: '#newlead', variant: 'yellow' },
secondaryCta: {
label: 'See how it works',
@@ -46,7 +48,7 @@ export const homepageContent: HomePageContent = {
imageAlt: 'Happy dog ready for a professional pack walk with Goodwalk Auckland dog walking service'
},
intro: {
text: 'Trusted by Auckland dog parents.',
text: 'Professional dog walking services across Auckland.',
reviewCta: {
label: '30+ five-star Google reviews',
href: 'https://g.page/r/CUsvrWPhkYrAEB0/',
@@ -69,22 +71,22 @@ export const homepageContent: HomePageContent = {
services: [
{
icon: 'fas fa-dog',
title: 'Pack Walks',
body: 'Small group Tiny Gang walks of 4-8 dogs - calm, social, and full of fun for your pup.',
title: 'Tiny Gang Pack Walks',
body: 'Small-group walks for dogs who love company, routine, and a familiar little crew.',
priceFrom: 'From $49.50 / walk',
href: '/pack-walks'
},
{
icon: 'fas fa-person-walking',
title: '1:1 Walks',
body: "One-on-one walks tailored to your dog's individual pace, personality, and needs.",
body: "One-on-one walks for dogs who need a quieter pace, focused attention, or a more tailored routine.",
priceFrom: 'From $45 / walk',
href: '/dog-walking'
},
{
icon: 'fas fa-house',
title: 'Puppy Visits',
body: 'In-home visits to check in on your puppy, play, and keep them company.',
body: 'In-home visits that help puppies with company, routine, and a calmer day while you are out.',
priceFrom: 'From $39 / visit',
href: '/puppy-visits'
}
@@ -92,30 +94,30 @@ export const homepageContent: HomePageContent = {
howItWorks: {
title: 'How it works',
intro:
'A calm, simple start designed to give you confidence quickly and help your dog settle into the right routine.',
'We keep the start simple and calm. First we meet your dog properly. Then we ease them into a routine that feels right.',
steps: [
{
phase: 'Meet',
benefit: 'No pressure, just clarity',
title: 'We get to know your dog properly',
title: 'We start with a proper Meet & Greet',
body:
'Your free Meet & Greet gives you a chance to ask questions, talk through routine and temperament, and make sure the fit feels right before anything starts.',
'You get to show us your dog, talk through routine and temperament, and make sure the fit feels right before anything starts.',
icon: 'fas fa-handshake'
},
{
phase: 'Settle',
benefit: 'A smoother start for nervous dogs',
title: 'Your dog eases in without overwhelm',
title: 'Your dog settles in without being rushed',
body:
'We start with assessment walks so your dog can build confidence, settle with the walker, and find the right pace before moving into a regular routine.',
'We use assessment walks to build confidence, get the pacing right, and make sure your dog feels comfortable before moving into a regular spot.',
icon: 'fas fa-clipboard-check'
},
{
phase: 'Thrive',
benefit: 'The outcome you actually want',
title: 'You get a calmer, happier dog at home',
title: 'Then the routine starts doing its job',
body:
'Once everything feels right, your dog joins their regular walks and comes home exercised, settled, and ready to relax while you get more peace of mind in your week.',
'Once your dog is settled, the week gets easier. They come home happier, evenings feel calmer, and you stop carrying the same guilt through the workday.',
icon: 'fas fa-heart'
}
]
@@ -198,7 +200,7 @@ export const homepageContent: HomePageContent = {
generalSubtitle:
"A few contact details and well reply properly within 24 hours.",
formAction: '/contact-us',
serviceOptions: ['Pack Walks', '1:1 Walks', 'Puppy Visits', 'Other Services'],
serviceOptions: ['Tiny Gang Pack Walks', '1:1 Walks', 'Puppy Visits', 'Other Services'],
ownerStepLabel: 'Your details',
dogStepLabel: 'Your dog',
generalIntro:
@@ -262,7 +264,7 @@ export const homepageContent: HomePageContent = {
brandText: 'Professional Dog Walking Services\nAuckland Central',
navigationLinks: [
{ label: 'Home', href: '/' },
{ label: 'Pack Walks', href: '/pack-walks' },
{ label: 'Tiny Gang Pack Walks', href: '/pack-walks' },
{ label: '1:1 Walks', href: '/dog-walking' },
{ label: 'Puppy Visits', href: '/puppy-visits' },
{ label: 'Our Pricing', href: '/our-pricing' },
+3 -3
View File
@@ -4,7 +4,7 @@ import { dogWalkingContent } from './dog-walking';
import { packWalksContent } from './pack-walks';
import { puppyVisitsContent } from './puppy-visits';
const packWalksService = sharedServices.find((service) => service.title === 'Pack Walks');
const packWalksService = sharedServices.find((service) => service.title === 'Tiny Gang Pack Walks');
const oneToOneService = sharedServices.find((service) => service.title === '1:1 Walks');
const puppyVisitsService = sharedServices.find((service) => service.title === 'Puppy Visits');
@@ -13,12 +13,12 @@ export const ourPricingContent: PricingPageContent = {
subtitle: 'Choose the Goodwalk routine that fits your dog, your week, and the kind of support you need.',
sections: [
{
title: 'Pack Walks',
title: 'Tiny Gang Pack Walks',
icon: packWalksService?.icon ?? 'fas fa-paw',
blurb:
'Our specialty for sociable small and medium dogs who thrive with calm structure, regular weekly outings, and the right dog company.',
detailCta: {
label: 'View Pack Walks',
label: 'View Tiny Gang Pack Walks',
href: '/pack-walks',
variant: 'green'
},
+3 -3
View File
@@ -2,11 +2,11 @@ import type { ServicePageContent } from '$lib/types';
export const packWalksContent: ServicePageContent = {
hero: {
eyebrow: 'Pack Walks',
title: 'Small-group pack walks for sociable small and medium dogs',
eyebrow: 'Tiny Gang Pack Walks',
title: 'Tiny Gang Pack Walks for sociable small and medium dogs',
subtitle: 'Tiny Gang walks are social, active, and carefully matched for dogs who love the right company.',
paragraphs: [
'Goodwalk Pack Walks are built for Auckland Central owners of small and medium dogs who want a reliable weekly routine, a well-exercised dog, and more peace of mind during the workday.',
'Goodwalk Tiny Gang Pack Walks are built for Auckland Central owners of small and medium dogs who want a reliable weekly routine, a well-exercised dog, and more peace of mind during the workday.',
'Our Tiny Gang packs stay small, calm, and carefully matched, so sociable dogs can build confidence, enjoy safe group outings, and come home settled instead of overstimulated.',
'Tiny Gang is best suited to sociable small and medium dogs who enjoy being around other dogs. If your dog would be better with a quieter, more individual setup, our 1:1 walks may be a better fit.',
'We run pack walks across Auckland Central — including Mt Eden, Kingsland, Ponsonby, Grey Lynn, Sandringham, Mt Albert, and surrounding suburbs — with free pickup and drop-off included in every booking.'
+27 -4
View File
@@ -7,7 +7,7 @@ export const puppyVisitsContent: ServicePageContent = {
subtitle: 'Toilet breaks, play, feeding, and calm one-on-one attention — at home, while you\'re out.',
paragraphs: [
'Goodwalk Puppy Visits are designed for busy owners who want their puppy cared for properly during the day, with toilet breaks, play, feeding, and calm one-on-one attention at home.',
'They are also the first stage of the Goodwalk journey. For puppies who may later join our Pack Walks, these visits help build familiarity, confidence, and the early routines that make that transition much smoother.',
'They are also the first stage of the Goodwalk journey. For puppies who may later join our Tiny Gang Pack Walks, these visits help build familiarity, confidence, and the early routines that make that transition much smoother.',
'Instead of just getting through the day, your puppy gets a more thoughtful start, and you get more peace of mind while you are away.',
'We offer puppy visits across Auckland Central including Mt Eden, Ponsonby, Grey Lynn, Kingsland, Sandringham, Herne Bay, and surrounding suburbs.'
],
@@ -22,9 +22,26 @@ export const puppyVisitsContent: ServicePageContent = {
},
highlight: {
eyebrow: 'Start well. Grow well.',
title: 'A home visit now can help set your puppy up for calmer routines and future Pack Walks later on',
title: 'A home visit now can help set your puppy up for calmer routines and future Tiny Gang Pack Walks later on',
imageUrl: '/images/auckland-puppy-visits-cavalier-king-charles-spaniel.jpg',
imageAlt: 'Young Cavalier King Charles Spaniel puppy resting at home before future Goodwalk Pack Walk training in Auckland'
imageAlt: 'Young Cavalier King Charles Spaniel puppy resting at home before future Goodwalk Pack Walk training in Auckland',
points: [
{
title: 'Practical support in the middle of the day',
body:
'Toilet breaks, feeding, play, and calming company help puppies get through the day in a way that feels properly cared for.'
},
{
title: 'Early routines that actually matter later',
body:
'Puppy Visits help build familiarity, confidence, and handling habits before your puppy is ready for bigger social adventures.'
},
{
title: 'A more personal start with Goodwalk',
body:
'Your puppy gets to know us early in a calm, familiar environment, which makes any future transition into walks feel much smoother.'
}
]
},
pricing: {
title: 'Choose the visit length that suits your puppy',
@@ -50,17 +67,23 @@ export const puppyVisitsContent: ServicePageContent = {
features: ['Longer home visit', 'More play, settling, and engagement', 'Extra support for younger puppies', 'Best for pups needing more time']
}
],
extras: [
{ label: 'Extra Puppy', note: 'Same household', price: '$15' },
{ label: 'Photo & update check-in', note: 'Included', price: '$0' }
],
scarcityNote: 'Puppy Visit spaces are limited so we can keep care consistent.'
},
benefits: {
title: 'Why Puppy Visits matter early',
intro:
'The puppy stage moves fast. Regular daytime visits give your puppy support now while also building the calm routines and familiarity that make later life easier.',
items: [
{
title: 'Fewer long stretches alone',
body: 'Regular visits break up the day, help with toilet timing, and give your puppy company, care, and comfort while you are out.'
},
{
title: 'Better foundations for future Pack Walks',
title: 'Better foundations for future Tiny Gang Pack Walks',
body: 'For puppies who may later join our Tiny Gang, early visits help build confidence, familiarity, and the routines that support a smoother next step.'
},
{
+2 -2
View File
@@ -1,5 +1,5 @@
export interface SharedServiceDefinition {
title: 'Pack Walks' | '1:1 Walks' | 'Puppy Visits';
title: 'Tiny Gang Pack Walks' | '1:1 Walks' | 'Puppy Visits';
href: string;
icon: string;
megaMenuDescription: string;
@@ -10,7 +10,7 @@ export interface SharedServiceDefinition {
export const sharedServices: SharedServiceDefinition[] = [
{
title: 'Pack Walks',
title: 'Tiny Gang Pack Walks',
href: '/pack-walks',
icon: 'fas fa-paw',
megaMenuDescription: 'Tiny Gang outdoor adventures',
+8 -2
View File
@@ -1,6 +1,6 @@
export const staticPages = {
'pack-walks': {
title: 'Pack Walks for Small Dogs | Mt Eden, Kingsland & Auckland Central | Goodwalk',
title: 'Tiny Gang Pack Walks for Small Dogs | Mt Eden, Kingsland & Auckland Central | Goodwalk',
description:
'Tiny Gang pack walks for small and medium dogs across Mt Eden, Kingsland, Ponsonby, Grey Lynn and Auckland Central. Small groups, calm outings, free pickup and drop-off.',
canonicalPath: '/pack-walks'
@@ -18,7 +18,7 @@ export const staticPages = {
canonicalPath: '/puppy-visits'
},
'our-pricing': {
title: 'Dog Walking Prices Auckland | Pack Walks & 1:1 Walks | Goodwalk',
title: 'Dog Walking Prices Auckland | Tiny Gang Pack Walks & 1:1 Walks | Goodwalk',
description:
'Transparent pricing for Goodwalk pack walks, 1:1 dog walks, and puppy visits across Auckland Central. From $49.50 per walk. Free Meet & Greet included.',
canonicalPath: '/our-pricing'
@@ -29,6 +29,12 @@ export const staticPages = {
'Meet Alessandra, founder of Goodwalk — Auckland Central\'s small dog walking specialist. Serving Mt Eden, Kingsland, Ponsonby, Grey Lynn and surrounding suburbs with 30+ five-star reviews.',
canonicalPath: '/about'
},
testimonials: {
title: 'Goodwalk Reviews | Testimonials From Auckland Dog Owners',
description:
'Read testimonials from Goodwalk clients across Auckland Central. Real reviews from dog owners whose dogs come home happier, calmer, and better settled.',
canonicalPath: '/testimonials'
},
'contact-us': {
title: 'Book a Dog Walker in Auckland | Contact Goodwalk',
description:
+1 -1
View File
@@ -101,7 +101,7 @@ export const termsAndConditionsContent: LegalPageContent = {
]
},
{
title: '5.11. Pack Walks',
title: '5.11. Tiny Gang Pack Walks',
blocks: [
{
type: 'list',
+17
View File
@@ -23,11 +23,24 @@
touch-action: manipulation;
}
.btn-with-arrow {
gap: 12px;
}
.btn-with-arrow .icon {
font-size: 14px;
transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) {
.btn:hover {
transform: translateY(-2px) scale(1.012);
box-shadow: 0 12px 24px rgba(17, 20, 24, 0.14);
}
.btn-with-arrow:hover .icon {
transform: translateX(2px);
}
}
.btn:active {
@@ -81,4 +94,8 @@
width: fit-content;
margin-inline: auto;
}
.btn-with-arrow {
gap: 10px;
}
}
+92 -95
View File
@@ -4,23 +4,20 @@
.booking-header {
text-align: center;
margin-bottom: 44px;
margin-bottom: 36px;
}
.booking-eyebrow {
display: inline-block;
margin-bottom: 14px;
padding: 7px 12px;
margin-bottom: 12px;
padding: 6px 10px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.08);
color: var(--gw-green);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
}
/* Intentional exception: this is a conversion-focused hero title, not a
standard section heading. */
.booking-title {
display: block;
text-align: center;
@@ -65,10 +62,10 @@
min-height: 40px;
padding: 8px 14px;
border-radius: 999px;
background: #fff;
background: linear-gradient(180deg, #ffffff 0%, #fbfaf6 100%);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 10px 24px rgba(17, 20, 24, 0.04);
0 12px 26px rgba(17, 20, 24, 0.05);
color: var(--gw-green);
font-size: 14px;
font-weight: 600;
@@ -123,6 +120,12 @@
justify-content: center;
gap: 28px;
margin-top: 24px;
padding: 18px 22px;
border-radius: 28px;
background: linear-gradient(180deg, #ffffff 0%, #f8f7f2 100%);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 14px 32px rgba(17, 20, 24, 0.05);
}
.booking-step {
@@ -150,7 +153,7 @@
font-family: var(--font-head);
font-size: 24px;
font-weight: 700;
background: #fff;
background: linear-gradient(180deg, #ffffff 0%, #f8f7f2 100%);
transition:
background 0.2s,
border-color 0.2s,
@@ -159,8 +162,9 @@
}
.booking-step.active .booking-step-number {
background: rgba(33, 48, 33, 0.1);
background: rgba(33, 48, 33, 0.12);
border-color: var(--gw-green);
box-shadow: 0 10px 24px rgba(17, 20, 24, 0.08);
}
@media (hover: hover) {
@@ -175,9 +179,10 @@
}
.booking-step-label {
font-family: var(--font-head);
font-size: 18px;
font-family: var(--font-body);
font-size: 16px;
font-weight: 700;
letter-spacing: 0.01em;
}
.booking-step-divider {
@@ -199,10 +204,12 @@
}
.booking-panel-banner {
background: linear-gradient(180deg, #f6f2ea 0%, #f1ece3 100%);
background:
radial-gradient(circle at top right, rgba(255, 209, 0, 0.16), transparent 32%),
linear-gradient(180deg, #ffffff 0%, #f5f1e8 100%);
color: #34363a;
border-radius: 28px 28px 0 0;
padding: 22px 28px 28px;
padding: 20px 24px 22px;
text-align: center;
font-family: var(--font-body);
font-size: 15px;
@@ -210,8 +217,8 @@
border: 1px solid rgba(17, 20, 24, 0.06);
border-bottom: none;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.18),
0 8px 22px rgba(17, 20, 24, 0.035);
inset 0 0 0 1px rgba(255, 255, 255, 0.28),
0 10px 24px rgba(17, 20, 24, 0.04);
}
.booking-card-grid {
@@ -236,16 +243,16 @@
}
.booking-field-card {
background: #fff;
background: linear-gradient(180deg, #ffffff 0%, #fdfcf9 100%);
border-radius: 28px;
padding: 32px 38px 30px;
padding: 28px 32px 26px;
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 10px 30px rgba(17, 20, 24, 0.04);
0 14px 32px rgba(17, 20, 24, 0.04);
}
.booking-field-card-group {
padding: 30px 32px;
padding: 26px 28px;
}
.booking-field-card-wide,
@@ -259,7 +266,7 @@
.booking-field-group {
display: grid;
gap: 24px;
gap: 20px;
}
.booking-field-group-owner {
@@ -284,10 +291,12 @@
align-items: center;
gap: 7px;
margin-bottom: 10px;
font-family: var(--font-head);
font-size: 15px;
font-family: var(--font-body);
font-size: 14px;
font-weight: 700;
color: #34363a;
letter-spacing: 0.02em;
text-transform: uppercase;
color: #4a4f55;
}
.booking-help-text {
@@ -297,10 +306,59 @@
color: #666;
}
.booking-inline-switch {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 8px;
color: #5f6369;
font-size: 14px;
line-height: 1.5;
}
.booking-inline-link {
border: none;
background: none;
padding: 0;
color: var(--gw-green);
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 0.18em;
}
.booking-field-stack .booking-service-label {
margin-bottom: 14px;
}
.booking-selected-service-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.booking-selected-service-row .booking-service-label {
margin-bottom: 0;
}
.booking-selected-service-chip {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 10px 16px;
border-radius: 18px;
background: linear-gradient(180deg, rgba(33, 48, 33, 0.08), rgba(33, 48, 33, 0.05));
color: var(--gw-green);
font-family: var(--font-body);
font-size: 14px;
font-weight: 700;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
}
.booking-required {
color: var(--yellow);
}
@@ -309,12 +367,13 @@
.booking-field-card textarea {
width: 100%;
border: 2px solid rgba(33, 48, 33, 0.18);
border-radius: 20px;
background: #fff;
padding: 16px 28px;
border-radius: 16px;
background: #fffdfa;
padding: 13px 18px;
font-size: 16px;
line-height: 1.2;
color: #34363a;
box-shadow: inset 0 1px 2px rgba(17, 20, 24, 0.03);
transition:
background 0.2s,
border-color 0.2s;
@@ -322,7 +381,8 @@
.booking-field-card input:hover,
.booking-field-card textarea:hover {
background: #f8f5ef;
border-color: rgba(33, 48, 33, 0.3);
background: #fff;
}
.booking-field-card input:focus,
@@ -333,7 +393,7 @@
}
.booking-field-card textarea {
min-height: 140px;
min-height: 124px;
resize: vertical;
}
@@ -351,69 +411,6 @@
0 10px 30px rgba(17, 20, 24, 0.04);
}
.booking-toggle-group {
display: flex;
flex-wrap: wrap;
gap: 14px;
}
.booking-toggle-option {
display: inline-flex;
align-items: center;
gap: 12px;
min-height: 56px;
padding: 14px 18px;
border: 2px solid rgba(33, 48, 33, 0.18);
border-radius: 18px;
font-family: var(--font-head);
font-size: 15px;
font-weight: 700;
color: #34363a;
cursor: pointer;
transition:
background 0.2s,
border-color 0.2s,
transform 0.15s ease;
}
.booking-toggle-option:hover {
background: #f8f5ef;
}
.booking-toggle-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.booking-toggle-indicator {
width: 24px;
height: 24px;
border: 2px solid rgba(33, 48, 33, 0.28);
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
transition:
background 0.2s,
border-color 0.2s;
}
.booking-toggle-option input:checked + .booking-toggle-indicator {
border-color: var(--gw-green);
background: rgba(33, 48, 33, 0.12);
}
.booking-toggle-option input:checked + .booking-toggle-indicator::after {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--gw-green);
}
.booking-service-options {
display: flex;
flex-wrap: wrap;
@@ -468,7 +465,7 @@
.booking-actions {
display: flex;
align-items: center;
margin-top: 28px;
margin-top: 24px;
}
.booking-actions-next {
+31 -55
View File
@@ -13,10 +13,7 @@ header {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
padding: 11px 24px;
flex-wrap: nowrap;
overflow: hidden;
padding: 10px 18px;
}
.nav-ribbon-item {
@@ -26,52 +23,33 @@ header {
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--gw-green);
white-space: nowrap;
padding: 0 28px;
text-align: center;
}
.nav-ribbon-item .icon {
font-size: 14px;
}
.nav-ribbon-divider {
width: 1px;
height: 14px;
background: rgba(33, 48, 33, 0.2);
flex: none;
font-size: 13px;
}
@media (max-width: 768px) {
.nav-ribbon {
padding: 14px 16px;
padding: 11px 14px;
}
.nav-ribbon-item {
flex: 1;
justify-content: center;
padding: 0;
gap: 7px;
font-size: 9px;
line-height: 1.25;
letter-spacing: 0.045em;
white-space: nowrap;
white-space: normal;
}
.nav-ribbon-item .icon {
font-size: 14px;
}
/* Hide the third ribbon item and its preceding divider on mobile */
.nav-ribbon > :nth-child(4),
.nav-ribbon > :nth-child(5) {
display: none;
}
.nav-ribbon-divider {
display: none;
font-size: 12px;
}
}
@@ -79,7 +57,6 @@ nav,
.mobile-menu,
.hero-inner,
.intro-inner,
.promise-inner,
.services-inner,
.values-inner,
.testimonials-inner,
@@ -112,6 +89,25 @@ nav {
list-style: none;
}
/* Left group hugs the centre logo; right group + actions sit in column 3 */
.nav-links-left {
justify-content: flex-end;
padding-right: 34px;
}
.nav-end {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding-left: 34px;
}
/* Breathing room so the nav groups don't crowd the centre logo */
.logo {
margin: 0 44px;
}
.nav-links li {
position: relative;
}
@@ -327,20 +323,17 @@ nav {
.mobile-phone {
display: none;
align-items: center;
gap: 8px;
padding: 8px 12px;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: #fff;
font-size: 14px;
font-weight: 700;
line-height: 1;
text-decoration: none;
white-space: nowrap;
}
.mobile-phone .icon {
font-size: 14px;
font-size: 16px;
}
.instagram-icon {
@@ -394,15 +387,13 @@ nav {
transform: translateY(1px) scale(0.94);
}
.hero-inner,
.promise-inner {
.hero-inner {
display: flex;
align-items: center;
gap: 60px;
width: 100%;
}
.promise-inner,
.services-inner,
.values-inner,
.testimonials-inner,
@@ -411,25 +402,10 @@ nav {
padding: 0 50px;
}
.services-grid,
.testimonials-grid {
margin-top: 48px;
}
.services-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
}
.values-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0;
margin-top: 48px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.testimonials-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
+86 -144
View File
@@ -5,7 +5,6 @@
padding-right: var(--space-container-x-tablet);
}
.promise-inner,
.services-inner,
.values-inner,
.testimonials-inner,
@@ -78,6 +77,7 @@
}
.logo {
margin: 0;
justify-content: flex-start;
justify-self: start;
}
@@ -91,19 +91,16 @@
display: inline-flex;
justify-self: center;
align-self: center;
min-height: 44px;
padding: 11px 14px;
width: 40px;
height: 40px;
min-height: 40px;
padding: 0;
background: rgba(33, 48, 33, 0.1);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0.01em;
}
.mobile-phone .icon {
font-size: 12px;
font-size: 15px;
}
.nav-links {
@@ -112,7 +109,13 @@
.nav-right {
display: flex;
gap: 18px;
align-items: center;
gap: 12px;
justify-content: flex-end;
}
.nav-end {
display: flex;
justify-content: flex-end;
grid-column: 3;
}
@@ -123,7 +126,7 @@
.instagram-icon {
color: #0a304e;
font-size: 24px;
font-size: 22px;
}
.hamburger {
@@ -170,6 +173,15 @@
visibility 160ms ease;
}
.mobile-menu-backdrop {
position: absolute;
inset: 0;
border: none;
padding: 0;
background: transparent;
cursor: pointer;
}
.mobile-menu-shell.open {
opacity: 1;
visibility: visible;
@@ -312,10 +324,6 @@
transform: none;
}
.hero-floating-pill {
display: none;
}
/* Text content sits above the gradient (z-index: 2) */
.hero-inner {
position: relative;
@@ -366,27 +374,27 @@
white-space: pre-line;
}
/* Subtitle and chips not needed — photo + kicker carry the context */
.hero-subtitle,
.hero-subtitle-desktop,
.hero-subtitle-desktop {
display: block;
margin: 0 0 16px;
max-width: 26ch;
font-size: 15px;
line-height: 1.45;
}
.hero-chips {
display: none;
}
.hero-trust-chip {
width: 100%;
justify-content: center;
margin-bottom: 14px;
padding: 9px 12px;
gap: 8px;
font-size: 11px;
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.18);
margin-bottom: 18px;
}
.hero-trust-stars {
font-size: 9px;
gap: 1px;
.hero-chip,
.hero-trust-chip {
min-height: 34px;
padding: 8px 12px;
font-size: 11px;
background: rgba(255, 255, 255, 0.09);
border-color: rgba(255, 255, 255, 0.14);
}
.hero-trust-logo {
@@ -397,11 +405,11 @@
.hero-buttons {
width: 100%;
flex-direction: column;
gap: 8px;
gap: 10px;
padding-right: 0;
}
.hero-buttons .btn {
.hero-buttons .btn-yellow {
width: 100%;
padding: 13px 20px;
font-size: 14px;
@@ -414,91 +422,60 @@
touch-action: manipulation;
}
.hero-buttons .btn-yellow {
background: var(--yellow);
color: #000;
}
.hero-buttons .btn-outline {
background: rgba(255, 255, 255, 0.08);
color: #fff;
border: 1.5px solid rgba(255, 255, 255, 0.3);
}
.hero-buttons .btn:active {
transform: translateY(1px) scale(0.985);
.hero-secondary-link {
justify-content: center;
width: 100%;
font-size: 13px;
}
.hero-buttons .btn-yellow:active {
background: #e6bb00;
}
.hero-buttons .btn-outline:active {
background: rgba(255, 255, 255, 0.14);
}
#intro {
padding: 30px 24px 24px;
padding: 20px 24px;
}
.intro-trust-badge {
grid-template-columns: 1fr;
gap: 14px;
padding: 18px 18px 16px;
border-radius: 22px;
grid-template-columns: auto minmax(0, 1fr);
gap: 12px;
align-items: center;
padding: 12px 14px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
}
.intro-trust-mark {
width: 48px;
height: 48px;
font-size: 21px;
width: 36px;
height: 36px;
font-size: 18px;
box-shadow: none;
}
.intro-google-logo {
width: 24px;
width: 18px;
}
#intro p {
font-size: 17px;
line-height: 1.45;
font-weight: 600;
font-size: 13px;
line-height: 1.3;
font-weight: 700;
margin-bottom: 0;
}
.intro-trust-copy-desktop {
display: none;
}
.intro-trust-copy-mobile {
display: inline;
}
.intro-trust-meta {
flex-direction: column;
align-items: flex-start;
gap: 10px;
display: none;
}
.intro-trust-cta {
gap: 8px;
padding: 8px 14px;
font-size: 15px;
}
.intro-trust-stars {
font-size: 13px;
}
.promise-inner {
flex-direction: column;
padding: 0 var(--space-container-x-mobile);
}
.promise-text {
order: 1;
}
.promise-img {
order: 2;
flex: none;
width: 100%;
max-width: 360px;
margin: 0 auto;
}
.services-grid,
.testimonials-grid,
.info-inner,
.field-group,
@@ -506,30 +483,8 @@
grid-template-columns: 1fr;
}
.values-grid {
grid-template-columns: 1fr;
}
.value-card:nth-child(odd) {
border-right: none;
}
.value-card {
padding: 24px 0;
}
.service-icon-bubble {
width: 78px;
height: 78px;
margin-bottom: 20px;
}
.service-card .service-card-icon {
font-size: 30px;
}
.booking-header {
margin-bottom: 34px;
margin-bottom: 26px;
}
.booking-eyebrow {
@@ -656,21 +611,10 @@
gap: 12px 18px;
}
.booking-toggle-group {
.booking-inline-switch {
flex-direction: column;
}
.booking-toggle-option {
width: 100%;
border-width: 2px;
border-radius: 16px;
padding: 14px 16px;
}
.booking-toggle-indicator {
width: 22px;
height: 22px;
border-width: 2px;
text-align: center;
gap: 4px;
}
.booking-check-option {
@@ -707,12 +651,8 @@
padding: 0 var(--space-container-x-mobile);
}
.section-heading {
font-size: 30px;
}
footer {
padding: 40px var(--space-container-x-mobile) var(--space-container-x-mobile);
padding: 32px var(--space-container-x-mobile) var(--space-container-x-mobile);
}
.footer-inner {
@@ -750,7 +690,7 @@
}
#instagram {
padding: 48px 24px;
padding: 40px 24px;
}
}
@@ -766,13 +706,9 @@
}
.mobile-phone {
gap: 6px;
padding: 10px 12px;
font-size: 12px;
}
.mobile-phone span {
letter-spacing: -0.01em;
width: 38px;
height: 38px;
min-height: 38px;
}
.hero-text h1,
@@ -792,7 +728,7 @@
padding-right: 0;
}
.hero-buttons .btn {
.hero-buttons .btn-yellow {
flex: 1 1 0;
width: 0;
margin-right: 0 !important;
@@ -800,6 +736,12 @@
font-size: 12px;
line-height: 1.1;
}
.hero-secondary-link {
width: auto;
flex: 0 0 auto;
font-size: 12px;
}
}
@media (min-width: 769px) {
+102 -283
View File
@@ -36,10 +36,10 @@
height: auto;
background: linear-gradient(
to bottom,
transparent 25%,
rgba(33, 48, 33, 0.4) 52%,
rgba(33, 48, 33, 0.88) 70%,
#213021 82%
transparent 18%,
rgba(33, 48, 33, 0.26) 48%,
rgba(33, 48, 33, 0.78) 72%,
#213021 86%
);
pointer-events: none;
z-index: 1;
@@ -58,10 +58,6 @@
}
.promise-text {
flex: 1;
}
.hero-text {
display: flex;
flex-direction: column;
@@ -71,69 +67,58 @@
}
.hero-kicker {
display: inline-block;
margin: 0 0 14px;
font-family: var(--font-head);
font-size: 11px;
padding: 7px 12px;
border-radius: 999px;
background: rgba(255, 209, 0, 0.12);
box-shadow: inset 0 0 0 1px rgba(255, 209, 0, 0.2);
font-family: var(--font-body);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.1em;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
color: var(--yellow);
}
.hero-subtitle {
margin: -8px 0 26px;
max-width: 480px;
margin: -6px 0 22px;
max-width: 560px;
color: rgba(255, 255, 255, 0.86);
font-size: 18px;
font-size: var(--body-lead-size);
line-height: 1.55;
text-align: center;
}
.hero-floating-pill {
display: flex;
position: absolute;
top: 28px;
left: 50%;
transform: translateX(-50%);
z-index: 3;
white-space: nowrap;
padding: 10px 20px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.28);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: #fff;
font-family: var(--font-head);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.01em;
line-height: 1;
}
.hero-chips {
display: none;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 22px;
}
.hero-chip,
.hero-trust-chip {
display: inline-flex;
align-items: center;
gap: 10px;
margin-bottom: 22px;
padding: 10px 16px;
gap: 8px;
min-height: 38px;
padding: 8px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.11);
border: 1px solid rgba(255, 255, 255, 0.12);
color: #fff;
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
font-size: 13px;
font-weight: 600;
line-height: 1.2;
letter-spacing: 0.01em;
text-decoration: none;
transition:
background 0.2s ease,
transform 0.16s cubic-bezier(0.22, 1, 0.36, 1);
}
:global(.hero-chip .icon) {
color: var(--yellow);
font-size: 12px;
}
.hero-trust-logo {
@@ -142,35 +127,46 @@
flex: 0 0 auto;
}
.hero-trust-stars {
display: inline-flex;
align-items: center;
gap: 2px;
color: var(--yellow);
font-size: 13px;
.hero-trust-chip {
text-decoration: none;
transition: background 0.2s ease, opacity 0.2s ease;
}
@media (hover: hover) {
.hero-trust-chip:hover {
background: rgba(255, 255, 255, 0.16);
transform: translateY(-1px);
}
}
.hero-trust-chip:active {
transform: translateY(1px) scale(0.99);
}
.hero-buttons {
display: flex;
gap: 16px;
gap: 18px;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
:global(.hero-cta-arrow) {
font-size: 11px;
margin-left: 4px;
margin-left: 2px;
}
.hero-secondary-link {
display: inline-flex;
align-items: center;
gap: 8px;
color: rgba(255, 255, 255, 0.86);
font-family: var(--font-head);
font-size: 15px;
font-weight: 700;
text-decoration: none;
transition: color 0.2s ease, opacity 0.2s ease;
}
@media (hover: hover) {
.hero-secondary-link:hover {
color: #fff;
}
}
.hero-img {
@@ -202,7 +198,7 @@
#intro {
background: #fff;
padding: 8px 50px 26px;
padding: 24px 50px;
text-align: left;
}
@@ -215,10 +211,12 @@
margin: 0;
padding: 18px 22px;
border-radius: 26px;
background: linear-gradient(180deg, #ffffff 0%, #fbf8f2 100%);
background:
radial-gradient(circle at top right, rgba(255, 209, 0, 0.14), transparent 30%),
linear-gradient(180deg, #ffffff 0%, #fbf8f2 100%);
box-shadow:
0 1px 0 rgba(17, 20, 24, 0.04),
0 14px 34px rgba(17, 20, 24, 0.06);
0 18px 40px rgba(17, 20, 24, 0.08);
}
.intro-trust-mark {
@@ -280,13 +278,17 @@
min-width: 0;
}
.intro-trust-copy-mobile {
display: none;
}
#intro p {
font-size: 18px;
font-weight: 600;
line-height: 1.35;
font-size: var(--body-lead-size);
font-weight: 700;
line-height: 1.3;
max-width: none;
margin: 0;
color: #34363a;
color: #1f2421;
}
.intro-trust-meta {
@@ -294,7 +296,7 @@
align-items: center;
justify-content: space-between;
gap: 18px;
margin-top: 12px;
margin-top: 10px;
}
.intro-trust-stars {
@@ -309,11 +311,11 @@
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 16px;
padding: 8px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
background: rgba(33, 48, 33, 0.06);
color: var(--gw-green);
font-size: 16px;
font-size: 14px;
font-weight: 700;
text-decoration: none;
box-shadow: inset 0 0 0 1px rgba(14, 27, 41, 0.08);
@@ -356,167 +358,22 @@
}
#promise,
#testimonials,
#info {
#values {
background: var(--off-white);
}
#services,
#testimonials,
#info,
#newlead {
background: #fff;
}
.promise-text p {
margin: 0 auto 28px;
font-size: 16px;
max-width: 520px;
}
.promise-text .btn {
display: block;
width: fit-content;
margin: 0 auto;
}
.promise-text {
order: 2;
text-align: center;
}
.promise-img {
order: 1;
flex: 0 0 48%;
max-width: 560px;
display: flex;
justify-content: center;
align-items: flex-end;
}
.promise-img-frame {
position: relative;
width: min(100%, 520px);
padding: 18px 18px 32px 32px;
border-radius: 32px;
background: linear-gradient(180deg, #faf8f3 0%, #f2eee6 100%);
box-shadow: 0 18px 34px rgba(17, 20, 24, 0.08);
}
.promise-img-frame::before {
content: '';
position: absolute;
left: 0;
right: 46px;
bottom: 0;
height: 18px;
border-radius: 999px;
background: var(--gw-green);
opacity: 0.14;
pointer-events: none;
}
.promise-img img {
width: 100%;
max-width: 560px;
display: block;
border-radius: 28px 28px 88px 28px;
box-shadow: 0 10px 24px rgba(17, 20, 24, 0.1);
object-fit: cover;
}
.service-card {
background: var(--off-white);
border-radius: 28px;
padding: 40px 32px;
text-align: center;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
transition:
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.22s ease;
}
.service-icon-bubble {
display: inline-flex;
align-items: center;
justify-content: center;
width: 88px;
height: 88px;
margin: 0 auto 24px;
border-radius: 50%;
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 10px 24px rgba(17, 20, 24, 0.08);
}
.service-icon-bubble {
background: linear-gradient(135deg, #ffd54a, var(--yellow-soft));
transform: translateY(0) rotate(0deg) scale(1);
transition:
transform 0.2s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.2s ease;
}
@media (hover: hover) {
.service-card:hover {
transform: translateY(-2px);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 8px 40px rgba(0, 0, 0, 0.08);
filter: brightness(1.02);
}
}
@media (hover: hover) and (min-width: 769px) {
.service-card:hover .service-icon-bubble {
transform: translateY(-3px) rotate(-5deg) scale(1.04);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 16px 28px rgba(17, 20, 24, 0.14);
}
.service-card:nth-child(even):hover .service-icon-bubble {
transform: translateY(-3px) rotate(5deg) scale(1.04);
}
}
.service-card:active {
transform: translateY(-1px) scale(0.992);
}
.service-card .service-card-icon {
font-size: 34px;
color: var(--gw-green);
margin-bottom: 0;
}
.service-card-price {
display: inline-block;
margin: 14px 0 0;
padding: 6px 14px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
letter-spacing: 0.02em;
}
.service-card a.btn {
margin-top: 18px;
}
.service-card-price + a.btn {
margin-top: 14px;
}
#values,
footer {
background: var(--gw-green);
color: #fff;
}
#values .section-heading,
#testimonials .section-heading {
text-align: center;
}
@@ -529,64 +386,6 @@ footer {
padding: 60px 50px 32px;
}
.values-inner .section-heading {
color: #fff;
}
.value-card {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 22px;
padding: 36px 32px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.2s ease;
}
.value-card:nth-child(odd) {
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
@media (hover: hover) {
.value-card:hover {
background: rgba(255, 255, 255, 0.04);
}
}
.value-icon-wrap {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.08);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
margin-top: 2px;
}
.value-card .value-card-icon {
font-size: 22px;
color: var(--yellow);
}
.value-text h3 {
margin: 0 0 8px;
font-family: var(--font-head);
font-size: 17px;
font-weight: 700;
color: #fff;
line-height: 1.2;
}
.value-card p {
margin: 0;
font-size: 14px;
line-height: 1.7;
opacity: 0.82;
}
.testimonial-card {
background: linear-gradient(180deg, #ffffff 0%, var(--off-white) 100%);
border-radius: 28px;
@@ -709,7 +508,7 @@ footer {
margin: 0;
padding: 16px 22px 20px;
color: var(--gray);
font-size: 15px;
font-size: var(--body-copy-size);
line-height: 1.65;
}
@@ -721,7 +520,7 @@ footer {
.instagram-blurb {
margin: -8px 0 24px;
font-size: 16px;
font-size: var(--body-copy-size);
color: rgba(0, 0, 0, 0.6);
}
@@ -873,11 +672,16 @@ footer {
}
.footer-back-top {
border: none;
background: none;
color: inherit;
font: inherit;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
opacity: 0.5;
cursor: pointer;
flex: 0 0 auto;
transition: opacity 0.2s;
}
@@ -906,7 +710,12 @@ footer {
}
.page-header--green .eyebrow {
color: rgba(255, 255, 255, 0.55);
display: inline-block;
padding: 7px 12px;
border-radius: 999px;
background: rgba(255, 209, 0, 0.12);
box-shadow: inset 0 0 0 1px rgba(255, 209, 0, 0.2);
color: var(--yellow);
}
/* White variant */
@@ -1047,10 +856,20 @@ footer {
gap: 24px;
}
.ph-copy {
text-align: center;
}
.ph-title {
font-size: clamp(28px, 7vw, 38px);
}
.ph-subtitle {
margin-left: auto;
margin-right: auto;
text-align: center;
}
.ph-chips {
gap: 8px;
}
+92 -32
View File
@@ -47,29 +47,48 @@
font-weight: 500;
}
.section-heading,
.promise-text h2,
.info-block h2,
#instagram h2 {
h2,
h3 {
font-family: var(--font-head);
font-weight: 700;
line-height: 1.08;
letter-spacing: -0.02em;
text-wrap: balance;
}
h2 {
font-weight: 700;
line-height: 1.06;
}
h3 {
font-weight: 700;
line-height: 1.16;
}
.section-heading,
.info-block h2,
#instagram h2 {
font-family: var(--font-head);
font-weight: 700;
line-height: 1.06;
letter-spacing: -0.025em;
text-wrap: balance;
}
.section-heading {
font-size: 42px;
font-size: var(--heading-section-size);
font-weight: 800;
line-height: 1.03;
letter-spacing: -0.035em;
color: #000;
margin-bottom: 20px;
margin: 0;
}
.hero-text h1 {
font-family: var(--font-head);
font-size: clamp(34px, 4vw, 56px);
font-weight: 800;
line-height: 1.05;
letter-spacing: -0.04em;
line-height: 1.03;
letter-spacing: -0.045em;
margin-bottom: 28px;
color: #fff;
}
@@ -79,15 +98,27 @@
* heading. Use this instead of bespoke per-section kickers.
*/
.eyebrow {
font-family: var(--font-head);
font-size: 13px;
font-family: var(--font-body);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--gw-green);
margin: 0 0 14px;
}
.section-header {
text-align: center;
}
.section-header .eyebrow {
display: inline-block;
}
.section-header .section-heading {
margin: 0;
}
.hero-text h1 .hero-heading-mobile {
display: none;
}
@@ -101,12 +132,6 @@
color: var(--yellow);
}
.promise-text h2 {
font-size: 42px;
margin-bottom: 20px;
}
.promise-text p,
.info-block p,
.faq details p,
.testimonial-card blockquote {
@@ -114,36 +139,71 @@
line-height: 1.72;
}
.service-card h3,
.value-card h3 {
.section-intro {
max-width: 720px;
margin: 16px auto 0;
color: #4c5056;
font-size: var(--body-lead-size);
line-height: 1.65;
}
.service-card h3 {
font-family: var(--font-head);
font-weight: 700;
}
.service-card h3 {
font-size: 20px;
margin-bottom: 12px;
}
.value-card h3 {
font-size: 18px;
font-size: var(--heading-card-size);
margin-bottom: 10px;
}
/*
* Intentional heading variants:
* - Info uses a smaller scale to suit the two-column blocks.
* - Instagram uses a compact CTA heading inside a tighter panel.
*/
.info-block h2 {
font-size: 30px;
font-size: clamp(28px, 2.4vw, 32px);
margin-bottom: 16px;
}
.info-block h3 {
font-size: 16px;
font-size: var(--heading-card-size);
font-weight: 700;
margin: 20px 0 8px;
margin: 18px 0 8px;
}
#instagram h2 {
font-size: 36px;
margin-bottom: 20px;
font-size: clamp(30px, 3vw, 36px);
margin-bottom: 16px;
}
@media (max-width: 768px) {
.eyebrow {
display: block;
width: fit-content;
margin-left: auto;
margin-right: auto;
text-align: center;
}
.section-heading,
.section-intro,
.info-block h2,
#instagram h2 {
text-align: center;
}
/* .section-heading scales fluidly via clamp() — no hard mobile jump needed */
.section-intro {
font-size: var(--body-lead-size-mobile);
line-height: 1.55;
}
.service-card h3,
.info-block h3 {
font-size: var(--heading-card-size-mobile);
}
}
footer h4 {
+17 -6
View File
@@ -27,25 +27,36 @@
/* Neutrals */
--gray: #59606d;
--beige: #e5d6c2; /* warm surfaces, image frames */
--off-white: #fbfbfb;
--off-white: #f8f7f2;
--surface-light: #f7f8f6; /* elevated light surface — above off-white */
--text: #2e3031;
--shadow-panel-elevated: 0 22px 52px rgba(17, 20, 24, 0.1);
/* Layout */
--max-w: 1280px;
--space-container-x: var(--space-9);
--space-container-x-tablet: 30px;
--space-container-x-mobile: var(--space-6);
--space-section-featured-y: var(--space-14);
--space-section-support-y: var(--space-12);
--space-section-form-y: var(--space-13);
--space-section-page-y: var(--space-13);
--space-section-mobile-y: var(--space-11);
/* Section vertical rhythm two predictable tiers:
feature (80) for key sections, standard (72) for the rest.
All tiers collapse to a single value on mobile for an even flow. */
--space-section-featured-y: var(--space-12);
--space-section-support-y: var(--space-11);
--space-section-form-y: var(--space-11);
--space-section-page-y: var(--space-11);
--space-section-mobile-y: var(--space-8);
--space-hero-inner-bottom: var(--space-7);
/* Typography */
--font-body: 'Readex Pro', sans-serif;
--font-head: 'Unbounded', sans-serif;
--heading-section-size: clamp(30px, 4.6vw, 44px);
--heading-card-size: 20px;
--heading-card-size-mobile: 18px;
--body-lead-size: 17px;
--body-lead-size-mobile: 15px;
--body-copy-size: 16px;
--body-copy-size-mobile: 15px;
}
@media (max-width: 768px) {
+68 -1
View File
@@ -6,6 +6,8 @@
import MobileBookBar from '$lib/components/MobileBookBar.svelte';
import RouteSkeleton from '$lib/components/RouteSkeleton.svelte';
import { isMobileCtaButtonEnabled } from '$lib/feature-flags';
import readex400Woff2 from '@fontsource/readex-pro/files/readex-pro-latin-400-normal.woff2?url';
import readex700Woff2 from '@fontsource/readex-pro/files/readex-pro-latin-700-normal.woff2?url';
import '@fontsource/readex-pro/latin-400.css';
import '@fontsource/readex-pro/latin-500.css';
import '@fontsource/readex-pro/latin-600.css';
@@ -16,6 +18,8 @@
import '@fontsource/noto-sans/latin-400.css';
import '@fontsource/noto-sans/latin-500.css';
import '@fontsource/noto-sans/latin-700.css';
import unbounded700Woff2 from '@fontsource/unbounded/files/unbounded-latin-700-normal.woff2?url';
import unbounded800Woff2 from '@fontsource/unbounded/files/unbounded-latin-800-normal.woff2?url';
import '@fontsource/unbounded/latin-400.css';
import '@fontsource/unbounded/latin-600.css';
import '@fontsource/unbounded/latin-700.css';
@@ -36,6 +40,34 @@
const mobileCtaButtonEnabled = isMobileCtaButtonEnabled();
let revealObserver: IntersectionObserver | null = null;
const scrollStoragePrefix = 'goodwalk:scroll:';
function getScrollStorageKey(urlLike: string) {
const url = new URL(urlLike, 'http://goodwalk.local');
return `${scrollStoragePrefix}${url.pathname}${url.search}`;
}
function saveScrollPosition(urlLike = window.location.href) {
if (typeof window === 'undefined') return;
sessionStorage.setItem(getScrollStorageKey(urlLike), String(window.scrollY));
}
function restoreScrollPosition(urlLike = window.location.href) {
if (typeof window === 'undefined' || window.location.hash) return;
const saved = sessionStorage.getItem(getScrollStorageKey(urlLike));
if (!saved) return;
const top = Number(saved);
if (!Number.isFinite(top)) return;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
window.scrollTo({ top, left: 0, behavior: 'auto' });
});
});
}
function shouldAnimateAnchorBounce() {
return typeof window !== 'undefined' && window.innerWidth > 768;
@@ -87,12 +119,40 @@
initClickTracking();
requestAnimationFrame(initReveal);
restoreScrollPosition();
// Same-page hash clicks aren't caught by afterNavigate
function onHashChange() {
bounceSection(window.location.hash);
}
function onPageHide() {
saveScrollPosition();
}
function onVisibilityChange() {
if (document.visibilityState === 'hidden') {
saveScrollPosition();
}
}
function onPageShow(event: PageTransitionEvent) {
if (event.persisted) {
restoreScrollPosition();
}
}
window.addEventListener('hashchange', onHashChange);
return () => window.removeEventListener('hashchange', onHashChange);
window.addEventListener('pagehide', onPageHide);
window.addEventListener('pageshow', onPageShow);
document.addEventListener('visibilitychange', onVisibilityChange);
return () => {
window.removeEventListener('hashchange', onHashChange);
window.removeEventListener('pagehide', onPageHide);
window.removeEventListener('pageshow', onPageShow);
document.removeEventListener('visibilitychange', onVisibilityChange);
};
});
function shouldShowSkeleton() {
@@ -148,6 +208,13 @@
});
</script>
<svelte:head>
<link rel="preload" href={readex400Woff2} as="font" type="font/woff2" crossorigin="anonymous" />
<link rel="preload" href={readex700Woff2} as="font" type="font/woff2" crossorigin="anonymous" />
<link rel="preload" href={unbounded700Woff2} as="font" type="font/woff2" crossorigin="anonymous" />
<link rel="preload" href={unbounded800Woff2} as="font" type="font/woff2" crossorigin="anonymous" />
</svelte:head>
<svelte:body class:mobile-cta-enabled={mobileCtaButtonEnabled} />
<div class="layout-shell">
+5 -3
View File
@@ -6,6 +6,7 @@
import HowItWorksSection from '$lib/components/HowItWorksSection.svelte';
import InfoSection from '$lib/components/InfoSection.svelte';
import InstagramSection from '$lib/components/InstagramSection.svelte';
import IntroStrip from '$lib/components/IntroStrip.svelte';
import BookingSection from '$lib/components/BookingSection.svelte';
import FounderStorySection from '$lib/components/FounderStorySection.svelte';
import ServicesSection from '$lib/components/ServicesSection.svelte';
@@ -130,13 +131,14 @@
<Header navigation={content.navigation} />
<HeroSection hero={content.hero} reviewCta={content.intro.reviewCta} />
<FounderStorySection founderStory={content.founderStory} />
<IntroStrip intro={content.intro} />
<ValuesSection values={content.values} />
<ServicesSection services={content.services} />
<HowItWorksSection content={content.howItWorks} />
<TestimonialsSection testimonials={content.testimonials} seedKey="/" />
<ValuesSection values={content.values} />
<BookingSection booking={content.booking} />
<FounderStorySection founderStory={content.founderStory} />
<InfoSection info={content.info} />
<BookingSection booking={content.booking} />
<InstagramSection instagram={content.instagram} />
<Footer footer={content.footer} />
{/if}
+22 -2
View File
@@ -7,6 +7,7 @@
import LegalPage from '$lib/components/LegalPage.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import PricingPage from '$lib/components/PricingPage.svelte';
import TestimonialsPage from '$lib/components/TestimonialsPage.svelte';
import { aboutPageContent } from '$lib/content/about';
import ServiceLandingPage from '$lib/components/ServiceLandingPage.svelte';
import { dogWalkingContent } from '$lib/content/dog-walking';
@@ -79,7 +80,7 @@
{
'@context': 'https://schema.org',
'@type': 'Service',
name: 'Goodwalk Pack Walks',
name: 'Goodwalk Tiny Gang Pack Walks',
description: data.page.description,
serviceType: 'Pack walks for small and medium dogs',
provider: { '@id': 'https://www.goodwalk.co.nz/#business' },
@@ -90,7 +91,7 @@
},
buildBreadcrumb([
{ name: 'Home', url: siteUrl },
{ name: 'Pack Walks', path: data.page.canonicalPath }
{ name: 'Tiny Gang Pack Walks', path: data.page.canonicalPath }
])
];
} else if (data.slug === 'dog-walking') {
@@ -179,6 +180,23 @@
{ name: 'About Us', path: data.page.canonicalPath }
])
];
} else if (data.slug === 'testimonials') {
seoImage = data.content.testimonials[0]?.imageUrl ?? defaultSeoImage;
seoImageAlt = 'Happy Goodwalk client dogs in Auckland';
pageStructuredData = [
{
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: data.page.title,
description: data.page.description,
url: `${siteUrl}${data.page.canonicalPath}`,
image: absoluteUrl(seoImage)
},
buildBreadcrumb([
{ name: 'Home', url: siteUrl },
{ name: 'Testimonials', path: data.page.canonicalPath }
])
];
}
}
</script>
@@ -206,6 +224,8 @@
<PricingPage content={data.content} pageContent={ourPricingContent} />
{:else if data.slug === 'about'}
<AboutPage pageContent={aboutPageContent} />
{:else if data.slug === 'testimonials'}
<TestimonialsPage content={data.content} />
{:else if data.slug === 'terms-and-conditions'}
<LegalPage pageContent={termsAndConditionsContent} />
{:else if data.slug === 'privacy-policy'}
@@ -56,6 +56,17 @@ describe('static slug page server load', () => {
});
});
it('returns the shared content and page metadata for the testimonials route', async () => {
getSharedPageContent.mockResolvedValue(sharedPageContent);
await expect(load({ params: { slug: 'testimonials' } } as never)).resolves.toEqual({
content: sharedPageContent,
generalEnquiryEnabled: false,
page: staticPages.testimonials,
slug: 'testimonials'
});
});
it('keeps general enquiries disabled on contact-us by default', async () => {
getSharedPageContent.mockResolvedValue(sharedPageContent);
+1
View File
@@ -16,6 +16,7 @@ describe('static slug route page', () => {
['puppy-visits', puppyVisitsContent.hero.title],
['our-pricing', ourPricingContent.subtitle],
['about', aboutPageContent.sections[0].title],
['testimonials', 'What our clients say'],
['contact-us', "Let's meet!"],
['terms-and-conditions', '1. Application of Terms'],
['privacy-policy', 'How we collect your information']
+20
View File
@@ -10,9 +10,29 @@ describe('home page route', () => {
data: createHomepageRouteData()
});
const hero = document.getElementById('hero');
const intro = document.getElementById('intro');
const values = document.getElementById('values');
const services = document.getElementById('services');
const howItWorks = document.getElementById('how-it-works');
const testimonials = document.getElementById('testimonials');
const founderStory = document.getElementById('promise');
const info = document.getElementById('info');
const booking = document.getElementById('newlead');
expect(screen.getByText(homepageContent.hero.highlight)).toBeInTheDocument();
expect(screen.getByText(homepageContent.intro.text)).toBeInTheDocument();
expect(screen.getByText('Calmer dogs. Clearer routines. Less worry.')).toBeInTheDocument();
expect(screen.getByText(homepageContent.howItWorks.title)).toBeInTheDocument();
expect(screen.getByText(homepageContent.info.title)).toBeInTheDocument();
expect(hero?.compareDocumentPosition(intro as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(intro?.compareDocumentPosition(values as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(values?.compareDocumentPosition(services as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(services?.compareDocumentPosition(howItWorks as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(howItWorks?.compareDocumentPosition(testimonials as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(testimonials?.compareDocumentPosition(founderStory as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(founderStory?.compareDocumentPosition(info as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(info?.compareDocumentPosition(booking as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(screen.queryByLabelText(/General enquiry/i)).not.toBeInTheDocument();
expect(document.title).toBe(homepageContent.seo.title);
expect(document.head.innerHTML).toContain('FAQPage');
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

+1 -1
View File
@@ -13,7 +13,7 @@ Goodwalk is Auckland Central's small dog walking specialist, run personally by A
- Speciality: Small and medium dogs
## Services
- Pack Walks (Tiny Gang): https://www.goodwalk.co.nz/pack-walks
- Tiny Gang Pack Walks: https://www.goodwalk.co.nz/pack-walks
- 1:1 Dog Walks: https://www.goodwalk.co.nz/dog-walking
- Puppy Visits: https://www.goodwalk.co.nz/puppy-visits
- Pricing: https://www.goodwalk.co.nz/our-pricing