This commit is contained in:
2026-05-18 09:43:29 +12:00
parent b950229003
commit 6ff970015f
189 changed files with 18603 additions and 2727 deletions
+1 -1
View File
@@ -34,7 +34,7 @@
</div>
</PageHeader>
<BookingSection {booking} {allowGeneralEnquiry} variant="card-stepper" />
<BookingSection {booking} {allowGeneralEnquiry} variant="contact-modern" />
<InfoSection {info} />
</main>
File diff suppressed because it is too large Load Diff
+99
View File
@@ -0,0 +1,99 @@
<script lang="ts">
import { accordion } from '$lib/actions/accordion';
import Icon from '$lib/components/Icon.svelte';
import type { FaqItem } from '$lib/types';
export let title = 'FAQs';
export let intro: string | undefined = undefined;
export let faqs: FaqItem[];
export let emitSchema = true;
export let variant: 'panel' | 'plain' = 'panel';
$: schemaJson = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer
}
}))
});
</script>
<svelte:head>
{#if emitSchema && faqs.length}
{@html `<script type="application/ld+json">${schemaJson}<` + `/script>`}
{/if}
</svelte:head>
<div class:faq-section-plain={variant === 'plain'} class="faq-section">
<h2 class="faq-section-heading">
<span class="faq-section-icon"><Icon name="fas fa-circle-question" /></span>
{title}
</h2>
{#if intro}
<p class="faq-section-intro">{intro}</p>
{/if}
<div use:accordion class="faq">
{#each faqs as faq}
<details>
<summary>{faq.question}</summary>
<p>{faq.answer}</p>
</details>
{/each}
</div>
</div>
<style>
.faq-section-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin-right: 10px;
border-radius: 12px;
background: var(--gw-green);
box-shadow: 0 10px 22px rgba(33, 48, 33, 0.16);
vertical-align: middle;
}
.faq-section-icon :global(.icon) {
color: var(--yellow);
font-size: 16px;
}
.faq-section-heading {
margin: 0 0 14px;
}
.faq-section-intro {
margin: 0 0 18px;
color: #5b6067;
font-size: 16px;
line-height: 1.6;
}
@media (max-width: 768px) {
.faq-section-icon {
display: none;
}
.faq-section-heading {
text-align: left;
white-space: normal;
overflow-wrap: anywhere;
text-wrap: balance;
font-size: clamp(22px, 5.6vw, 26px);
line-height: 1.18;
}
.faq summary,
.faq details p {
text-align: left;
}
}
</style>
+3 -1
View File
@@ -100,7 +100,9 @@
</div>
<div class="footer-locations">
<p class="footer-col-label">Areas we serve</p>
<p class="footer-col-label">
<a href="/locations">Areas we serve</a>
</p>
<ul class="footer-nav">
{#each locationPages as loc}
<li><a href="/locations/{loc.slug}">{loc.suburb}</a></li>
+281 -119
View File
@@ -6,6 +6,12 @@
export let founderStory: FounderStoryContent;
const founderTrustNotes = [
'The same friendly face at the door',
'Little groups, never a crowded van',
'Updates that help you relax while you are out'
];
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.',
@@ -19,154 +25,240 @@
<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
class="founder-avatar-img"
src={founderStory.imageUrl}
alt={founderStory.imageAlt}
loading="lazy"
decoding="async"
/>
{/if}
<div class="founder-intro">
<span class="eyebrow founder-kicker">A note from Aless</span>
<span class="founder-greeting">Hi, I'm Aless.</span>
</div>
<div class="founder-signoff-text">
<span class="founder-name">Aless</span>
<span class="founder-role">Goodwalk founder</span>
<h2 class="founder-heading">
<span class="founder-heading-main">{founderStory.title}</span>
<span class="founder-heading-sub">Goodwalk is built around trust.</span>
</h2>
<div class="founder-trust-strip" aria-label="What owners can expect from Goodwalk">
<span class="founder-trust-label">What owners notice first</span>
<ul class="founder-trust-list">
{#each founderTrustNotes as note}
<li>{note}</li>
{/each}
</ul>
</div>
</div>
<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>
<div class="founder-body">
{#each founderStoryParagraphs as paragraph}
<p>{paragraph}</p>
{/each}
</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>
<p class="founder-closing">
Ready to <strong>{founderStory.emphasis}</strong>
</p>
<div class="founder-actions">
<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>
<a href={founderStory.cta.href} class="btn btn-green btn-with-arrow btn-hide-arrow-mobile founder-cta">
{founderStory.cta.label}
<Icon name="fas fa-arrow-right" />
</a>
</div>
<div class="founder-signoff">
<div class="founder-signoff-copy">
<p class="founder-signoff-name">Aless, founder of Goodwalk</p>
<p class="founder-signoff-line">The same calm face at the door, the same trusted routine for your dog.</p>
</div>
<div class="founder-media-card">
{#if founderStoryEnhanced}
<enhanced:img
class="founder-portrait"
src={founderStoryEnhanced}
alt={founderStory.imageAlt}
loading="lazy"
decoding="async"
/>
{:else}
<img
class="founder-portrait"
src={founderStory.imageUrl}
alt={founderStory.imageAlt}
loading="lazy"
decoding="async"
/>
{/if}
</div>
</div>
</article>
</div>
</section>
<style>
/* A quiet, letter-style sign-off — left-aligned, one clean panel, no ornament. */
#promise {
content-visibility: auto;
contain-intrinsic-size: 980px;
}
.founder-inner {
max-width: var(--max-w);
max-width: 880px;
margin: 0 auto;
padding: 0 50px;
}
.founder-media-card {
overflow: hidden;
width: min(100%, 168px);
border-radius: 24px;
background:
linear-gradient(180deg, oklch(0.97 0.02 100) 0%, oklch(0.93 0.03 102) 100%);
box-shadow: 0 16px 30px rgba(17, 20, 24, 0.08);
}
.founder-portrait {
display: block;
width: 100%;
aspect-ratio: 0.86;
object-fit: cover;
object-position: center 24%;
}
.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);
padding: clamp(32px, 4vw, 48px);
background: var(--surface-panel);
border: 1px solid var(--border-soft);
border-radius: 34px;
box-shadow: 0 24px 60px rgba(17, 20, 24, 0.06);
}
.founder-intro {
display: grid;
gap: 8px;
margin-bottom: 18px;
}
.founder-kicker {
display: block;
letter-spacing: 0.14em;
display: inline-block;
color: var(--text-subtle);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.founder-greeting {
color: oklch(0.29 0.02 118);
font-family: var(--font-head);
font-size: clamp(22px, 2.5vw, 28px);
font-weight: 600;
letter-spacing: -0.03em;
line-height: 1.5;
}
.founder-heading {
margin: 0 0 26px;
display: grid;
gap: 8px;
margin: 0 0 24px;
}
.founder-heading-main {
display: block;
color: #0d1a0d;
color: oklch(0.23 0.02 136);
font-family: var(--font-head);
font-size: clamp(32px, 3.6vw, 46px);
font-weight: 800;
letter-spacing: -0.04em;
line-height: 1;
font-size: clamp(28px, 3.6vw, 42px);
font-weight: 700;
letter-spacing: -0.035em;
line-height: 1.02;
}
.founder-heading-sub {
display: block;
max-width: 28ch;
color: oklch(0.4 0.018 118);
font-size: clamp(16px, 1.8vw, 20px);
font-weight: 500;
line-height: 1.45;
}
.founder-trust-strip {
display: grid;
gap: 10px;
margin: 0 0 28px;
padding-top: 18px;
border-top: 1px solid var(--border-soft);
}
.founder-trust-label {
color: var(--text-subtle);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.founder-trust-list {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
margin: 0;
padding: 0;
list-style: none;
}
.founder-trust-list li {
position: relative;
padding-left: 14px;
color: oklch(0.35 0.017 118);
font-size: 14px;
font-weight: 600;
line-height: 1.5;
}
.founder-trust-list li::before {
content: '';
position: absolute;
top: 0.65em;
left: 0;
width: 6px;
height: 6px;
border-radius: 50%;
background: color-mix(in srgb, var(--gw-green) 72%, white);
}
.founder-body {
display: grid;
gap: 16px;
gap: 18px;
max-width: 67ch;
}
.founder-body p {
margin: 0;
color: #4c5056;
color: oklch(0.39 0.014 105);
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;
.founder-closing {
margin: 24px 0 0;
color: oklch(0.24 0.018 136);
font-family: var(--font-head);
font-size: 16px;
font-weight: 700;
line-height: 1.2;
font-size: clamp(18px, 2vw, 22px);
font-weight: 600;
letter-spacing: -0.025em;
line-height: 1.25;
}
.founder-role {
color: var(--gray);
font-size: 13px;
line-height: 1.3;
.founder-closing strong {
color: var(--gw-green);
font-weight: 800;
}
.founder-cta {
.founder-actions {
display: grid;
gap: 18px;
justify-items: start;
margin-top: 28px;
}
@@ -174,17 +266,16 @@
display: inline-flex;
align-items: center;
gap: 10px;
margin-top: 20px;
padding: 12px 16px;
padding: 0;
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);
color: oklch(0.33 0.018 118);
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;
transition:
color 0.18s ease,
transform 0.18s ease;
}
.founder-contact-wave {
@@ -194,18 +285,60 @@
width: 28px;
height: 28px;
border-radius: 50%;
background: #fff;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.07);
background: var(--surface-panel-muted);
box-shadow: inset 0 0 0 1px var(--border-soft);
font-size: 16px;
flex: 0 0 auto;
}
.founder-cta {
margin-top: 2px;
}
.founder-signoff {
display: flex;
align-items: end;
justify-content: space-between;
gap: 22px;
margin-top: 34px;
padding-top: 24px;
border-top: 1px solid var(--border-soft);
}
.founder-signoff-copy {
display: grid;
gap: 6px;
max-width: 30ch;
}
.founder-signoff-name {
margin: 0;
color: oklch(0.24 0.02 136);
font-family: var(--font-head);
font-size: clamp(20px, 2vw, 24px);
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1.1;
}
.founder-signoff-line {
margin: 0;
color: var(--text-subtle);
font-size: 14px;
line-height: 1.55;
}
@media (hover: hover) {
.founder-media-card:hover .founder-portrait {
transform: scale(1.02);
}
.founder-portrait {
transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.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);
color: var(--gw-green);
transform: translateY(-1px);
}
}
@@ -216,13 +349,12 @@
}
.founder-note {
padding: 26px 24px 24px;
padding: 26px 22px 24px;
border-radius: 24px;
}
.founder-heading {
margin: 0 0 22px;
text-align: center;
}
.founder-body p {
@@ -230,6 +362,25 @@
line-height: 1.7;
}
.founder-media-card {
border-radius: 24px;
width: 132px;
}
.founder-trust-strip {
margin-bottom: 24px;
padding-top: 16px;
}
.founder-trust-list {
display: grid;
gap: 8px;
}
.founder-trust-list li {
font-size: 13px;
}
.founder-contact-note {
display: flex;
width: 100%;
@@ -242,5 +393,16 @@
width: 100%;
justify-content: center;
}
.founder-signoff {
align-items: start;
flex-direction: column;
margin-top: 28px;
padding-top: 20px;
}
.founder-signoff-copy {
max-width: none;
}
}
</style>
+2 -1
View File
@@ -55,12 +55,13 @@ describe('Header', () => {
const menuToggle = container.querySelector('.hamburger') as HTMLButtonElement;
const mobileMenuShell = container.querySelector('.mobile-menu-shell') as HTMLDivElement;
const mobileMenuBackdrop = container.querySelector('.mobile-menu-backdrop') as HTMLButtonElement;
await fireEvent.click(menuToggle);
expect(menuToggle).toHaveAttribute('aria-expanded', 'true');
expect(mobileMenuShell.classList.contains('open')).toBe(true);
await fireEvent.click(mobileMenuShell);
await fireEvent.click(mobileMenuBackdrop);
expect(menuToggle).toHaveAttribute('aria-expanded', 'false');
expect(mobileMenuShell.classList.contains('open')).toBe(false);
});
+34 -11
View File
@@ -10,8 +10,11 @@
$: mobileLead = mobileTitle.includes(hero.highlight)
? mobileTitle.slice(0, mobileTitle.lastIndexOf(hero.highlight))
: mobileTitle;
$: accessibleTitle = `${titleParts.lead}${titleParts.connector ? ` ${titleParts.connector}` : ''} ${hero.highlight}`.trim();
$: proofItems = (hero.subtitleChips ?? []).slice(0, 3);
const trustStars = Array.from({ length: 5 });
function splitTitle(title: string) {
const trimmed = title.trim();
@@ -43,12 +46,20 @@
constrained by hero-inner's stacking context -->
<div class="hero-img">
<picture>
{#if hero.desktopImageWebpUrl}
<source media="(min-width: 769px)" srcset={hero.desktopImageWebpUrl} type="image/webp" />
{/if}
{#if hero.desktopImageUrl}
<source media="(min-width: 769px)" srcset={hero.desktopImageUrl} />
{/if}
{#if hero.imageWebpUrl}
<source srcset={hero.imageWebpUrl} type="image/webp" />
{/if}
<img
src={hero.imageUrl}
alt={hero.imageAlt}
width={hero.imageWidth ?? undefined}
height={hero.imageHeight ?? undefined}
loading="eager"
fetchpriority="high"
/>
@@ -62,7 +73,8 @@
{/if}
<h1 class="hero-heading">
<span class="hero-heading-desktop">
<span class="visually-hidden">{accessibleTitle}</span>
<span class="hero-heading-desktop" aria-hidden="true">
<span class="hero-title-main">{titleParts.lead}</span>
{#if titleParts.connector}
<span class="hero-title-connector"> {titleParts.connector}</span>
@@ -70,11 +82,15 @@
<br />
<span class="hero-title-highlight">{hero.highlight}</span>
</span>
<span class="hero-heading-mobile">
<span class="hero-heading-mobile" aria-hidden="true">
{mobileLead}<span class="hero-title-highlight">{hero.highlight}</span>
</span>
</h1>
{#if hero.seoHeading}
<h2 class="hero-seo-heading">{hero.seoHeading}</h2>
{/if}
{#if hero.subtitle}
<p class="hero-subtitle hero-subtitle-desktop">{hero.subtitle}</p>
{/if}
@@ -95,14 +111,21 @@
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>
<span class="hero-trust-mark" aria-hidden="true">
<img
class="hero-trust-logo"
src="/images/google-g-logo.svg"
alt=""
width="16"
height="17"
/>
</span>
<span class="hero-trust-stars" aria-hidden="true">
{#each trustStars as _, index}
<Icon name="fas fa-star" className={`hero-trust-star hero-trust-star-${index + 1}`} />
{/each}
</span>
<span class="hero-trust-label">{reviewCta.label}</span>
</a>
{/if}
</div>
@@ -113,7 +136,7 @@
href={hero.primaryCta.href}
target={linkTarget(hero.primaryCta.external)}
rel={linkRel(hero.primaryCta.external)}
class="btn btn-yellow btn-with-arrow"
class="btn btn-yellow btn-with-arrow btn-hide-arrow-mobile"
>
{hero.primaryCta.label}
<Icon name="fas fa-arrow-right" />
+8 -8
View File
@@ -40,7 +40,7 @@
<span class="hiw-num">0{index + 1}</span>
</div>
<div class="hiw-icon-wrap">
<Icon name={step.icon} className="hiw-step-icon" />
<Icon name={step.icon ?? 'fas fa-paw'} className="hiw-step-icon" />
</div>
<h3 class="hiw-title">{step.title}</h3>
<p class="hiw-body">{step.body}</p>
@@ -251,8 +251,8 @@
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 34px;
padding: 0 14px;
min-height: 44px;
padding: 0 16px;
border-radius: 999px;
background: var(--gw-green);
box-shadow:
@@ -291,8 +291,8 @@
}
.hiw-journey-pill {
min-height: 32px;
padding: 0 12px;
min-height: 44px;
padding: 0 14px;
font-size: 11px;
}
@@ -347,10 +347,10 @@
/* ── Reveal ── */
:global(.reveal-ready.reveal-block) {
opacity: 0;
transform: translate3d(0, var(--reveal-distance, 24px), 0);
transform: translate3d(0, var(--reveal-distance, 16px), 0);
transition:
opacity 0.55s ease,
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
opacity 0.3s ease,
transform 0.45s cubic-bezier(0.2, 0.8, 0.2, 1);
transition-delay: var(--reveal-delay, 0ms);
}
+21 -21
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { accordion } from '$lib/actions/accordion';
import Icon from '$lib/components/Icon.svelte';
import FaqSection from '$lib/components/FaqSection.svelte';
import { locationPages } from '$lib/content/locations';
import type { InfoContent } from '$lib/types';
@@ -50,29 +50,23 @@
</div>
<div class="info-block">
<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>
<summary>{faq.question}</summary>
<p>{faq.answer}</p>
</details>
{/each}
</div>
<FaqSection title={info.faqTitle} faqs={info.faqs} />
</div>
</div>
</section>
<style>
#info {
content-visibility: auto;
contain-intrinsic-size: 1100px;
}
.info-heading-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
width: 40px;
height: 40px;
margin-right: 10px;
border-radius: 12px;
background: var(--gw-green);
@@ -104,8 +98,8 @@
.info-suburb-chip {
display: inline-flex;
align-items: center;
min-height: 40px;
padding: 8px 14px;
min-height: 44px;
padding: 10px 16px;
border-radius: 999px;
background: #fff;
box-shadow:
@@ -177,6 +171,12 @@
margin-top: 0;
}
@media (max-width: 768px) {
.info-heading-icon {
display: none;
}
}
.info-hours-card p {
margin-bottom: 0;
}
@@ -205,8 +205,8 @@
}
.info-heading-icon {
width: 32px;
height: 32px;
width: 40px;
height: 40px;
margin-right: 8px;
border-radius: 11px;
}
@@ -218,8 +218,8 @@
}
.info-suburb-chip {
min-height: 36px;
padding: 8px 12px;
min-height: 44px;
padding: 10px 14px;
font-size: 13px;
}
+3 -1
View File
@@ -21,13 +21,15 @@
</div>
<div class="instagram-dog-wrap" aria-hidden="true">
<enhanced:img src="$lib/images/dog-cutout.png" alt="" class="instagram-dog" loading="lazy" decoding="async" />
<enhanced:img src="$lib/images/goodwalk-instagram-dog-cutout.webp" alt="" class="instagram-dog" loading="lazy" decoding="async" />
</div>
</div>
</aside>
<style>
#instagram {
content-visibility: auto;
contain-intrinsic-size: 360px;
overflow: hidden;
padding-bottom: 40px;
}
+45 -28
View File
@@ -5,45 +5,62 @@
export let intro: IntroContent;
const stars = Array.from({ length: 5 });
const statement = intro.text.replace(/\.$/, '');
const statementWords = statement.split(/(\s+)/);
</script>
<div id="intro">
<section id="intro" aria-label="Goodwalk at a glance">
<div class="intro-inner">
<div class="intro-trust-badge">
<div class="intro-statement">
<span class="intro-kicker" aria-hidden="true">
<span class="intro-kicker-rule"></span>
Goodwalk · Auckland
</span>
<h2 class="intro-headline">
{#each statementWords as token, index}
{#if /\s+/.test(token)}
{token}
{:else}
<span class="intro-word" style="--word-i: {index};">{token}</span>
{/if}
{/each}
</h2>
</div>
<aside class="intro-trust" aria-label="Reviews">
<a
class="intro-trust-mark intro-trust-mark-link"
class="intro-google"
href={intro.reviewCta.href}
target="_blank"
rel="noopener"
aria-label="Read our Google reviews"
>
<img
class="intro-google-logo"
src="/images/google-g-logo.svg"
alt=""
width="28"
height="29"
/>
</a>
<div class="intro-trust-copy">
<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">
<span class="intro-google-mark" aria-hidden="true">
<img
class="intro-google-logo"
src="/images/google-g-logo.svg"
alt=""
width="22"
height="23"
/>
</span>
<span class="intro-google-copy">
<span class="intro-stars" aria-label="5 star rating">
{#each stars as _, index}
<Icon name="fas fa-star" className={`intro-star intro-star-${index + 1}`} />
{/each}
</div>
<a class="intro-trust-cta" href={intro.reviewCta.href} target="_blank" rel="noopener">
</span>
<span class="intro-google-label">
{intro.reviewCta.label}
</a>
</div>
</div>
</div>
</span>
</span>
</a>
<p class="intro-meta">
<span class="intro-meta-dot" aria-hidden="true"></span>
Auckland Central · Mon&ndash;Fri
</p>
</aside>
</div>
</div>
</section>
+1 -2
View File
@@ -194,8 +194,7 @@
}
:global(.mobile-book-bar-cta .mobile-book-bar-arrow) {
font-size: 12px;
opacity: 0.75;
display: none;
}
@media (prefers-reduced-motion: reduce) {
+37 -16
View File
@@ -1,30 +1,51 @@
<script lang="ts">
import { onMount } from 'svelte';
export let onClose: () => void;
export let ariaLabel: string | undefined = undefined;
export let ariaLabelledBy: string | undefined = undefined;
let hasMounted = false;
function portalToBody(node: HTMLElement) {
document.body.appendChild(node);
return {
destroy() {
node.remove();
}
};
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') onClose();
}
onMount(() => {
hasMounted = true;
});
</script>
<div
class="modal-backdrop"
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
on:click|self={onClose}
on:keydown={handleKeydown}
tabindex="-1"
>
<div class="modal-card">
<button class="modal-close" type="button" aria-label="Close" on:click={onClose}>
&#x2715;
</button>
<slot />
{#if hasMounted}
<div
use:portalToBody
class="modal-backdrop"
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
on:click|self={onClose}
on:keydown={handleKeydown}
tabindex="-1"
>
<div class="modal-card">
<button class="modal-close" type="button" aria-label="Close" on:click={onClose}>
&#x2715;
</button>
<slot />
</div>
</div>
</div>
{/if}
<style>
.modal-backdrop {
File diff suppressed because it is too large Load Diff
+213 -236
View File
@@ -10,50 +10,48 @@
mobileOrder: number;
};
export let variant: 'pricing' | 'service' = 'service';
$: featured = plan.isPopular;
const ctaLabel = 'Book a Meet & Greet';
</script>
<article
class="plan-card"
class:plan-card--popular={plan.isPopular}
class:plan-card--featured={featured}
class:plan-card--supporting={!featured}
class:plan-card--pricing={variant === 'pricing'}
class:plan-card--service={variant === 'service'}
style="--mobile-order:{plan.mobileOrder};"
>
{#if plan.isPopular}
<span class="plan-card__ribbon">
<Icon name="fas fa-star" className="plan-card__ribbon-icon" />
Popular
{#if featured}
<span class="plan-card__ribbon" aria-label="Most chosen routine">
<Icon name="fas fa-paw" className="plan-card__ribbon-icon" />
Goodwalk favourite
</span>
{/if}
<div class="plan-card__header">
<div class="plan-card__eyebrow">
<span class="plan-card__eyebrow-badge">
<Icon name={variant === 'pricing' ? 'fas fa-paw' : 'fas fa-leaf'} className="plan-card__eyebrow-icon" />
</span>
<span>{plan.isPopular ? 'Goodwalk favourite' : variant === 'pricing' ? 'Flexible routine' : 'Tailored support'}</span>
<header class="plan-card__header">
<h3 class="plan-card__title">{plan.title}</h3>
<div class="plan-card__price-block">
<span class="plan-card__price">{plan.price}</span>
<span class="plan-card__period">{plan.period}</span>
</div>
<h3>{plan.title}</h3>
<div class="plan-card__price">{plan.price}</div>
<p class="plan-card__period">{plan.period}</p>
</div>
<div class="plan-card__body">
<p class="plan-card__feature-label">
<Icon name="fas fa-circle-check" className="plan-card__feature-label-icon" />
What's included
</p>
<ul class="plan-card__features">
{#each plan.features as feature}
<li>
<Icon
name={variant === 'pricing' ? 'fas fa-check' : 'fas fa-paw'}
className="plan-card__feature-icon"
/>
<span>{feature}</span>
</li>
{/each}
</ul>
</div>
<a class="btn btn-yellow plan-card__cta" href="#newlead">Book a Meet &amp; Greet</a>
</header>
<ul class="plan-card__features">
{#each plan.features as feature}
<li>
<Icon name="fas fa-check" className="plan-card__feature-icon" />
<span>{feature}</span>
</li>
{/each}
</ul>
<a class="btn btn-yellow plan-card__cta" href="#newlead">
{ctaLabel}
<Icon name="fas fa-arrow-right" />
</a>
</article>
<style>
@@ -62,310 +60,289 @@
position: relative;
display: flex;
flex-direction: column;
border-radius: 28px;
border: 1.5px solid rgba(17, 20, 24, 0.09);
padding: 38px 26px 30px;
overflow: visible;
border-radius: 26px;
padding: 30px 26px 28px;
overflow: hidden;
isolation: isolate;
transition:
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.22s ease,
border-color 0.22s ease,
filter 0.22s ease;
transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.28s ease,
border-color 0.22s ease;
}
.plan-card::before {
content: '';
position: absolute;
inset: 0 0 auto;
height: 88px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));
pointer-events: none;
}
.plan-card--popular {
border: 2px solid var(--yellow);
/* ── Supporting plans (white on cream page) ── */
.plan-card--supporting {
background: var(--surface-panel);
border: 1px solid rgba(33, 48, 33, 0.10);
color: var(--text-heading);
box-shadow:
inset 0 0 0 1px rgba(242, 191, 47, 0.45),
0 14px 34px rgba(17, 20, 24, 0.06);
0 1px 0 rgba(255, 255, 255, 0.7) inset,
0 10px 24px rgba(17, 20, 24, 0.08),
0 2px 6px rgba(17, 20, 24, 0.05);
}
/* ── Service variant ── */
.plan-card--service {
align-items: stretch;
height: 100%;
background: linear-gradient(180deg, rgba(251, 251, 251, 0.98) 0%, rgba(247, 248, 246, 1) 100%);
/* ── Featured plan (green-drenched, brand-true) ── */
.plan-card--featured {
background-color: var(--gw-green);
background-image: none;
border: 1px solid rgba(255, 209, 0, 0.22);
color: rgba(255, 248, 230, 0.96);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.045),
0 8px 40px rgba(0, 0, 0, 0.06);
0 1px 0 rgba(255, 248, 230, 0.08) inset,
0 22px 46px rgba(33, 48, 33, 0.30),
0 4px 10px rgba(33, 48, 33, 0.18);
padding-top: 44px;
}
/* ── Pricing variant ── */
.plan-card--pricing {
align-items: center;
text-align: center;
background: linear-gradient(180deg, rgba(251, 251, 251, 0.98) 0%, rgba(247, 248, 246, 1) 100%);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.045),
0 14px 34px rgba(17, 20, 24, 0.05);
.plan-card--featured .plan-card__header,
.plan-card--featured .plan-card__features {
background: transparent;
}
.plan-card--pricing.plan-card--popular {
background:
radial-gradient(circle at top, rgba(255, 209, 0, 0.12), rgba(255, 209, 0, 0) 40%),
linear-gradient(180deg, rgba(251, 251, 251, 0.98) 0%, rgba(247, 248, 246, 1) 100%);
/* ── Pricing variant: featured can grow taller on desktop for hierarchy ── */
.plan-card--pricing.plan-card--featured {
padding: 48px 28px 32px;
}
.plan-card__header,
.plan-card__body {
position: relative;
z-index: 1;
width: 100%;
}
/* ── Ribbon ── */
/* ── Ribbon (featured only) — hand-pinned stamp ── */
.plan-card__ribbon {
display: inline-flex;
align-items: center;
gap: 6px;
gap: 7px;
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -40%);
padding: 6px 12px;
top: 16px;
left: 22px;
padding: 5px 11px 5px 9px;
border-radius: 999px;
background: linear-gradient(135deg, var(--gw-green), var(--green-mid));
color: #fff;
background: var(--yellow);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
letter-spacing: 0.06em;
text-transform: uppercase;
box-shadow: 0 10px 20px rgba(17, 20, 24, 0.08);
white-space: nowrap;
transform: rotate(-3deg);
transform-origin: 12px 50%;
box-shadow:
0 1px 0 rgba(33, 48, 33, 0.10),
0 8px 18px rgba(255, 209, 0, 0.32);
z-index: 2;
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.plan-card--featured:hover .plan-card__ribbon {
transform: rotate(-1deg) translateY(-1px);
}
@media (prefers-reduced-motion: reduce) {
.plan-card__ribbon,
.plan-card--featured:hover .plan-card__ribbon {
transform: none;
transition: none;
}
}
:global(.plan-card__ribbon-icon) {
color: var(--yellow);
font-size: 10px;
}
.plan-card__eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
margin-bottom: 18px;
color: var(--gw-green);
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
/* ── Header ── */
.plan-card__header {
/* Override the global `header { ... }` in layout.css (page chrome only).
Without this, every card inherits a background band and box-shadow. */
background: transparent;
box-shadow: none;
z-index: 1;
isolation: auto;
overflow: visible;
position: relative;
}
.plan-card__eyebrow-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 12px;
background: linear-gradient(135deg, var(--gw-green), var(--green-mid));
color: #fff;
box-shadow: 0 10px 22px rgba(33, 48, 33, 0.14);
}
:global(.plan-card__eyebrow-icon) {
color: var(--yellow);
font-size: 14px;
}
/* ── Heading ── */
.plan-card h3 {
.plan-card__title {
margin: 0;
font-family: var(--font-head);
font-size: 22px;
line-height: 1.2;
color: #16181d;
font-size: 19px;
font-weight: 600;
line-height: 1.25;
letter-spacing: -0.005em;
}
.plan-card--featured .plan-card__title {
color: rgba(255, 248, 230, 0.96);
}
.plan-card--supporting .plan-card__title {
color: var(--text-heading);
}
/* ── Price block ── */
.plan-card__price-block {
display: flex;
align-items: baseline;
flex-wrap: wrap;
column-gap: 8px;
row-gap: 4px;
margin-top: 18px;
min-width: 0;
}
/* ── Price ── */
.plan-card__price {
font-family: var(--font-head);
line-height: 1;
}
.plan-card--service .plan-card__price {
margin-top: 20px;
font-size: 44px;
color: var(--gw-green);
}
.plan-card--pricing .plan-card__price {
margin-top: 22px;
font-size: 52px;
line-height: 0.95;
letter-spacing: -0.05em;
color: #16181d;
}
/* ── Period ── */
.plan-card__period {
margin: 8px 0 0;
font-size: 14px;
text-transform: uppercase;
}
.plan-card--service .plan-card__period {
color: #5d6166;
font-size: 50px;
font-weight: 600;
letter-spacing: 0.06em;
line-height: 1;
letter-spacing: -0.035em;
}
.plan-card--pricing .plan-card__period {
color: #5e6167;
letter-spacing: 0.08em;
.plan-card--featured .plan-card__price {
color: var(--yellow);
position: relative;
display: inline-block;
}
.plan-card__body {
margin-top: 24px;
padding-top: 18px;
border-top: 1px solid rgba(17, 20, 24, 0.07);
.plan-card--supporting .plan-card__price {
color: var(--text-heading);
font-size: 48px;
}
.plan-card__feature-label {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0 0 14px;
color: #59606d;
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
.plan-card__period {
font-family: var(--font-body);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
:global(.plan-card__feature-label-icon) {
color: var(--gw-green);
font-size: 12px;
.plan-card--featured .plan-card__period {
color: rgba(255, 248, 230, 0.78);
}
.plan-card--supporting .plan-card__period {
color: var(--text-muted);
}
/* ── Features ── */
.plan-card__features {
margin: 0;
padding: 0;
list-style: none;
}
.plan-card--service .plan-card__features {
position: relative;
z-index: 1;
flex: 1 1 auto;
margin: 24px 0 0;
padding: 20px 0 0;
list-style: none;
border-top: 1px solid currentColor;
}
/* Service: bullet style */
.plan-card--service .plan-card__features li {
.plan-card--featured .plan-card__features {
border-top-color: rgba(255, 248, 230, 0.24);
}
.plan-card--supporting .plan-card__features {
border-top-color: rgba(33, 48, 33, 0.10);
}
.plan-card__features li {
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
grid-template-columns: 16px minmax(0, 1fr);
gap: 10px;
color: #34363a;
font-size: 15px;
line-height: 1.5;
align-items: start;
font-size: 14px;
line-height: 1.55;
}
.plan-card--service .plan-card__features li + li {
margin-top: 12px;
.plan-card__features li + li {
margin-top: 10px;
}
:global(.plan-card--service .plan-card__feature-icon) {
margin-top: 3px;
color: var(--yellow-soft);
font-size: 12px;
.plan-card--featured .plan-card__features li {
color: rgba(255, 248, 230, 0.94);
}
/* Pricing: divider style */
.plan-card--pricing .plan-card__features {
width: 100%;
.plan-card--supporting .plan-card__features li {
color: var(--text-heading-soft);
}
.plan-card--pricing .plan-card__features li {
display: grid;
grid-template-columns: 18px minmax(0, 1fr);
gap: 10px;
padding: 15px 0;
border-top: 1px solid rgba(17, 20, 24, 0.08);
color: #34363a;
font-size: 16px;
line-height: 1.5;
text-align: left;
:global(.plan-card__feature-icon) {
margin-top: 4px;
font-size: 11px;
}
.plan-card--pricing .plan-card__features li:first-child {
padding-top: 0;
border-top: none;
:global(.plan-card--featured .plan-card__feature-icon) {
color: var(--yellow);
}
:global(.plan-card--pricing .plan-card__feature-icon) {
margin-top: 3px;
:global(.plan-card--supporting .plan-card__feature-icon) {
color: var(--gw-green);
font-size: 12px;
}
/* ── CTA ── */
.plan-card__cta {
display: flex;
width: fit-content;
margin: 28px auto 0;
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 26px;
align-self: center;
width: auto;
min-width: 180px;
padding-inline: 22px;
font-family: var(--font-head);
}
/* ── Hover ── */
@media (hover: hover) {
.plan-card--service:hover {
.plan-card--supporting:hover {
transform: translateY(-2px);
border-color: rgba(17, 20, 24, 0.14);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.055),
0 10px 40px rgba(0, 0, 0, 0.08);
filter: brightness(1.015);
border-color: rgba(33, 48, 33, 0.28);
}
.plan-card--pricing:hover {
transform: translateY(-2px);
border-color: rgba(17, 20, 24, 0.14);
.plan-card--featured:hover {
transform: translateY(-3px);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 18px 38px rgba(17, 20, 24, 0.08);
filter: brightness(1.012);
inset 0 1px 0 rgba(255, 248, 230, 0.10),
0 26px 52px rgba(33, 48, 33, 0.26);
}
.plan-card--popular:hover {
border-color: var(--yellow);
}
}
.plan-card:active {
transform: translateY(-2px) scale(0.992);
transform: translateY(-1px);
}
/* ── Pricing variant alignment overrides ── */
.plan-card--pricing {
text-align: left;
}
/* ── Mobile ── */
@media (max-width: 768px) {
.plan-card {
order: var(--mobile-order, 0);
width: min(100%, 420px);
width: min(100%, 440px);
margin-inline: auto;
padding: 36px 22px 28px;
padding: 28px 22px 24px;
}
.plan-card__body {
margin-top: 22px;
padding-top: 16px;
.plan-card--featured {
padding-top: 44px;
}
.plan-card--pricing .plan-card__price {
font-size: 46px;
.plan-card--pricing.plan-card--featured {
padding: 44px 22px 24px;
}
.plan-card__price {
font-size: 44px;
}
.plan-card--supporting .plan-card__price {
font-size: 42px;
}
.plan-card__cta {
display: none;
width: 100%;
min-width: 0;
}
}
</style>
+13 -2
View File
@@ -4,13 +4,16 @@
export let title: string;
export let description: string;
export let canonicalPath: string;
export let image = '/images/auckland-dog-walking-happy-dog-hero.png';
export let image = '/images/goodwalk-auckland-happy-dog-hero.webp';
export let imageAlt = 'Goodwalk Auckland dog walking services';
export let type = 'website';
export let structuredData: Record<string, unknown>[] = [];
export let noindex = false;
export let preloadImage = false;
export let preloadImageUrl = ''; // explicit URL to preload (defaults to the og:image)
export let preloadImageType = ''; // mime type, e.g. "image/webp"
export let preloadImageSrcset = ''; // optional responsive srcset
export let preloadImageSizes = ''; // optional sizes attribute
const siteName = 'Goodwalk';
const siteUrl = 'https://www.goodwalk.co.nz';
@@ -52,7 +55,15 @@
<meta name="geo.region" content="NZ-AUK" />
<meta name="geo.placename" content="Auckland Central" />
{#if preloadImage && resolvedPreloadUrl}
<link rel="preload" as="image" href={resolvedPreloadUrl} fetchpriority="high" />
<link
rel="preload"
as="image"
href={resolvedPreloadUrl}
type={preloadImageType || undefined}
imagesrcset={preloadImageSrcset || undefined}
imagesizes={preloadImageSizes || undefined}
fetchpriority="high"
/>
{/if}
<link rel="canonical" href={canonicalUrl} />
<link rel="alternate" hreflang="en-NZ" href={canonicalUrl} />
+369 -54
View File
@@ -6,6 +6,7 @@
import PricingPlanCard from '$lib/components/PricingPlanCard.svelte';
import ServiceHero from '$lib/components/ServiceHero.svelte';
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte';
import FaqSection from '$lib/components/FaqSection.svelte';
import { getEnhancedImage } from '$lib/enhanced-images';
import { decoratePlans } from '$lib/utils/pricing';
import type { ServicePageContent, SiteSharedContent } from '$lib/types';
@@ -25,11 +26,17 @@
})) ?? [];
$: relatedServices = content.services.filter((s) => s.href && s.href !== currentPath);
$: pricingPlans = decoratePlans(pageContent.pricing.plans);
$: introLeadParagraph = pageContent.hero.paragraphs?.[0] ?? '';
$: introBodyParagraphs = pageContent.hero.paragraphs?.slice(1) ?? [];
$: benefitCards = pageContent.benefits.items.map((benefit, index) => ({
...benefit,
tintClass: `service-benefit-tint-${(index % 3) + 1}`,
featured: index === 0
}));
$: useSimpleBenefitSwipe =
currentPath === '/pack-walks' ||
currentPath === '/dog-walking' ||
currentPath === '/puppy-visits';
$: showRelatedServices = relatedServices.length > 0 && currentPath !== '/pack-walks';
$: relatedCards = [
@@ -217,7 +224,7 @@
</div>
<div class="service-benefit-shell">
<div bind:this={benefitScroller} class="service-benefit-grid">
<div bind:this={benefitScroller} class:service-benefit-grid-simple-swipe={useSimpleBenefitSwipe} class="service-benefit-grid">
{#each benefitCards as benefit, index}
<article
class:active={index === activeBenefitIndex}
@@ -236,43 +243,81 @@
{/each}
</div>
<div class="service-benefit-mobile-controls" aria-label="Benefit cards navigation">
<button
type="button"
class="service-benefit-mobile-button"
aria-label="Previous benefit"
disabled={activeBenefitIndex === 0}
on:click={() => scrollBenefits(-1)}
>
<Icon name="fas fa-chevron-left" />
</button>
<div class="service-benefit-mobile-pager" aria-label="Current benefit">
{#each benefitCards as _, index}
<button
type="button"
class:active={index === activeBenefitIndex}
class="service-benefit-mobile-dot"
aria-label={`Go to benefit ${index + 1}`}
aria-pressed={index === activeBenefitIndex}
on:click={() => scrollBenefitTo(index)}
></button>
{/each}
{#if !useSimpleBenefitSwipe}
<div class="service-benefit-mobile-controls" aria-label="Benefit cards navigation">
<button
type="button"
class="service-benefit-mobile-button"
aria-label="Previous benefit"
disabled={activeBenefitIndex === 0}
on:click={() => scrollBenefits(-1)}
>
<Icon name="fas fa-chevron-left" />
</button>
<div class="service-benefit-mobile-pager" aria-label="Current benefit">
{#each benefitCards as _, index}
<button
type="button"
class:active={index === activeBenefitIndex}
class="service-benefit-mobile-dot"
aria-label={`Go to benefit ${index + 1}`}
aria-pressed={index === activeBenefitIndex}
on:click={() => scrollBenefitTo(index)}
></button>
{/each}
</div>
<button
type="button"
class="service-benefit-mobile-button"
aria-label="Next benefit"
disabled={activeBenefitIndex === benefitCards.length - 1}
on:click={() => scrollBenefits(1)}
>
<Icon name="fas fa-chevron-right" />
</button>
</div>
<button
type="button"
class="service-benefit-mobile-button"
aria-label="Next benefit"
disabled={activeBenefitIndex === benefitCards.length - 1}
on:click={() => scrollBenefits(1)}
>
<Icon name="fas fa-chevron-right" />
</button>
</div>
{/if}
</div>
</div>
</section>
<section use:reveal class="service-pricing reveal-block">
{#if pageContent.hero.paragraphs?.length}
<section use:reveal class="service-intro reveal-block">
<div class="page-inner">
<article class="service-intro-card">
<div class="service-intro-header">
<div class="service-intro-badge">
<div class="service-intro-mark" aria-hidden="true">
<Icon name="fas fa-paw" />
</div>
<p class="service-intro-eyebrow">
{pageContent.hero.introEyebrow ?? 'The detail'}
</p>
</div>
<h2 class="service-intro-heading">
{pageContent.hero.introHeading ?? `More about ${pageContent.hero.eyebrow}`}
</h2>
</div>
<div class="service-intro-body">
<p class="service-intro-lead">{introLeadParagraph}</p>
{#if introBodyParagraphs.length}
<div class="service-intro-prose">
{#each introBodyParagraphs as paragraph}
<p>{paragraph}</p>
{/each}
</div>
{/if}
</div>
</article>
</div>
</section>
{/if}
<section
use:reveal
class:service-pricing-immediate-mobile={useSimpleBenefitSwipe}
class="service-pricing reveal-block"
>
<div class="page-inner">
<div class="service-section-heading">
<h2>{pageContent.pricing.title}</h2>
@@ -320,6 +365,20 @@
</div>
</section>
{#if pageContent.faq?.items?.length}
<section use:reveal class="service-faq reveal-block">
<div class="page-inner">
<div class="service-faq-card">
<FaqSection
title={pageContent.faq.title ?? 'Frequently asked questions'}
intro={pageContent.faq.intro}
faqs={pageContent.faq.items}
/>
</div>
</div>
</section>
{/if}
{#if showRelatedServices}
<section use:reveal class="service-related reveal-block" aria-label="Other services">
<div class="page-inner">
@@ -366,6 +425,216 @@
background: var(--off-white);
}
.service-intro {
padding: 18px 0 60px;
}
.service-intro-card {
display: grid;
grid-template-columns: minmax(240px, 0.9fr) minmax(0, 1.35fr);
gap: 44px;
max-width: 980px;
margin: 0 auto;
padding: 38px 40px 40px;
border-radius: 30px;
background:
radial-gradient(circle at top right, rgba(255, 209, 0, 0.14), transparent 30%),
linear-gradient(180deg, rgba(252, 250, 243, 0.98) 0%, rgba(244, 239, 228, 0.98) 100%);
box-shadow:
inset 0 0 0 1px rgba(33, 48, 33, 0.08),
0 24px 52px rgba(17, 20, 24, 0.07);
position: relative;
overflow: hidden;
}
.service-intro-card::before {
content: '';
position: absolute;
inset: 16px;
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 24px;
pointer-events: none;
}
.service-intro-card::after {
content: '';
position: absolute;
inset: 0 auto auto 0;
width: 160px;
height: 160px;
border-radius: 50%;
background: radial-gradient(circle, rgba(33, 48, 33, 0.08) 0%, transparent 72%);
transform: translate(-28%, -36%);
pointer-events: none;
}
.service-intro-header,
.service-intro-body {
position: relative;
z-index: 1;
}
.service-intro-header {
display: flex;
flex-direction: column;
gap: 18px;
justify-content: space-between;
padding-right: 12px;
}
.service-intro-badge {
display: inline-flex;
align-items: center;
gap: 12px;
width: fit-content;
padding: 10px 16px 10px 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.62);
box-shadow:
inset 0 0 0 1px rgba(33, 48, 33, 0.07),
0 12px 24px rgba(17, 20, 24, 0.04);
backdrop-filter: blur(10px);
}
.service-intro-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 14px;
background: linear-gradient(180deg, var(--gw-green) 0%, var(--green-mid) 100%);
color: var(--yellow);
font-size: 17px;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.06),
0 10px 22px rgba(33, 48, 33, 0.18);
}
.service-intro-eyebrow {
margin: 0;
color: var(--gw-green);
font-family: var(--font-head);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0.84;
}
.service-intro-heading {
margin: 0;
color: var(--gw-green);
font-size: clamp(26px, 2.7vw, 34px);
line-height: 1.08;
letter-spacing: -0.03em;
text-wrap: balance;
}
.service-intro-body {
display: grid;
gap: 18px;
align-content: start;
}
.service-intro-prose p {
margin: 0 0 15px;
color: #454a50;
font-size: 16px;
line-height: 1.75;
}
.service-intro-prose p:last-child {
margin-bottom: 0;
}
.service-intro-lead {
margin: 0;
padding: 18px 20px 18px 22px;
border-radius: 22px;
border-left: 4px solid var(--yellow-soft);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.72) 0%, rgba(250, 247, 239, 0.92) 100%);
box-shadow:
inset 0 0 0 1px rgba(33, 48, 33, 0.05),
0 12px 22px rgba(17, 20, 24, 0.04);
color: #1f242a;
font-size: 18px;
line-height: 1.7;
font-weight: 500;
}
@media (max-width: 768px) {
.service-intro {
padding: 6px 0 42px;
}
.service-intro-card {
grid-template-columns: 1fr;
gap: 20px;
padding: 26px 20px 24px;
border-radius: 24px;
}
.service-intro-card::before {
inset: 12px;
border-radius: 18px;
}
.service-intro-header {
gap: 16px;
padding-right: 0;
}
.service-intro-badge {
padding: 8px 14px 8px 8px;
gap: 10px;
}
.service-intro-mark {
width: 38px;
height: 38px;
border-radius: 12px;
font-size: 16px;
}
.service-intro-heading {
font-size: clamp(24px, 7vw, 30px);
line-height: 1.12;
}
.service-intro-prose p {
font-size: 15px;
}
.service-intro-lead {
padding: 16px 16px 16px 18px;
font-size: 16px;
line-height: 1.65;
}
}
.service-faq {
padding: 0 0 var(--space-section-featured-y);
}
.service-faq-card {
max-width: 880px;
margin: 0 auto;
padding: 38px 40px 32px;
border-radius: 30px;
background: #fff;
box-shadow:
inset 0 0 0 1px rgba(33, 48, 33, 0.06),
0 18px 40px rgba(17, 20, 24, 0.06);
}
@media (max-width: 768px) {
.service-faq-card {
padding: 26px 20px 22px;
border-radius: 24px;
}
}
.service-related {
padding: 0 0 var(--space-section-featured-y);
}
@@ -852,7 +1121,7 @@
}
.service-benefit-card-featured::after {
background: linear-gradient(90deg, #ffd54a 0%, rgba(242, 191, 47, 0.72) 100%);
display: none;
}
.service-benefit-icon {
@@ -862,35 +1131,31 @@
width: 54px;
height: 54px;
border-radius: 16px;
background: linear-gradient(180deg, rgba(255, 250, 236, 0.96) 0%, rgba(242, 191, 47, 0.18) 100%);
background: var(--gw-green);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.04),
0 8px 20px rgba(17, 20, 24, 0.08);
color: var(--gw-green);
font-size: 17px;
inset 0 0 0 1px rgba(255, 255, 255, 0.06),
0 10px 22px rgba(33, 48, 33, 0.22);
color: var(--yellow);
font-size: 19px;
margin-top: 4px;
margin-bottom: 14px;
position: relative;
z-index: 1;
}
.service-benefit-tint-1 .service-benefit-icon {
background: linear-gradient(180deg, rgba(255, 250, 236, 0.96) 0%, rgba(242, 191, 47, 0.16) 100%);
}
.service-benefit-tint-2 .service-benefit-icon {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(229, 214, 194, 0.42) 100%);
}
.service-benefit-tint-1 .service-benefit-icon,
.service-benefit-tint-2 .service-benefit-icon,
.service-benefit-tint-3 .service-benefit-icon {
background: linear-gradient(180deg, rgba(250, 252, 248, 0.96) 0%, rgba(33, 48, 33, 0.08) 100%);
background: var(--gw-green);
color: var(--yellow);
}
.service-benefit-card-featured .service-benefit-icon {
background: linear-gradient(180deg, rgba(255, 248, 214, 0.96) 0%, rgba(255, 209, 0, 0.34) 100%);
background: var(--yellow);
color: var(--gw-green);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
0 10px 24px rgba(0, 0, 0, 0.16);
inset 0 0 0 1px rgba(255, 255, 255, 0.4),
0 10px 24px rgba(0, 0, 0, 0.22);
}
.service-benefit-card h3,
@@ -1046,6 +1311,36 @@
margin-right: 2px;
}
.service-benefit-grid-simple-swipe {
display: flex;
gap: 12px;
margin: 0 -16px;
padding: 0 16px 8px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
touch-action: pan-x pinch-zoom;
}
.service-benefit-grid-simple-swipe::-webkit-scrollbar {
display: none;
}
.service-benefit-grid-simple-swipe .service-benefit-card {
flex: 0 0 78%;
min-height: clamp(230px, 42svh, 320px);
min-width: 0;
scroll-snap-align: start;
scroll-snap-stop: always;
}
:global(.reveal-ready.reveal-block).service-pricing-immediate-mobile {
opacity: 1;
transform: none;
transition: none;
}
.service-highlight-layout {
gap: 24px;
@@ -1197,15 +1492,35 @@
}
.service-highlight-collage {
grid-template-columns: 1fr;
display: flex;
gap: 12px;
max-width: 420px;
max-width: none;
margin: 0 -16px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
padding: 0 16px 8px;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
touch-action: pan-x pinch-zoom;
}
.service-highlight-collage::-webkit-scrollbar {
display: none;
}
.service-collage-card {
flex: 0 0 78%;
min-height: 168px;
min-width: 0;
border-radius: 22px;
scroll-snap-align: start;
scroll-snap-stop: always;
}
.service-collage-card-1,
.service-collage-card-2,
.service-collage-card-3 {
min-height: 220px;
transform: none;
}
}
+83 -166
View File
@@ -6,7 +6,7 @@
export let services: IconCard[];
export let heading = 'Choose the walk style that suits your dog best.';
export let intro =
'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.';
'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 sharedPromises = [
'Familiar walkers',
@@ -22,7 +22,6 @@
{
eyebrow: string;
featured?: boolean;
featuredLabel?: string;
imageUrl: string;
imageAlt: string;
lead: string;
@@ -32,22 +31,21 @@
'Tiny Gang Pack Walks': {
eyebrow: 'Good Walk Signature',
featured: true,
featuredLabel: 'Most loved',
imageUrl: '/images/auckland-pack-walk-small-dogs-group.jpg',
imageUrl: '/images/goodwalk-tiny-gang-pack-walk-small-dogs-auckland.webp',
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',
imageUrl: '/images/goodwalk-brown-curly-dog-one-on-one-walk-auckland.webp',
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',
imageUrl: '/images/goodwalk-puppy-visit-cavalier-king-charles-spaniel-auckland.webp',
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']
@@ -91,9 +89,6 @@
{#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>
<div class="service-card-body">
@@ -116,7 +111,7 @@
{/if}
<span class="service-card-cta">
View service page
More info
<Icon name="fas fa-arrow-right" className="service-card-cta-arrow" />
</span>
</div>
@@ -127,112 +122,52 @@
</section>
<style>
.services-inner {
max-width: min(1180px, calc(var(--max-w) - 40px));
margin: 0 auto;
}
.section-header {
grid-template-columns: minmax(0, 1fr) minmax(20rem, 0.85fr);
align-items: start;
column-gap: clamp(32px, 5vw, 72px);
row-gap: 14px;
text-align: left;
}
.section-header .section-heading {
max-width: 22ch;
text-align: left;
}
.section-header .section-intro {
margin: 0;
padding-top: 14px;
max-width: 44ch;
justify-self: end;
text-align: left;
line-height: 1.72;
color: var(--text-heading-soft, var(--text-muted));
}
/* ── 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;
max-width: 34ch;
}
/* ── Service cards ── */
.services-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 22px;
margin-top: 30px;
align-items: stretch;
gap: 24px;
margin-top: 40px;
}
.service-card {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
padding: 0;
text-align: left;
@@ -248,6 +183,8 @@
border-color 0.28s ease;
}
/* Featured emphasis lives in the chrome (border + glow + emblem shine),
not the column span — keeps all three cards visually balanced. */
.service-card-featured {
border-color: rgba(242, 191, 47, 0.45);
box-shadow:
@@ -267,7 +204,7 @@
transform: scale(1.06);
}
.service-card:hover .service-card-cta-arrow {
.service-card:hover :global(.service-card-cta-arrow) {
transform: translateX(4px);
}
@@ -297,21 +234,6 @@
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;
@@ -389,11 +311,15 @@
line-height: 1.6;
}
/* Push cues+CTA cluster to the bottom so it lines up across all cards
regardless of how long each card's lead paragraph runs. */
.service-card-cues {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 16px;
margin-top: auto;
padding-top: 18px;
padding-bottom: 18px;
}
.service-card-cue {
@@ -414,7 +340,6 @@
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);
@@ -423,64 +348,56 @@
font-weight: 700;
}
.service-card-cta-arrow {
:global(.service-card-cta-arrow) {
font-size: 11px;
transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
/* ── Mobile ── */
@media (max-width: 1024px) {
.section-header {
grid-template-columns: 1fr;
text-align: center;
}
.section-header .section-heading,
.section-header .section-intro {
max-width: 32rem;
justify-self: center;
text-align: center;
}
.section-header .section-intro {
padding-top: 0;
}
.services-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* Featured card takes the full width on tablet so it sits on its own
row above the other two — keeps the emphasis without the asymmetric
desktop grid. */
.service-card-featured {
grid-column: 1 / -1;
}
}
@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-featured {
grid-column: auto;
}
.service-card-body {
padding: 40px 22px 24px;
}
@@ -524,10 +441,10 @@
/* ── Reveal ── */
:global(.reveal-ready.reveal-block) {
opacity: 0;
transform: translate3d(0, var(--reveal-distance, 24px), 0);
transform: translate3d(0, var(--reveal-distance, 16px), 0);
transition:
opacity 0.55s ease,
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
opacity 0.3s ease,
transform 0.45s cubic-bezier(0.2, 0.8, 0.2, 1);
transition-delay: var(--reveal-delay, 0ms);
}
+36 -27
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import confetti from 'canvas-confetti';
import ModalShell from '$lib/components/ModalShell.svelte';
export let firstName: string;
@@ -13,35 +12,45 @@
$: isGeneralEnquiry = enquiryType === 'general';
onMount(() => {
const duration = 3200;
const end = Date.now() + duration;
let cancelled = false;
const frame = () => {
confetti({
particleCount: 3,
angle: 60,
spread: 65,
origin: { x: 0, y: 0.75 },
colors: ['#FFD100', gwGreenHex, '#ffffff', '#7aaa7a', '#ffeaa0'],
gravity: 0.9,
scalar: 1.1,
});
confetti({
particleCount: 3,
angle: 120,
spread: 65,
origin: { x: 1, y: 0.75 },
colors: ['#FFD100', gwGreenHex, '#ffffff', '#7aaa7a', '#ffeaa0'],
gravity: 0.9,
scalar: 1.1,
});
void import('canvas-confetti').then(({ default: confetti }) => {
if (cancelled) return;
if (Date.now() < end) {
requestAnimationFrame(frame);
}
const duration = 3200;
const end = Date.now() + duration;
const frame = () => {
confetti({
particleCount: 3,
angle: 60,
spread: 65,
origin: { x: 0, y: 0.75 },
colors: ['#FFD100', gwGreenHex, '#ffffff', '#7aaa7a', '#ffeaa0'],
gravity: 0.9,
scalar: 1.1
});
confetti({
particleCount: 3,
angle: 120,
spread: 65,
origin: { x: 1, y: 0.75 },
colors: ['#FFD100', gwGreenHex, '#ffffff', '#7aaa7a', '#ffeaa0'],
gravity: 0.9,
scalar: 1.1
});
if (!cancelled && Date.now() < end) {
requestAnimationFrame(frame);
}
};
frame();
});
return () => {
cancelled = true;
};
frame();
});
</script>
+267 -524
View File
@@ -7,60 +7,36 @@
export let content: SiteSharedContent;
$: testimonials = content.testimonials.filter((testimonial) => testimonial.imageUrl);
$: featuredTestimonial = testimonials[0];
$: supportingTestimonials = testimonials.slice(1);
$: testimonialCards = testimonials.map((testimonial, index) => ({
$: testimonials = content.testimonials;
$: testimonialCards = testimonials.map((testimonial) => ({
...testimonial,
enhanced: getEnhancedImage(testimonial.imageUrl),
accentClass: `testimonial-card-accent-${(index % 4) + 1}`
enhanced: testimonial.imageUrl ? getEnhancedImage(testimonial.imageUrl) : undefined
}));
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 dogNameFromDetail(detail: string) {
const match = detail.match(/^([^']+)/);
return match ? match[1].trim() : '';
}
function reviewerRole(testimonial: TestimonialContent) {
const detail = testimonial.detail.trim();
const reviewer = testimonial.reviewer.trim();
const detailHasRelation = /(mum|dad|father|owner)/i.test(detail);
const reviewerHasRelation = /(mum|dad|father|owner)/i.test(reviewer);
if (detailHasRelation) return detail;
if (reviewerHasRelation) return reviewer;
return 'Goodwalk client';
}
function authorLine(testimonial: TestimonialContent) {
const reviewer = testimonial.reviewer.trim();
const role = reviewerRole(testimonial);
return role === reviewer ? reviewer : `${reviewer} - ${role}`;
}
function reviewAlt(testimonial: TestimonialContent) {
const detailLead = testimonial.detail.match(/^([^']+)/)?.[1]?.trim();
const detailLead = dogNameFromDetail(testimonial.detail);
return detailLead
? `${detailLead}, a Goodwalk dog in Auckland`
: `${testimonial.reviewer}'s dog with Goodwalk in Auckland`;
@@ -71,168 +47,79 @@
<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."
title="Client Testimonials"
subtitle="Read why clients say their dogs love Aless, the Tiny Gang, and getting out with their mates."
>
<a
class="testimonials-page-trust"
href="https://g.page/r/CUsvrWPhkYrAEB0/"
target="_blank"
rel="noopener"
aria-label="Read our Google reviews"
class="btn btn-yellow btn-hide-arrow-mobile testimonials-page-header-cta"
href="#newlead"
aria-label="Book a Meet and Greet"
>
<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>
<span>Book a Meet &amp; Greet</span>
<Icon name="fas fa-arrow-right" />
</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}`}>
<article class="testimonials-page-card">
<div class="testimonials-page-card-media">
{#if testimonial.enhanced}
{#if testimonial.imageUrl && testimonial.enhanced}
<enhanced:img
src={testimonial.enhanced}
alt={reviewAlt(testimonial)}
loading="lazy"
decoding="async"
/>
{:else}
{:else if testimonial.imageUrl}
<img
src={testimonial.imageUrl}
alt={reviewAlt(testimonial)}
loading="lazy"
decoding="async"
/>
{:else}
<div class="testimonials-page-card-fallback" aria-hidden="true">
<span class="testimonials-page-card-fallback-mark">
<span class="testimonials-page-card-fallback-mark-ring">
<Icon name="fas fa-paw" />
</span>
</span>
</div>
{/if}
<div class="testimonials-page-card-meta">
<span class="testimonials-page-card-dog">
{dogNameFromDetail(testimonial.detail) || testimonial.reviewer}
</span>
</div>
</div>
<div class="testimonials-page-card-copy">
<span class="testimonials-page-quote-mark">"</span>
<div class="testimonials-page-card-top">
<span class="testimonials-page-card-quote" aria-hidden="true"></span>
<div class="testimonials-page-card-review-meta">
{#if testimonial.type === 'Google'}
<img
class="testimonials-page-card-source-icon"
src="/images/google-g-logo.svg"
alt="Google review"
width="14"
height="15"
/>
{/if}
<span class="testimonials-page-card-stars" aria-hidden="true">
{#each Array(5) as _}
<Icon name="fas fa-star" />
{/each}
</span>
</div>
</div>
<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>
<span class="testimonials-page-author-name">{authorLine(testimonial)}</span>
</div>
</div>
</article>
@@ -265,269 +152,57 @@
<style>
.testimonials-page {
background: #fff;
background: var(--off-white);
}
.testimonials-page-trust {
.testimonials-page-header-cta {
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;
padding: 28px 0 72px;
}
.testimonials-page-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
gap: 22px;
align-items: stretch;
max-width: 1240px;
margin: 0 auto;
}
.testimonials-page-card {
position: relative;
display: grid;
grid-template-rows: auto 1fr;
height: 100%;
overflow: hidden;
padding: 12px;
border: 1px solid rgba(33, 48, 33, 0.09);
border-radius: 28px;
background: #fff;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.78),
0 16px 34px rgba(33, 48, 33, 0.08);
}
.testimonials-page-card-media {
position: relative;
aspect-ratio: 4 / 3;
background: #ede4d2;
overflow: hidden;
border-radius: 22px;
background: rgba(33, 48, 33, 0.12);
}
.testimonials-page-card-media img {
@@ -537,74 +212,160 @@
object-fit: cover;
}
.testimonials-page-card-copy {
padding: 28px 28px 30px;
.testimonials-page-card-fallback {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 24px;
text-align: center;
background: var(--gw-green);
overflow: hidden;
}
.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-fallback::before,
.testimonials-page-card-fallback::after {
content: '';
position: absolute;
border-radius: 999px;
background: rgba(255, 209, 0, 0.08);
pointer-events: none;
}
.testimonials-page-card-fallback::before {
top: 18px;
right: 18px;
width: 72px;
height: 72px;
}
.testimonials-page-card-fallback::after {
bottom: -20px;
left: -10px;
width: 120px;
height: 120px;
}
.testimonials-page-card-fallback-mark {
position: relative;
z-index: 1;
}
.testimonials-page-card-fallback-mark {
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.testimonials-page-card-fallback-mark-ring {
display: inline-flex;
align-items: center;
justify-content: center;
width: 72px;
height: 72px;
border-radius: 50%;
background: linear-gradient(180deg, rgba(255, 244, 204, 0.96), rgba(255, 209, 0, 0.82));
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 12px 28px rgba(17, 20, 24, 0.18);
}
.testimonials-page-card-fallback-mark-ring :global(.icon) {
font-size: 28px;
color: var(--gw-green);
}
.testimonials-page-card-meta {
position: absolute;
right: 14px;
bottom: 14px;
left: 14px;
display: flex;
align-items: flex-end;
justify-content: flex-start;
padding: 14px 16px;
border-radius: 18px;
background: linear-gradient(180deg, rgba(18, 26, 18, 0.1) 0%, rgba(18, 26, 18, 0.78) 100%);
}
.testimonials-page-card-dog {
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
font-family: var(--font-head);
font-size: 22px;
font-weight: 700;
line-height: 1;
letter-spacing: -0.03em;
}
.testimonials-page-card-copy {
display: grid;
align-content: start;
grid-template-rows: auto 1fr auto;
gap: 14px;
padding: 18px 8px 10px;
}
.testimonials-page-card-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.testimonials-page-card-quote {
color: rgba(33, 48, 33, 0.22);
font-family: Georgia, 'Times New Roman', serif;
font-size: 40px;
line-height: 0.8;
}
.testimonials-page-card-review-meta {
display: inline-flex;
align-items: center;
gap: 8px;
}
.testimonials-page-card-source-icon {
flex: 0 0 auto;
}
.testimonials-page-card-stars {
display: inline-flex;
gap: 4px;
color: #f0b72f;
font-size: 13px;
}
.testimonials-page-card blockquote {
margin: 0;
color: #2e3031;
font-size: 16px;
line-height: 1.68;
align-self: start;
color: #243024;
font-size: 17px;
line-height: 1.7;
}
.testimonials-page-author {
display: flex;
align-items: center;
gap: 10px;
margin-top: 18px;
margin-top: auto;
}
.testimonials-page-author-name {
color: #102010;
color: var(--gw-green);
font-family: var(--font-head);
font-size: 14px;
font-size: 15px;
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;
margin-top: 36px;
}
.testimonials-page-google-cta {
@@ -614,11 +375,11 @@
min-height: 50px;
padding: 0 18px;
border-radius: 999px;
background: #fff;
background: rgba(255, 255, 255, 0.78);
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);
0 16px 30px rgba(67, 49, 21, 0.08);
color: #1d281e;
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
@@ -626,95 +387,77 @@
}
@media (max-width: 1024px) {
.testimonials-page-intro-grid,
.testimonials-page-featured-grid,
.testimonials-page-highlights-grid,
.testimonials-page-grid,
.testimonials-page-spotlight {
.testimonials-page-grid {
grid-template-columns: 1fr;
}
}
.testimonials-page-highlights-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
@media (min-width: 1400px) {
.testimonials-page-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
max-width: 1500px;
}
}
.testimonials-page-spotlight-media {
aspect-ratio: 4 / 3;
@media (min-width: 1800px) {
.testimonials-page-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
max-width: 1760px;
gap: 20px;
}
}
@media (max-width: 768px) {
.testimonials-page-intro,
.testimonials-page-featured-section,
.testimonials-page-highlights-section,
.testimonials-page-grid-section {
padding: 52px 0;
padding: 20px 0 56px;
}
.testimonials-page-trust {
.testimonials-page-header-cta {
gap: 10px;
padding: 10px 14px;
font-size: 13px;
}
.testimonials-page-copy {
padding: 24px 20px 22px;
.testimonials-page-header-cta :global(.icon) {
display: none;
}
.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;
gap: 14px;
padding: 18px 6px 8px;
}
.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-card {
border-radius: 26px;
padding: 12px;
}
.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-card-media {
border-radius: 18px;
}
.testimonials-page-card-meta {
right: 12px;
bottom: 12px;
left: 12px;
padding: 12px 14px;
border-radius: 16px;
}
.testimonials-page-card-dog {
font-size: 20px;
}
.testimonials-page-card blockquote {
font-size: 16px;
line-height: 1.64;
}
.testimonials-page-author {
align-items: flex-start;
flex-direction: column;
gap: 4px;
}
.testimonials-page-author-detail::before {
content: '';
margin-right: 0;
}
}
</style>
+135 -111
View File
@@ -20,30 +20,38 @@
Kate: {
reviewer: 'Kate',
detail: "Archie's mum",
type: 'Google',
showInSlider: true,
quote:
'Love Aless! She is so amazing with my slightly hyper and anxious dog. She is great with communication if anything on either of our ends need to change. Archie love his walks, and I love the photos she posts of him.',
imageUrl: '/images/archie-auckland-dog-walking-review.jpg'
imageUrl: '/images/archie-goodwalk-dog-walking-review-auckland.webp'
},
Estelle: {
reviewer: 'Estelle',
detail: "Monty's mum",
type: 'Google',
showInSlider: true,
quote:
'GoodWalk was the best dog walking service for my little pooch ! Aless was very helpful - basically doubled as a second mum to Monty. She always provided feedback on his outings and assisted where possible with any additional training that she felt he could work on and made recommendations where necessary which i feel is what every dog mum wants and needs!',
imageUrl: '/images/monty-auckland-dog-walking-review.jpg'
imageUrl: '/images/monty-goodwalk-dog-walking-review-auckland.webp'
},
Ross: {
reviewer: 'Ross',
detail: "Otis's dad",
type: 'Google',
showInSlider: true,
quote:
'Truly the best dog walker in Auckland! I feel so lucky to have found Aless and my little terrier Otis absolutely adores her. He enjoys his regular weekly walks and always comes back happy & tired. Love the updates on social media so I can see how my dog is enjoying his day! Aless makes logistics so easy too. Highly highly recommend, theres a reason she has 5 stars!',
imageUrl: '/images/otis-auckland-dog-walking-review.jpg'
imageUrl: '/images/otis-goodwalk-dog-walking-review-auckland.webp'
},
Nina: {
reviewer: 'Nina',
detail: "Wallace's mum",
type: 'Google',
showInSlider: true,
quote:
'Alessandra has been walking and spending time with my pup since she was 10 weeks old, coming over and doing puppy visits through to transitioning her to pack walks with her little doggo friends. I know Alassandra loves and cares for my dog as much as I do and my dog has a great time! Cant recommend enough',
imageUrl: '/images/wallace-auckland-dog-walking-review.jpg'
imageUrl: '/images/wallace-goodwalk-dog-walking-review-auckland.webp'
}
};
@@ -57,6 +65,7 @@
$: slides = testimonials
.map((testimonial) => wordpressTestimonials[testimonial.reviewer] ?? testimonial)
.filter((testimonial) => testimonial.showInSlider)
.filter((testimonial): testimonial is TestimonialSlide => Boolean(testimonial.imageUrl));
$: if (activeIndex >= slides.length) {
@@ -158,7 +167,7 @@
syncMobileStage('auto');
const interval = window.setInterval(() => {
if (!paused && !prefersReducedMotion && inView && slides.length > 1) {
if (!isMobileViewport() && !paused && !prefersReducedMotion && inView && slides.length > 1) {
showNext();
}
}, 9000);
@@ -174,10 +183,33 @@
<section id="testimonials" use:reveal={{ delay: 40 }} class="reveal-block">
<div class="testimonials-inner">
<span class="testimonials-eyebrow">{eyebrow}</span>
<h2 class="section-heading">{heading}</h2>
<div class="testimonials-intro">
<p>{blurb}</p>
<div class="testimonials-header">
<div class="testimonials-header-main">
<span class="testimonials-eyebrow">
<Icon name="fas fa-star" className="testimonials-eyebrow-star" />
{eyebrow}
</span>
<h2 class="section-heading">{heading}</h2>
</div>
<div class="testimonials-header-side">
<div class="testimonials-intro">
<p>{blurb}</p>
</div>
<div class="testimonials-cta-row">
<a href={testimonialsHref} class="btn btn-yellow 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="btn btn-outline-green 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>
</div>
{#if slides.length}
@@ -257,17 +289,6 @@
<Icon name="fas fa-chevron-right" />
</button>
</div>
<a class="testimonial-google" href={googleReviewsHref} target="_blank" rel="noopener">
<img
class="testimonial-google-logo"
src="/images/google-g-logo.svg"
alt=""
width="18"
height="19"
/>
<span>30+ five-star Google reviews</span>
</a>
</div>
</article>
{/each}
@@ -283,43 +304,71 @@
</button>
</div>
{/if}
<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>
<style>
#testimonials {
content-visibility: auto;
contain-intrinsic-size: 980px;
}
.testimonials-inner {
max-width: min(1220px, calc(var(--max-w) - 24px));
margin: 0 auto;
padding: 0 var(--space-container-x);
}
.testimonials-header {
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(20rem, 0.85fr);
align-items: end;
gap: clamp(24px, 4vw, 56px);
}
.testimonials-header-main,
.testimonials-header-side {
min-width: 0;
}
.testimonials-header-main {
text-align: center;
}
.testimonials-eyebrow {
display: block;
display: inline-flex;
align-items: center;
gap: 8px;
width: fit-content;
margin: 0 auto 10px;
padding: 7px 12px;
margin: 0 auto 14px;
padding: 7px 14px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.08);
background: var(--yellow);
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);
box-shadow: inset 0 0 0 1px rgba(33, 48, 33, 0.18);
}
.testimonials-eyebrow :global(.testimonials-eyebrow-star) {
color: var(--gw-green);
font-size: 11px;
line-height: 1;
}
.testimonials-header .section-heading {
text-align: center;
max-width: 11ch;
margin-left: auto;
margin-right: auto;
}
.testimonials-intro {
max-width: 760px;
margin: 18px auto 0;
text-align: center;
max-width: 34ch;
margin: 0;
text-align: left;
}
.testimonials-intro p {
@@ -330,48 +379,35 @@
}
.testimonials-cta-row {
display: grid;
grid-template-columns: repeat(2, max-content);
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
max-width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
gap: 12px;
margin: 22px auto 0;
margin-top: 20px;
}
.testimonials-cta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
min-height: 42px;
padding: 9px 16px;
border-radius: 999px;
flex-shrink: 0;
text-decoration: none;
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;
}
.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-secondary:hover {
color: var(--gw-green);
}
.testimonials-cta-logo {
flex: 0 0 auto;
}
@@ -381,16 +417,13 @@
}
@media (hover: 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);
color: var(--gw-green);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 10px 22px rgba(17, 20, 24, 0.08);
@@ -400,10 +433,38 @@
.testimonials-carousel {
position: relative;
margin-top: 48px;
margin-top: 40px;
padding: 0 38px;
}
@media (max-width: 1024px) {
.testimonials-header {
grid-template-columns: 1fr;
gap: 20px;
}
.testimonials-header-main {
text-align: center;
}
.testimonials-eyebrow {
margin-left: auto;
margin-right: auto;
}
.testimonials-header .section-heading,
.testimonials-intro {
max-width: 34rem;
margin-left: auto;
margin-right: auto;
text-align: center;
}
.testimonials-cta-row {
justify-content: center;
}
}
@media (max-width: 768px) {
.testimonials-eyebrow {
margin-bottom: 8px;
@@ -422,7 +483,7 @@
.testimonials-cta-row {
gap: 8px;
margin-top: 16px;
margin-top: 14px;
}
.testimonials-cta {
@@ -463,10 +524,10 @@
:global(.reveal-ready.reveal-block) {
opacity: 0;
transform: translate3d(0, var(--reveal-distance, 24px), 0);
transform: translate3d(0, var(--reveal-distance, 16px), 0);
transition:
opacity 0.55s ease,
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
opacity 0.3s ease,
transform 0.45s cubic-bezier(0.2, 0.8, 0.2, 1);
transition-delay: var(--reveal-delay, 0ms);
}
@@ -581,33 +642,6 @@
background: #e7e7e7;
}
.testimonial-google {
display: inline-flex;
align-items: center;
gap: 12px;
margin-top: 28px;
padding: 10px 20px;
border-radius: 999px;
background: #f8f8f8;
color: #0a304e;
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0.01em;
box-shadow: 0 0 0 1px rgba(10, 48, 78, 0.06);
}
.testimonial-google-logo {
width: 18px;
height: 19px;
flex: 0 0 auto;
}
.testimonial-google:hover {
background: #efe6d5;
}
.testimonial-mobile-controls {
display: none;
}
@@ -674,9 +708,10 @@
padding-bottom: 0;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x proximity;
scroll-snap-type: x mandatory;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
touch-action: pan-x pinch-zoom;
}
.testimonial-stage::-webkit-scrollbar {
@@ -756,17 +791,6 @@
transform: scale(0.95);
}
.testimonial-google {
margin-top: 16px;
font-size: 16px;
gap: 10px;
padding: 10px 14px;
}
.testimonial-google :global(.icon) {
font-size: 20px;
}
.testimonial-arrow-left,
.testimonial-arrow-right {
display: none;
@@ -92,13 +92,17 @@ describe('TestimonialsSection', () => {
{
reviewer: 'Casey',
detail: "Poppy's mum",
type: 'Client',
quote: 'Thoughtful updates and a very happy dog after every walk.',
imageUrl: '/images/custom-casey-review.png'
imageUrl: '/images/custom-casey-review.webp',
showInSlider: true
},
{
reviewer: 'Jordan',
detail: "Scout's dad",
quote: 'Should be hidden because there is no image.'
type: 'Client',
quote: 'Should be hidden because there is no image.',
showInSlider: true
}
];
+102 -71
View File
@@ -33,34 +33,34 @@
];
const clientPhotos = [
{
imageUrl: '/images/dog.png',
imageUrl: '/images/goodwalk-client-dogs-mt-cecila-auckland.webp',
alt: 'Two happy Goodwalk client dogs out on a walk in Auckland',
name: 'Happy clients',
detail: 'out with Goodwalk'
name: 'Happy clients at Mt Cecila',
detail: ''
},
{
imageUrl: '/images/pack-walk-tiny-gang.png',
imageUrl: '/images/goodwalk-tiny-gang-pack-walk-auckland.webp',
alt: 'Goodwalk Tiny Gang dogs together on a walk in Auckland',
name: 'Tiny Gang',
detail: 'small group pack walks'
name: 'The Tiny Gang on a pack walk',
detail: ''
},
{
imageUrl: '/images/tiny-gang-mt-albert-park.png',
imageUrl: '/images/goodwalk-tiny-gang-mt-albert-park-auckland.webp',
alt: 'Goodwalk dogs together at Mt Albert Park in Auckland',
name: 'Mt Albert Park',
detail: 'Goodwalk regulars'
name: 'Distinguished crew at Mt Albert park',
detail: ''
},
{
imageUrl: '/images/otis-auckland-dog-walking-review.jpg',
imageUrl: '/images/goodwalk-dogs-group-outing-auckland.webp',
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'
imageUrl: '/images/goodwalk-tiny-gang-finishing-walk-suv-auckland.webp',
alt: 'Tiny Gang Pack finishing up after a walk',
name: 'Tiny Gang heading home',
detail: ''
}
];
@@ -114,9 +114,11 @@
decoding="async"
/>
{/if}
<figcaption class="values-photo-caption">
<figcaption class:values-photo-caption-solo={!photo.detail} class="values-photo-caption">
<span class="values-photo-name">{photo.name}</span>
<span class="values-photo-detail">{photo.detail}</span>
{#if photo.detail}
<span class="values-photo-detail">{photo.detail}</span>
{/if}
</figcaption>
</figure>
{/each}
@@ -184,20 +186,20 @@
.values-inner {
max-width: var(--max-w);
margin: 0 auto;
padding: 0 50px;
padding: 0 var(--space-container-x);
}
.values-inner .section-heading {
color: #000;
color: var(--text-heading);
text-align: center;
}
.values-eyebrow {
width: fit-content;
padding: 6px 12px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.07);
border-radius: var(--radius-pill);
background: var(--surface-brand-soft);
box-shadow: var(--shadow-inset-strong);
}
.values-intro {
@@ -220,11 +222,11 @@
position: relative;
overflow: hidden;
min-height: 0;
border-radius: 28px;
background: #ede4d2;
border-radius: var(--radius-xl);
background: var(--beige);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 18px 34px rgba(17, 20, 24, 0.08);
var(--shadow-inset-soft),
var(--shadow-card);
}
.values-photo-card-featured {
@@ -255,11 +257,16 @@
justify-content: space-between;
gap: 10px;
padding: 12px 14px;
border-radius: 18px;
border-radius: var(--radius-md);
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);
var(--shadow-inset-soft),
var(--shadow-card);
}
.values-photo-caption-solo {
justify-content: center;
text-align: center;
}
.values-photo-name,
@@ -268,14 +275,14 @@
}
.values-photo-name {
color: #102010;
color: var(--text-heading);
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
}
.values-photo-detail {
color: #5a605f;
color: var(--text-muted);
font-size: 13px;
line-height: 1.3;
text-align: right;
@@ -288,11 +295,11 @@
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;
background: rgba(var(--ink-rgb), 0.06);
border: 1px solid rgba(var(--ink-rgb), 0.06);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.06);
box-shadow: var(--shadow-panel-strong);
}
/* ── Before / after contrast ── */
@@ -304,8 +311,8 @@
.values-contrast-cell {
display: flex;
flex-direction: column;
padding: 38px 36px;
background: #fff;
padding: var(--space-8) var(--space-8);
background: var(--surface-panel);
}
.values-contrast-cell-good {
@@ -324,9 +331,9 @@
display: inline-flex;
align-items: center;
padding: 5px 11px;
border-radius: 999px;
background: rgba(17, 20, 24, 0.05);
color: var(--gray);
border-radius: var(--radius-pill);
background: rgba(var(--ink-rgb), 0.05);
color: var(--text-muted);
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
@@ -336,14 +343,14 @@
.values-contrast-label-good {
background: var(--yellow);
color: #000;
color: var(--gw-green);
}
.values-contrast-num {
font-family: var(--font-head);
font-size: 12px;
font-weight: 700;
color: rgba(17, 20, 24, 0.22);
color: rgba(var(--ink-rgb), 0.22);
letter-spacing: 0.04em;
}
@@ -354,12 +361,12 @@
font-weight: 700;
line-height: 1.22;
letter-spacing: -0.02em;
color: #0d1a0d;
color: var(--text-heading);
}
.values-contrast-body {
margin: 0 0 20px;
color: #4c5056;
color: var(--text-muted);
font-size: 15px;
line-height: 1.65;
}
@@ -377,13 +384,13 @@
gap: 12px;
align-items: start;
padding: 13px 0;
color: #3f4348;
color: var(--text-muted);
font-size: 15px;
line-height: 1.5;
}
.values-contrast-list li + li {
border-top: 1px solid rgba(17, 20, 24, 0.08);
border-top: 1px solid var(--border-muted);
}
.values-contrast-bullet {
@@ -397,7 +404,7 @@
.values-contrast-list :global(.values-contrast-glyph) {
font-size: 10px;
color: var(--gray);
color: var(--text-muted);
}
.values-contrast-cell-good .values-contrast-bullet {
@@ -413,10 +420,10 @@
.values-contrast-footer {
margin: auto 0 0;
padding-top: 18px;
border-top: 1px solid rgba(17, 20, 24, 0.08);
border-top: 1px solid var(--border-muted);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 13px;
font-size: 15px;
font-weight: 700;
line-height: 1.5;
}
@@ -428,23 +435,23 @@
/* Light text for the gw-green "With Goodwalk" cell */
.values-contrast-cell-good h3 {
color: #fff;
color: var(--text-inverse);
}
.values-contrast-cell-good .values-contrast-num {
color: rgba(255, 255, 255, 0.4);
color: rgba(var(--white-rgb), 0.4);
}
.values-contrast-cell-good .values-contrast-body {
color: rgba(255, 255, 255, 0.82);
color: var(--text-inverse-muted);
}
.values-contrast-cell-good .values-contrast-list li {
color: rgba(255, 255, 255, 0.9);
color: rgba(var(--white-rgb), 0.9);
}
.values-contrast-cell-good .values-contrast-list li + li {
border-top-color: rgba(255, 255, 255, 0.14);
border-top-color: var(--border-inverse-strong);
}
/* ── Values points header ── */
@@ -461,13 +468,13 @@
font-weight: 700;
line-height: 1.14;
letter-spacing: -0.03em;
color: #000;
color: var(--text-heading);
}
.values-points-intro {
max-width: 560px;
margin: 14px auto 0;
color: #4c5056;
color: var(--text-muted);
font-size: var(--body-copy-size);
line-height: 1.65;
}
@@ -482,14 +489,14 @@
.values-point {
display: flex;
flex-direction: column;
padding: 32px 30px;
background: #fff;
padding: var(--space-7) var(--space-7);
background: var(--surface-panel);
transition: background 0.18s ease;
}
@media (hover: hover) {
.values-point:hover {
background: #fcfbf6;
background: var(--surface-panel-warm);
}
}
@@ -500,9 +507,9 @@
width: 40px;
height: 40px;
margin-bottom: 18px;
border-radius: 11px;
border-radius: var(--radius-sm);
background: var(--gw-green);
box-shadow: 0 6px 16px rgba(33, 48, 33, 0.18);
box-shadow: var(--shadow-badge);
}
.values-point-icon :global(.values-point-glyph) {
@@ -516,12 +523,12 @@
font-size: 17px;
font-weight: 700;
line-height: 1.25;
color: #0d1a0d;
color: var(--text-heading);
}
.values-point p {
margin: 0;
color: #4c5056;
color: var(--text-muted);
font-size: 14px;
line-height: 1.6;
}
@@ -545,7 +552,8 @@
.values-eyebrow {
padding: 6px 10px;
font-size: 11px;
font-size: 12px;
letter-spacing: 0.06em;
}
.values-photo-grid {
@@ -555,31 +563,54 @@
margin-top: 22px;
}
.values-photo-card,
.values-photo-card-featured {
.values-photo-card {
grid-row: auto;
min-height: 178px;
border-radius: 22px;
border-radius: var(--radius-lg);
}
.values-photo-card-featured {
grid-column: 1 / -1;
grid-row: auto;
min-height: 240px;
border-radius: var(--radius-lg);
}
.values-photo-caption {
left: 10px;
right: 10px;
bottom: 10px;
display: grid;
justify-content: start;
align-items: start;
gap: 3px;
padding: 10px 11px;
border-radius: 16px;
border-radius: var(--radius-md);
}
.values-photo-caption-solo {
justify-content: center;
text-align: center;
}
.values-photo-name {
font-size: 12px;
line-height: 1.15;
}
.values-photo-detail {
font-size: 11px;
line-height: 1.25;
line-clamp: 2;
text-align: left;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.values-bento {
border-radius: 16px;
border-radius: var(--radius-md);
}
.values-contrast {
@@ -629,10 +660,10 @@
/* ── Reveal ── */
:global(.reveal-ready.reveal-block) {
opacity: 0;
transform: translate3d(0, var(--reveal-distance, 24px), 0);
transform: translate3d(0, var(--reveal-distance, 16px), 0);
transition:
opacity 0.55s ease,
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
opacity var(--motion-reveal-opacity, 0.3s ease),
transform var(--motion-reveal-transform, 0.45s cubic-bezier(0.2, 0.8, 0.2, 1));
transition-delay: var(--reveal-delay, 0ms);
}