v4.0.6 - Simplicity focus

This commit is contained in:
2026-06-22 22:56:00 +12:00
parent 0e88e7bdac
commit bfd96c677f
9 changed files with 1247 additions and 42 deletions
+520
View File
@@ -0,0 +1,520 @@
<script lang="ts">
/*
* Clarity homepage design (the live homepage as of the swap).
*
* Purpose-built green hero that says the specialism in the first second
* (small & medium dogs), a founder philosophy strip, then offer-first order:
* Services -> How it works -> Values -> the rest. A view component only: the
* route that renders it owns data loading, SEO head, and structured data.
*
* Paired design: HomeClassic.svelte. To roll back, swap the component the
* homepage route renders. See src/routes/+page.svelte.
*/
import Footer from '$lib/components/ui/Footer.svelte';
import Header from '$lib/components/ui/Header.svelte';
import Icon from '$lib/components/ui/Icon.svelte';
import HowItWorksSection from '$lib/components/sections/HowItWorksSection.svelte';
import InfoSection from '$lib/components/sections/InfoSection.svelte';
import InstagramSection from '$lib/components/sections/InstagramSection.svelte';
import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
import FounderStorySection from '$lib/components/sections/FounderStorySection.svelte';
import ServicesSection from '$lib/components/sections/ServicesSection.svelte';
import TestimonialsSection from '$lib/components/sections/TestimonialsSection.svelte';
import ValuesSection from '$lib/components/sections/ValuesSection.svelte';
import type { HomePageContent } from '$lib/types';
import type { AbContext } from '$lib/ab';
export let content: HomePageContent;
export let ab: AbContext | undefined = undefined;
export let pagePath = '/';
const reviewCta = content.intro.reviewCta;
const heroImage = '/images/smiling-happy-dog-maya.webp';
const stars = Array.from({ length: 5 });
</script>
<Header navigation={content.navigation} />
<section class="chero" data-track-location="hero" aria-label="Goodwalk dog walking">
<div class="chero-inner">
<div class="chero-copy">
<p class="chero-eyebrow">Goodwalk · Auckland dog walking</p>
<h1 class="chero-title">
<span class="chero-title-lead">All we do is</span>
<span class="chero-title-main">small &amp; medium dogs</span>
</h1>
<p class="chero-lead">
Real adventures with the Tiny Gang, in small, matched groups across
Auckland. Same walker every time, so your dog comes home happy, tired,
and ready to settle.
</p>
</div>
<div class="chero-media">
<div class="chero-photo">
<img
src={heroImage}
alt="Maya, a happy small dog, after a Goodwalk walk in Auckland"
width="1536"
height="1024"
loading="eager"
fetchpriority="high"
decoding="async"
/>
</div>
</div>
<div class="chero-actions">
<a
href="#newlead"
class="btn btn-yellow btn-with-arrow"
data-track-event="cta_click"
data-track-type="hero_primary"
data-track-label="Book a free Meet & Greet"
>
Book a free Meet &amp; Greet
<Icon name="fas fa-arrow-right" />
</a>
<a
class="chero-reviews"
href={reviewCta.href}
target={reviewCta.external ? '_blank' : undefined}
rel={reviewCta.external ? 'noopener' : undefined}
aria-label="30+ five-star Google reviews"
data-track-event="social_proof_click"
data-track-type="hero_reviews"
>
<span class="chero-reviews-mark" aria-hidden="true">
<img src="/images/google-g-logo.svg" alt="" width="15" height="16" />
</span>
<span class="chero-reviews-stars" aria-hidden="true">
{#each stars as _}
<Icon name="fas fa-star" />
{/each}
</span>
<span class="chero-reviews-label">30+ five-star reviews</span>
</a>
</div>
</div>
</section>
<!-- Founder philosophy strip: the sincere counter to a competitor's
"total freedom in nature" pitch. Frames the upside the owner is paying
for: a day built around their dog, not a one-size pack. -->
<section class="cfounder" aria-label="Our approach">
<div class="cfounder-inner">
<figure class="cfounder-quote">
<blockquote class="cfounder-blockquote">
<p class="cfounder-text">
Every dog gets a walk built around them, not a one-size pack: the right
small group, a walker who knows their name and their quirks, and
adventures that grow with their confidence. That's how a walk becomes
the best part of a dog's day.
</p>
</blockquote>
<figcaption class="cfounder-attr">
<img
class="cfounder-avatar"
src="/images/alessandra-goodwalk-founder-auckland.webp"
alt="Alessandra, founder of Goodwalk, walking a dog in Auckland"
width="96"
height="96"
loading="lazy"
decoding="async"
/>
<span class="cfounder-byline">
<span class="cfounder-name">Alessandra</span>
<span class="cfounder-role">Founder of Goodwalk</span>
</span>
</figcaption>
</figure>
</div>
</section>
<!-- Offer first: what can I actually book, and roughly what does it cost? -->
<ServicesSection services={content.services} />
<!-- Then the process: what happens after I enquire? Lowers the unknown. -->
<HowItWorksSection content={content.howItWorks} />
<!-- Only now the reasons to choose us. -->
<ValuesSection values={content.values} />
<TestimonialsSection testimonials={content.testimonials} seedKey={pagePath} />
<FounderStorySection founderStory={content.founderStory} />
<InfoSection info={content.info} />
<BookingWizard booking={content.booking} {pagePath} {ab} />
<InstagramSection instagram={content.instagram} />
<Footer footer={content.footer} />
<style>
/* ── Hero ──────────────────────────────────────────────────────────── */
.chero {
position: relative;
overflow: hidden;
background: var(--gw-green);
color: var(--text-inverse);
}
/* A single soft warm light, top-right, for depth. Not glassmorphism,
not a gradient headline: just one breath of the brand yellow. */
.chero::before {
content: '';
position: absolute;
top: -28%;
right: -10%;
width: 60%;
height: 90%;
background: radial-gradient(closest-side, rgba(var(--accent-rgb), 0.16), transparent 70%);
pointer-events: none;
z-index: 0;
}
.chero-inner {
position: relative;
z-index: 1;
max-width: var(--max-w);
min-height: clamp(500px, 68vh, 640px);
margin: 0 auto;
padding: clamp(48px, 5.5vw, 80px) var(--space-container-x);
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
grid-template-areas:
'copy media'
'actions media';
column-gap: clamp(32px, 5vw, 76px);
row-gap: clamp(22px, 3vw, 32px);
align-content: center;
align-items: center;
}
.chero-copy {
grid-area: copy;
}
.chero-eyebrow {
display: inline-flex;
align-items: center;
gap: 12px;
margin: 0 0 18px;
font-family: var(--font-body);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: rgba(var(--white-rgb), 0.7);
}
.chero-eyebrow::before {
content: '';
width: 26px;
height: 2px;
border-radius: 2px;
background: var(--yellow);
}
.chero-title {
margin: 0;
font-family: var(--font-head);
}
.chero-title-lead {
display: block;
margin-bottom: 6px;
font-size: clamp(18px, 2.1vw, 26px);
font-weight: 600;
line-height: 1.15;
letter-spacing: -0.01em;
color: rgba(var(--white-rgb), 0.82);
}
.chero-title-main {
display: block;
font-size: clamp(42px, 6.4vw, 78px);
font-weight: 800;
line-height: 0.98;
letter-spacing: -0.04em;
color: var(--yellow);
text-wrap: balance;
}
.chero-lead {
margin: 22px 0 0;
max-width: 40ch;
font-size: clamp(16px, 1.3vw, 19px);
line-height: 1.6;
color: rgba(var(--white-rgb), 0.82);
}
.chero-actions {
grid-area: actions;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px 26px;
}
.chero-reviews {
display: inline-flex;
align-items: center;
gap: 9px;
text-decoration: none;
color: rgba(var(--white-rgb), 0.9);
transition: color var(--motion-fast);
}
.chero-reviews-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--surface-page);
box-shadow: var(--shadow-card);
}
.chero-reviews-mark img {
display: block;
width: 15px;
height: auto;
}
.chero-reviews-stars {
display: inline-flex;
gap: 2px;
color: var(--yellow);
font-size: 11px;
}
.chero-reviews-label {
font-size: 13px;
font-weight: 600;
color: rgba(var(--white-rgb), 0.9);
}
@media (hover: hover) {
.chero-reviews:hover {
color: var(--text-inverse);
}
}
.chero-reviews:focus-visible {
outline: 2px solid var(--yellow);
outline-offset: 3px;
border-radius: var(--radius-sm);
}
/* Media */
.chero-media {
grid-area: media;
position: relative;
justify-self: end;
width: 100%;
max-width: 460px;
}
.chero-media::before {
content: '';
position: absolute;
inset: 8% -6% -10% 10%;
border-radius: var(--radius-xl);
background: rgba(var(--accent-rgb), 0.22);
filter: blur(46px);
z-index: 0;
}
.chero-photo {
position: relative;
z-index: 1;
aspect-ratio: 4 / 5;
border-radius: 28px;
overflow: hidden;
box-shadow:
inset 0 0 0 1px rgba(var(--white-rgb), 0.12),
var(--shadow-2xl);
}
.chero-photo img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 28%;
}
/* ── Founder philosophy strip ──────────────────────────────────────── */
.cfounder {
background: var(--surface-panel-cream);
border-bottom: 1px solid var(--border-soft);
}
.cfounder-inner {
max-width: 940px;
margin: 0 auto;
padding: clamp(30px, 3.6vw, 48px) var(--space-container-x);
}
.cfounder-quote {
margin: 0;
position: relative;
padding-left: clamp(40px, 5vw, 60px);
}
.cfounder-blockquote {
margin: 0;
}
/* A single large opening quote mark, brand yellow, as the editorial cue.
Keeps the strip recognisably a statement without a repeated eyebrow. */
.cfounder-quote::before {
content: '\201C';
position: absolute;
left: -4px;
top: -0.18em;
font-family: var(--font-head);
font-size: clamp(64px, 9vw, 104px);
font-weight: 800;
line-height: 1;
color: var(--yellow);
}
.cfounder-text {
margin: 0;
max-width: 46ch;
font-family: var(--font-body);
font-size: clamp(19px, 2.1vw, 27px);
font-weight: 500;
line-height: 1.45;
letter-spacing: -0.01em;
color: var(--text-heading);
text-wrap: pretty;
}
.cfounder-attr {
display: flex;
align-items: center;
gap: 14px;
margin-top: clamp(18px, 2.4vw, 26px);
}
.cfounder-avatar {
width: 56px;
height: 56px;
border-radius: 50%;
object-fit: cover;
object-position: center 25%;
box-shadow:
inset 0 0 0 2px rgba(var(--white-rgb), 0.9),
var(--shadow-card);
}
.cfounder-byline {
display: flex;
flex-direction: column;
line-height: 1.25;
}
.cfounder-name {
font-family: var(--font-head);
font-size: 15px;
font-weight: 700;
color: var(--text-heading);
}
.cfounder-role {
font-size: 13px;
color: var(--text-muted);
}
/* Entrance */
@media (prefers-reduced-motion: no-preference) {
.chero-eyebrow,
.chero-title,
.chero-lead,
.chero-actions {
animation: cheroRise 0.6s var(--ease-out-soft) both;
}
.chero-title {
animation-delay: 0.08s;
}
.chero-lead {
animation-delay: 0.16s;
}
.chero-actions {
animation-delay: 0.26s;
}
.chero-photo {
animation: cheroPhoto 0.8s var(--ease-out-soft) both;
}
}
@keyframes cheroRise {
from {
opacity: 0;
transform: translateY(14px);
}
}
@keyframes cheroPhoto {
from {
opacity: 0;
transform: scale(1.04);
}
}
/* ── Mobile: one calm column, message first, then Maya, then action ── */
@media (max-width: 768px) {
.chero-inner {
display: flex;
flex-direction: column;
min-height: 0;
padding: 28px var(--space-container-x-mobile) 36px;
row-gap: 0;
}
.chero-copy {
order: 1;
}
.chero-media {
order: 2;
justify-self: stretch;
max-width: none;
margin: 24px 0 26px;
}
.chero-actions {
order: 3;
flex-direction: column;
align-items: flex-start;
gap: 18px;
}
.chero-photo {
aspect-ratio: 5 / 4;
}
.chero-lead {
max-width: none;
}
.chero-actions .btn {
width: 100%;
}
.cfounder-quote {
padding-left: 40px;
}
.cfounder-quote::before {
left: -2px;
top: -0.08em;
font-size: 52px;
}
}
</style>
@@ -0,0 +1,47 @@
<script lang="ts">
/*
* Classic homepage design (the original layout, pre-clarity swap).
*
* Hero -> Values -> Services -> How it works -> Testimonials -> Founder ->
* Info -> Booking -> Instagram. Preserved so the homepage can be rolled back
* to it by swapping the component rendered in src/routes/+page.svelte.
*
* Paired design: HomeClarity.svelte. A view component only: the route that
* renders it owns data loading, SEO head, and structured data.
*/
import Footer from '$lib/components/ui/Footer.svelte';
import Header from '$lib/components/ui/Header.svelte';
import HeroSection from '$lib/components/sections/HeroSection.svelte';
import HowItWorksSection from '$lib/components/sections/HowItWorksSection.svelte';
import InfoSection from '$lib/components/sections/InfoSection.svelte';
import InstagramSection from '$lib/components/sections/InstagramSection.svelte';
import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
import FounderStorySection from '$lib/components/sections/FounderStorySection.svelte';
import ServicesSection from '$lib/components/sections/ServicesSection.svelte';
import TestimonialsSection from '$lib/components/sections/TestimonialsSection.svelte';
import ValuesSection from '$lib/components/sections/ValuesSection.svelte';
import type { CallToAction, HomePageContent } from '$lib/types';
import type { AbContext } from '$lib/ab';
export let content: HomePageContent;
export let ab: AbContext | undefined = undefined;
export let primaryCtaOverride: CallToAction | undefined = undefined;
export let pagePath = '/';
</script>
<Header navigation={content.navigation} />
<HeroSection
hero={content.hero}
reviewCta={content.intro.reviewCta}
{primaryCtaOverride}
{ab}
/>
<ValuesSection values={content.values} />
<ServicesSection services={content.services} />
<HowItWorksSection content={content.howItWorks} />
<TestimonialsSection testimonials={content.testimonials} seedKey={pagePath} />
<FounderStorySection founderStory={content.founderStory} />
<InfoSection info={content.info} />
<BookingWizard booking={content.booking} {pagePath} {ab} />
<InstagramSection instagram={content.instagram} />
<Footer footer={content.footer} />
@@ -0,0 +1,613 @@
<script lang="ts">
/*
* Tiny Gang pack walks page — clarity redesign.
*
* One design system, one message. Replaces the multi-card / multi-gradient
* ServiceLandingPage treatment (for this page) with a single green hero +
* light body that shares the homepage's visual language. The message is said
* once, in order: what it is -> what's different -> how joining works -> fit
* -> proof -> price -> FAQ -> book.
*
* View component only: the route owns Header/Footer, SEO, and data.
* Preview at /variants/tiny-gang. To go live, render this instead of
* ServiceLandingPage for pack-walks in src/routes/[slug]/+page.svelte.
*/
import Icon from '$lib/components/ui/Icon.svelte';
import ServiceHero from '$lib/components/sections/ServiceHero.svelte';
import { reveal } from '$lib/actions/reveal';
import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
import PricingPlanCard from '$lib/components/ui/PricingPlanCard.svelte';
import TestimonialsSection from '$lib/components/sections/TestimonialsSection.svelte';
import FaqSection from '$lib/components/sections/FaqSection.svelte';
import { decoratePlans } from '$lib/utils/pricing';
import type { ServicePageContent, SiteSharedContent } from '$lib/types';
export let content: SiteSharedContent;
export let pageContent: ServicePageContent;
export let currentPath = '/pack-walks';
// What makes Tiny Gang different. Said once here, not spread across a
// 3-point highlight and a 6-card benefit grid that repeated each other.
const traits = [
{
icon: 'fas fa-users',
title: 'Carefully matched groups',
body: '4 to 8 dogs, grouped by temperament, energy, and play style. Never a random pack.'
},
{
icon: 'fas fa-shield-dog',
title: 'Small & medium dogs only',
body: 'Pace, group size, and play style are all built for little dogs, so yours is never the odd one out.'
},
{
icon: 'fas fa-user-check',
title: 'The same walker, every week',
body: 'One familiar face at the door and on the walk. Your dog is known, not handed around.'
},
{
icon: 'fas fa-eye',
title: 'Eyes on every dog',
body: 'Small groups mean we notice the little things early, from recall to a quiet day.'
}
];
// How joining works. The process, made explicit instead of buried in a
// paragraph and an FAQ.
const steps = [
{
title: 'Free Meet & Greet',
body: 'We come to your home, meet your dog, and talk honestly about whether the pack suits them.'
},
{
title: 'Two assessment walks',
body: 'Your dog settles in gently while we confirm the right group, pace, and handling.'
},
{
title: 'Your weekly slot',
body: 'A regular spot with the same little crew and the same walker, week in, week out.'
}
];
// Distilled pricing message. The title already says the routine and the
// price says the cost, so each card keeps only the one or two lines that
// actually differ. The universal "free pickup" promise is stated once under
// the grid instead of repeated on every card.
const planLines: Record<string, string[]> = {
'1 Walk Per Week': [
'A calm weekly outing',
'Time to sniff, play, and unwind',
'Great for dogs just starting out'
],
'2-3 Walks Per Week': [
'Steady exercise and real social time',
'A settled mid-week rhythm',
'Our balanced routine'
],
'4-5 Walks Per Week': [
'Best value per walk',
'Maximum routine and structure',
'For high-energy, social dogs'
],
'Casual Pack Walk': [
'No weekly commitment',
'Book only the weeks you need',
'For dogs already in the pack'
]
};
$: pricingPlans = decoratePlans(
pageContent.pricing.plans.map((plan) => ({
...plan,
features: planLines[plan.title] ?? plan.features
}))
);
$: faqItems = pageContent.faq?.items?.slice(0, 6) ?? [];
</script>
<main class="tg">
<!-- Hero: the shared service-page hero (green copy left, photo bleeds
right), identical to /dog-walking and /puppy-visits. -->
<ServiceHero
eyebrow={pageContent.hero.eyebrow}
title={pageContent.hero.title}
subtitle={pageContent.hero.subtitle}
imageUrl={pageContent.hero.imageUrl}
imageAlt={pageContent.hero.imageAlt}
chips={pageContent.hero.chips ?? []}
cta={pageContent.hero.cta}
/>
<!-- What makes it different (said once) -->
<section class="tg-section" use:reveal={{ distance: 0 }} aria-labelledby="tg-diff-heading">
<div class="tg-inner">
<div class="tg-head">
<p class="tg-eyebrow">What makes Tiny Gang different</p>
<h2 id="tg-diff-heading" class="tg-h2">Not a big anonymous pack. A small, matched crew.</h2>
</div>
<ul class="tg-traits">
{#each traits as trait}
<li class="tg-trait">
<span class="tg-trait-icon" aria-hidden="true"><Icon name={trait.icon} /></span>
<div>
<h3>{trait.title}</h3>
<p>{trait.body}</p>
</div>
</li>
{/each}
</ul>
</div>
</section>
<!-- How joining works (the process, explicit) -->
<section class="tg-section tg-section-steps" use:reveal={{ distance: 0 }} aria-labelledby="tg-steps-heading">
<div class="tg-inner">
<div class="tg-head">
<p class="tg-eyebrow">How joining works</p>
<h2 id="tg-steps-heading" class="tg-h2">Three simple steps, no pressure.</h2>
</div>
<ol class="tg-steps">
{#each steps as step, i}
<li class="tg-step">
<span class="tg-step-num" aria-hidden="true">{i + 1}</span>
<h3>{step.title}</h3>
<p>{step.body}</p>
</li>
{/each}
</ol>
</div>
</section>
<!-- Is it right for your dog? -->
{#if pageContent.decision}
<section class="tg-section" use:reveal={{ distance: 0 }} aria-labelledby="tg-fit-heading">
<div class="tg-inner">
<div class="tg-head">
<p class="tg-eyebrow">Honest fit check</p>
<h2 id="tg-fit-heading" class="tg-h2">{pageContent.decision.title ?? 'Is Tiny Gang right for your dog?'}</h2>
</div>
<div class="tg-fit">
<div class="tg-fit-col">
<h3>{pageContent.decision.fitTitle ?? 'A great fit if your dog:'}</h3>
<ul>
{#each pageContent.decision.fitItems as item}
<li>
<span class="tg-fit-mark tg-fit-mark-yes" aria-hidden="true"><Icon name="fas fa-check" /></span>
<span>{item}</span>
</li>
{/each}
</ul>
</div>
<div class="tg-fit-col">
<h3>{pageContent.decision.notFitTitle ?? 'Better on a solo walk if your dog:'}</h3>
<ul>
{#each pageContent.decision.notFitItems as item}
<li>
<span class="tg-fit-mark tg-fit-mark-no" aria-hidden="true"><Icon name="fas fa-xmark" /></span>
<span>{item}</span>
</li>
{/each}
</ul>
</div>
</div>
{#if pageContent.decision.footnote}
<p class="tg-fit-note">{pageContent.decision.footnote}</p>
{/if}
</div>
</section>
{/if}
<!-- Proof: owners love it -->
<TestimonialsSection
heading="Owners love the Tiny Gang"
testimonials={content.testimonials}
seedKey={currentPath}
/>
<!-- Price -->
<section class="tg-section" use:reveal={{ distance: 0 }} aria-labelledby="tg-price-heading">
<div class="tg-inner tg-inner-wide">
<div class="tg-head">
<p class="tg-eyebrow">Simple weekly pricing</p>
<h2 id="tg-price-heading" class="tg-h2">{pageContent.pricing.title}</h2>
<p class="tg-price-tagline">The best investment you'll ever make is in your dog's happiness.</p>
{#if pageContent.pricing.intro}
<p class="tg-head-sub">{pageContent.pricing.intro}</p>
{/if}
</div>
<div class="tg-plans">
{#each pricingPlans as plan}
<PricingPlanCard {plan} variant="service" hideCtaOnMobile={true} />
{/each}
</div>
<div class="tg-price-notes">
{#if pageContent.pricing.scarcityNote}
<p class="tg-note tg-note-warm"><Icon name="fas fa-clock" />{pageContent.pricing.scarcityNote}</p>
{/if}
<p class="tg-note"><Icon name="fas fa-stopwatch" />60+ minute adventures.</p>
<p class="tg-note"><Icon name="fas fa-car" />Free pickup &amp; drop-off, every walk.</p>
<p class="tg-note"><Icon name="fas fa-shield-heart" />Starts with a free Meet &amp; Greet.</p>
</div>
<a class="btn btn-yellow btn-mobile-center tg-plans-cta" href="#newlead">Book a Meet &amp; Greet</a>
</div>
</section>
<!-- FAQ -->
{#if faqItems.length}
<section class="tg-section" use:reveal={{ distance: 0 }}>
<div class="tg-inner tg-inner-narrow">
<FaqSection
title={pageContent.faq?.title ?? 'Tiny Gang FAQs'}
intro={pageContent.faq?.intro}
faqs={faqItems}
/>
</div>
</section>
{/if}
<BookingWizard booking={pageContent.booking} pagePath={currentPath} />
</main>
<style>
/* ── One system: tokens for this page ──────────────────────────────── */
.tg {
--tg-max: 1120px;
--tg-narrow: 820px;
background: var(--off-white);
color: var(--text-heading);
}
.tg-inner {
max-width: var(--tg-max);
margin: 0 auto;
padding-inline: var(--space-container-x);
}
.tg-inner-narrow {
max-width: var(--tg-narrow);
}
/* Pricing breaks out wider than the body so four cards have real width
(and the full CTA never clips). */
.tg-inner-wide {
max-width: 1280px;
}
.tg-section {
padding-block: clamp(48px, 6vw, 84px);
}
/* Alternating surface for rhythm, not a new "system": just a tint. */
.tg-section-steps {
background: var(--surface-panel-cream);
border-block: 1px solid var(--border-soft);
}
/* One eyebrow, one heading, everywhere. */
.tg-eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
margin: 0 0 14px;
font-family: var(--font-body);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--gw-green);
}
.tg-eyebrow::before {
content: '';
width: 24px;
height: 2px;
border-radius: 2px;
background: var(--yellow);
}
.tg-head {
max-width: 40rem;
margin: 0 auto clamp(28px, 4vw, 44px);
text-align: center;
}
.tg-h2 {
margin: 0;
font-family: var(--font-head);
font-size: clamp(28px, 3.4vw, 42px);
font-weight: 700;
line-height: 1.08;
letter-spacing: -0.03em;
color: var(--text-heading);
text-wrap: balance;
}
.tg-head-sub {
margin: 14px auto 0;
max-width: 36rem;
color: var(--text-muted);
font-size: 17px;
line-height: 1.6;
}
.tg-price-tagline {
margin: 14px auto 0;
max-width: 32rem;
color: var(--gw-green);
font-family: var(--font-head);
font-size: clamp(17px, 1.7vw, 20px);
font-weight: 600;
line-height: 1.4;
letter-spacing: -0.01em;
text-wrap: balance;
}
/* ── Traits: a clean feature list, not boxed cards ─────────────────── */
.tg-traits {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: clamp(24px, 3vw, 40px) clamp(32px, 5vw, 64px);
}
.tg-trait {
display: grid;
grid-template-columns: 44px minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.tg-trait-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 14px;
background: var(--surface-brand);
color: var(--yellow);
font-size: 17px;
}
.tg-trait h3 {
margin: 2px 0 6px;
font-family: var(--font-head);
font-size: 19px;
line-height: 1.2;
color: var(--text-heading);
}
.tg-trait p {
margin: 0;
color: var(--text-muted);
font-size: 15px;
line-height: 1.6;
max-width: 38ch;
}
/* ── Steps: numerals, not cards ────────────────────────────────────── */
.tg-steps {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: clamp(24px, 4vw, 48px);
counter-reset: tg-step;
}
.tg-step {
position: relative;
}
.tg-step-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
margin-bottom: 16px;
border-radius: 50%;
background: var(--yellow);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 16px;
font-weight: 800;
}
.tg-step h3 {
margin: 0 0 8px;
font-family: var(--font-head);
font-size: 19px;
line-height: 1.2;
color: var(--text-heading);
}
.tg-step p {
margin: 0;
color: var(--text-muted);
font-size: 15px;
line-height: 1.6;
}
/* ── Fit check: one card, two columns ──────────────────────────────── */
.tg-fit {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: clamp(20px, 3vw, 32px);
padding: clamp(24px, 3vw, 40px);
border-radius: var(--radius-xl);
background: var(--surface-panel);
box-shadow:
inset 0 0 0 1px var(--border-soft-strong),
var(--shadow-card);
}
.tg-fit-col h3 {
margin: 0 0 16px;
font-family: var(--font-head);
font-size: 17px;
line-height: 1.3;
color: var(--text-heading);
}
.tg-fit-col ul {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 12px;
}
.tg-fit-col li {
display: grid;
grid-template-columns: 22px minmax(0, 1fr);
gap: 12px;
align-items: start;
color: var(--text-heading-soft);
font-size: 15px;
line-height: 1.5;
}
.tg-fit-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
margin-top: 1px;
border-radius: 50%;
font-size: 10px;
}
.tg-fit-mark-yes {
background: rgba(var(--brand-rgb), 0.1);
color: var(--gw-green);
}
.tg-fit-mark-no {
background: rgba(var(--ink-rgb), 0.06);
color: var(--text-subtle);
}
.tg-fit-note {
margin: 22px auto 0;
max-width: 48rem;
color: var(--text-muted);
font-size: 14px;
line-height: 1.6;
text-align: center;
}
/* ── Pricing ───────────────────────────────────────────────────────── */
.tg-plans {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 24px;
}
/* Roomier, consistent cards on this page, scoped via .tg-plans so the shared
PricingPlanCard and other pages keep their own sizing. */
.tg-plans :global(.plan-card) {
padding: 44px 30px 38px;
}
/* The featured card carries the "Goodwalk favourite" ribbon (absolute,
top: 16px). Give it enough head room that the ribbon sits clearly above
the title instead of over it. */
.tg-plans :global(.plan-card--featured) {
padding-top: 62px;
}
/* Full-width CTA: never squished, and consistent across all four cards.
display:flex (block-level) overrides the component's inline-flex +
align-self:center, so the button always spans the full card width. */
.tg-plans :global(.plan-card__cta) {
display: flex;
width: 100%;
min-width: 0;
align-self: stretch;
padding-inline: 15px;
}
.tg-price-notes {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
margin-top: 24px;
}
.tg-note {
display: inline-flex;
align-items: center;
gap: 10px;
margin: 0;
padding: 8px 16px;
border-radius: var(--radius-pill);
background: var(--surface-brand-soft);
color: var(--gw-green);
font-size: 14px;
font-weight: 600;
}
.tg-note-warm {
background: var(--surface-accent-muted);
color: #5a4500;
}
:global(.tg-note .icon) {
color: var(--yellow);
font-size: 14px;
}
:global(.tg-note-warm .icon) {
color: #b88800;
}
.tg-plans-cta {
display: none;
}
/* ── Responsive ────────────────────────────────────────────────────── */
@media (max-width: 980px) {
.tg-plans {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.tg-traits {
grid-template-columns: 1fr;
}
.tg-steps {
grid-template-columns: 1fr;
gap: 24px;
}
.tg-fit {
grid-template-columns: 1fr;
}
.tg-plans {
grid-template-columns: 1fr;
}
.tg-plans :global(.plan-card) {
padding: 30px 24px 26px;
}
.tg-plans :global(.plan-card--featured) {
padding-top: 46px;
}
.tg-plans-cta {
display: flex;
}
}
</style>
+2 -2
View File
@@ -36,9 +36,9 @@ export const homepageContent: HomePageContent = {
title: 'Come home to a',
highlight: 'calm, happy dog',
mobileTitle: 'Come home to a\ncalm, happy dog',
seoHeading: 'Dog walking for Auckland\'s small & medium dogs',
seoHeading: 'Specialist dog walking for Auckland\'s small & medium dogs',
subtitle:
'The same trusted walker every time, in small groups. Never a rotating roster.',
'Gentle pacing, familiar routines, and calmer evenings at home.',
primaryCta: { label: 'Book a Meet & Greet', href: '#newlead', variant: 'yellow' },
secondaryCta: {
label: 'See how it works',
+1 -1
View File
@@ -4,7 +4,7 @@ export const packWalksContent: ServicePageContent = {
hero: {
eyebrow: 'Tiny Gang Pack Walks',
title: 'Pack walks that actually suit small & medium dogs',
subtitle: 'Small groups, thoughtful matching, and off-leash freedom only when a dog is truly ready.',
subtitle: 'A proper adventure with a pack of like-minded dogs their own size, so yours comes home happy, completely worn out, and done for the day.',
introEyebrow: 'How Tiny Gang works',
introHeading: 'What joining Tiny Gang actually looks like',
detailHighlights: [
+7 -39
View File
@@ -1,36 +1,19 @@
<script lang="ts">
import SeoHead from '$lib/components/ui/SeoHead.svelte';
import Footer from '$lib/components/ui/Footer.svelte';
import Header from '$lib/components/ui/Header.svelte';
import HeroSection from '$lib/components/sections/HeroSection.svelte';
import HowItWorksSection from '$lib/components/sections/HowItWorksSection.svelte';
import InfoSection from '$lib/components/sections/InfoSection.svelte';
import InstagramSection from '$lib/components/sections/InstagramSection.svelte';
import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
import FounderStorySection from '$lib/components/sections/FounderStorySection.svelte';
import ServicesSection from '$lib/components/sections/ServicesSection.svelte';
import TestimonialsSection from '$lib/components/sections/TestimonialsSection.svelte';
import ValuesSection from '$lib/components/sections/ValuesSection.svelte';
import OnboardingPage from '$lib/components/pages/OnboardingPage.svelte';
// ── Homepage design ──────────────────────────────────────────────────
// The live homepage renders HomeClarity. To roll back to the original
// layout, change the two references below to HomeClassic (its preview lives
// at /variants/homepage-original). Both are pure view components; all data
// loading, SEO head, and structured data stay here.
import HomeClarity from '$lib/components/pages/HomeClarity.svelte';
import { buildAreaServed } from '$lib/seo';
import type { PageData } from './$types';
import type { CallToAction } from '$lib/types';
export let data: PageData;
const siteUrl = 'https://www.goodwalk.co.nz';
// hero_cta experiment: free_emphasis swaps "Book a Meet & Greet"
// for "Book a FREE Meet & Greet". Same href, same destination.
$: heroAb = data.ab;
$: heroOverride =
data.siteVariant === 'marketing' && data.content && heroAb?.variant === 'free_emphasis'
? ({
...data.content.hero.primaryCta,
label: 'Book a FREE Meet & Greet',
} satisfies CallToAction)
: undefined;
function absoluteUrl(value: string) {
if (value.startsWith('http://') || value.startsWith('https://')) {
return value;
@@ -166,20 +149,5 @@
: ''}
/>
<Header navigation={content.navigation} />
<HeroSection
hero={content.hero}
reviewCta={content.intro.reviewCta}
primaryCtaOverride={heroOverride}
ab={heroAb}
/>
<ValuesSection values={content.values} />
<ServicesSection services={content.services} />
<HowItWorksSection content={content.howItWorks} />
<TestimonialsSection testimonials={content.testimonials} seedKey="/" />
<FounderStorySection founderStory={content.founderStory} />
<InfoSection info={content.info} />
<BookingWizard booking={content.booking} pagePath="/" ab={heroAb} />
<InstagramSection instagram={content.instagram} />
<Footer footer={content.footer} />
<HomeClarity {content} ab={data.ab} pagePath="/" />
{/if}
@@ -0,0 +1,16 @@
<script lang="ts">
/*
* Preview of the clarity homepage design (currently live at /).
* Single source of truth is HomeClarity.svelte. noindex so it does not
* compete with the real homepage in search.
*/
import HomeClarity from '$lib/components/pages/HomeClarity.svelte';
import { homepageContent } from '$lib/content/homepage';
</script>
<svelte:head>
<title>Homepage clarity design (preview) | Goodwalk</title>
<meta name="robots" content="noindex, nofollow" />
</svelte:head>
<HomeClarity content={homepageContent} pagePath="/variants/homepage-clarity" />
@@ -0,0 +1,17 @@
<script lang="ts">
/*
* Preview of the original homepage design, kept for rollback reference.
* Single source of truth is HomeClassic.svelte. To restore this as the live
* homepage, render HomeClassic in src/routes/+page.svelte. noindex so it does
* not compete with the real homepage in search.
*/
import HomeClassic from '$lib/components/pages/HomeClassic.svelte';
import { homepageContent } from '$lib/content/homepage';
</script>
<svelte:head>
<title>Homepage original design (preview) | Goodwalk</title>
<meta name="robots" content="noindex, nofollow" />
</svelte:head>
<HomeClassic content={homepageContent} pagePath="/variants/homepage-original" />
@@ -0,0 +1,24 @@
<script lang="ts">
/*
* Preview of the redesigned Tiny Gang pack walks page.
* Single source of truth is TinyGangPage.svelte. noindex so it does not
* compete with the live /pack-walks page in search.
*
* To go live: in src/routes/[slug]/+page.svelte, render TinyGangPage instead
* of ServiceLandingPage in the `data.slug === 'pack-walks'` branch.
*/
import Header from '$lib/components/ui/Header.svelte';
import Footer from '$lib/components/ui/Footer.svelte';
import TinyGangPage from '$lib/components/pages/TinyGangPage.svelte';
import { homepageContent } from '$lib/content/homepage';
import { packWalksContent } from '$lib/content/pack-walks';
</script>
<svelte:head>
<title>Tiny Gang pack walks (redesign preview) | Goodwalk</title>
<meta name="robots" content="noindex, nofollow" />
</svelte:head>
<Header navigation={homepageContent.navigation} />
<TinyGangPage content={homepageContent} pageContent={packWalksContent} currentPath="/variants/tiny-gang" />
<Footer footer={homepageContent.footer} />