v4.0.6 - Simplicity focus

This commit is contained in:
2026-07-04 09:59:57 +12:00
parent bfd96c677f
commit 8eb20813c8
21 changed files with 2410 additions and 5221 deletions
+49 -21
View File
@@ -27,10 +27,40 @@
export let content: HomePageContent;
export let ab: AbContext | undefined = undefined;
export let pagePath = '/';
// The booking form is swappable so variants can preview an alternate Meet &
// Greet form on the same homepage design. Defaults to the live wizard, so the
// homepage route renders unchanged. See /variants/meet-greet.
export let BookingComponent: typeof BookingWizard = BookingWizard;
const meetGreetVariantPath = '/variants/meet-greet';
$: isMeetGreetVariant = pagePath === meetGreetVariantPath;
const reviewCta = content.intro.reviewCta;
const heroImage = '/images/smiling-happy-dog-maya.webp';
const stars = Array.from({ length: 5 });
$: heroLead = isMeetGreetVariant
? 'Small group walks, solo support, and puppy visits for Auckland dogs who do better with a calm, consistent routine.'
: '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.';
$: founderQuote = isMeetGreetVariant
? "Small groups, careful matching, and the same walker each time. That's how a walk feels safe, social, and worth looking forward to."
: "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.";
$: servicesHeading = isMeetGreetVariant
? 'Choose the setup that fits your dog.'
: undefined;
$: servicesIntro = isMeetGreetVariant
? 'Pack walks for social dogs, solo walks for dogs who need more space, and puppy visits for the early routine.'
: undefined;
$: howItWorksContent = isMeetGreetVariant
? {
...content.howItWorks,
intro: 'Start with a free Meet & Greet. We assess the fit, then build the right routine for your dog.'
}
: content.howItWorks;
$: bookingContent = isMeetGreetVariant
? {
...content.booking,
subtitle: 'Tell us a little about your dog. We reply within 24 hours to arrange your free Meet & Greet.'
}
: content.booking;
</script>
<Header navigation={content.navigation} />
@@ -40,14 +70,10 @@
<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>
<span class="chero-title-lead">Specialist walks for small &amp; medium dogs</span>
<span class="chero-title-main">Meet the Tiny Gang.</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>
<p class="chero-lead">{heroLead}</p>
</div>
<div class="chero-media">
@@ -107,10 +133,7 @@
<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.
{founderQuote}
</p>
</blockquote>
<figcaption class="cfounder-attr">
@@ -133,19 +156,24 @@
</section>
<!-- Offer first: what can I actually book, and roughly what does it cost? -->
<ServicesSection services={content.services} />
<ServicesSection services={content.services} heading={servicesHeading} intro={servicesIntro} />
<!-- Then the process: what happens after I enquire? Lowers the unknown. -->
<HowItWorksSection content={content.howItWorks} />
<HowItWorksSection content={howItWorksContent} />
<!-- 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} />
{#if isMeetGreetVariant}
<TestimonialsSection testimonials={content.testimonials} seedKey={pagePath} />
<InfoSection info={content.info} />
<svelte:component this={BookingComponent} booking={bookingContent} {pagePath} {ab} />
{:else}
<!-- 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} />
<svelte:component this={BookingComponent} booking={bookingContent} {pagePath} {ab} />
<InstagramSection instagram={content.instagram} />
{/if}
<Footer footer={content.footer} />
<style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,613 +0,0 @@
<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>