v4.0.6 0 Simplicity focus

This commit is contained in:
2026-07-04 10:00:16 +12:00
parent 8eb20813c8
commit 018024510b
31 changed files with 11902 additions and 744 deletions
+1 -1
View File
@@ -847,7 +847,7 @@
</div>
</section>
<OnboardingFooter email={userEmail} on:logout={handleLogout} />
<OnboardingFooter on:logout={handleLogout} />
{/if}
</main>
@@ -0,0 +1,737 @@
<script lang="ts">
/*
* Services-focus homepage variant.
*
* Same design language as HomeClarity (green hero, yellow CTAs, the shared
* ServicesSection / ValuesSection / TestimonialsSection), but every section is
* pointed at one job: get the visitor to click through into a service page.
*
* The site gets traffic but few leads, so this variant removes the big-commitment
* booking form from the homepage and lowers the first ask to "pick the walk that
* fits your dog". The three services are offered as quick-picks in the hero
* itself, repeated as the main ServicesSection cards, reinforced with proof, and
* offered a final time in a closing band. A view component only: the route that
* renders it owns data loading, SEO head, and structured data.
*
* Preview route: /variants/services-first (noindex).
*/
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 ServicesSection from '$lib/components/sections/ServicesSection.svelte';
import TestimonialsSection from '$lib/components/sections/TestimonialsSection.svelte';
import type { HomePageContent } from '$lib/types';
export let content: HomePageContent;
export let pagePath = '/variants/services-first';
const reviewCta = content.intro.reviewCta;
const heroImage = '/images/smiling-happy-dog-maya.webp';
const stars = Array.from({ length: 5 });
// The assurance bar replaces the old grid of value cards: four plain facts,
// stated once. Each is a reason to trust the click, not a feature to skim.
const assurances = [
{ icon: 'fas fa-user-check', label: 'Same walker, every time' },
{ icon: 'fas fa-users', label: 'Small, matched groups' },
{ icon: 'fas fa-shield-heart', label: 'Pet first-aid certified & insured' },
{ icon: 'fas fa-star', label: '5.0 from 30+ owners' }
];
// The hero quick-picks and the closing band both draw from the same services
// data the main ServicesSection uses, so the click targets never drift apart.
$: services = content.services;
</script>
<Header navigation={content.navigation} />
<section class="sfhero" data-track-location="hero" aria-label="Goodwalk dog walking">
<div class="sfhero-inner">
<div class="sfhero-copy">
<p class="sfhero-eyebrow">Professional dog walkers</p>
<h1 class="sfhero-title">
<span class="sfhero-title-main">Come home to a calmer dog.</span>
</h1>
<p class="sfhero-lead">
Small, matched groups for small &amp; medium dogs. The same walker, every time.
</p>
<div class="sfhero-actions">
<a
href="#services"
class="btn btn-yellow btn-with-arrow"
data-track-event="cta_click"
data-track-type="hero_primary"
data-track-label="See our walks"
>
See our walks
<Icon name="fas fa-arrow-right" />
</a>
<a
class="sfhero-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="sfhero-reviews-mark" aria-hidden="true">
<img src="/images/google-g-logo.svg" alt="" width="15" height="16" />
</span>
<span class="sfhero-reviews-stars" aria-hidden="true">
{#each stars as _}
<Icon name="fas fa-star" />
{/each}
</span>
<span class="sfhero-reviews-label">30+ five-star reviews</span>
</a>
</div>
</div>
<div class="sfhero-media">
<div class="sfhero-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>
<!-- Quick-picks: the three services as clickable choices, in the hero, above
the fold. This is the conversion move — the visitor's first decision is
"which walk", not "fill in a form". -->
<nav class="sfhero-picks" aria-label="Choose a service">
{#each services as service}
<a
class="sfhero-pick"
href={service.href}
data-track-event="service_quickpick_click"
data-track-type="hero_quickpick"
data-track-label={service.title}
>
<span class="sfhero-pick-glyph" aria-hidden="true">
<Icon name={service.icon} />
</span>
<span class="sfhero-pick-text">
<span class="sfhero-pick-title">{service.title}</span>
{#if service.priceFrom}
<span class="sfhero-pick-price">{service.priceFrom}</span>
{/if}
</span>
<Icon name="fas fa-arrow-right" className="sfhero-pick-arrow" />
</a>
{/each}
</nav>
</div>
</section>
<!-- The conversion engine: the service cards themselves link straight through to
each service page. -->
<ServicesSection
services={content.services}
heading="Choose your dog's walk."
intro="Three ways to walk — pack, solo, or puppy visits. Same walker, small groups, built around your dog."
/>
<!-- One calm assurance beat. Replaces the old wall of value cards: four plain
facts, stated once, then straight on to proof. No competing form. -->
<section class="sfproof" aria-label="What every Goodwalk includes">
<ul class="sfproof-row">
{#each assurances as item}
<li class="sfproof-item">
<Icon name={item.icon} className="sfproof-icon" />
<span>{item.label}</span>
</li>
{/each}
</ul>
</section>
<TestimonialsSection testimonials={content.testimonials} seedKey={pagePath} />
<!-- Closing band: offer the same three choices one more time, on brand green. -->
<section class="sfclose" aria-label="Choose your dog's walk">
<div class="sfclose-inner">
<div class="sfclose-head">
<h2 class="sfclose-title">Find the right walk for your dog.</h2>
<p class="sfclose-lead">
Three options. No commitment. Just pick the one that sounds like your dog.
</p>
</div>
<div class="sfclose-grid">
{#each services as service}
<a
class="sfclose-card"
href={service.href}
data-track-event="service_card_click"
data-track-type="closing_band"
data-track-label={service.title}
>
<span class="sfclose-card-glyph" aria-hidden="true">
<Icon name={service.icon} />
</span>
<span class="sfclose-card-title">{service.title}</span>
<span class="sfclose-card-body">{service.body}</span>
{#if service.priceFrom}
<span class="sfclose-card-price">{service.priceFrom}</span>
{/if}
<span class="sfclose-card-cta">
View this walk
<Icon name="fas fa-arrow-right" className="sfclose-card-arrow" />
</span>
</a>
{/each}
</div>
</div>
</section>
<Footer footer={content.footer} />
<style>
/* ── Hero ──────────────────────────────────────────────────────────────
Mirrors HomeClarity's .chero so the variant reads as the same site:
green field, one soft yellow light, identical type scale. The layout
adds a third row for the service quick-picks. */
.sfhero {
position: relative;
overflow: hidden;
background: var(--gw-green);
color: var(--text-inverse);
}
.sfhero::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;
}
.sfhero-inner {
position: relative;
z-index: 1;
max-width: var(--max-w);
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'
'picks picks';
column-gap: clamp(32px, 5vw, 76px);
row-gap: clamp(32px, 4vw, 48px);
align-items: center;
}
.sfhero-copy {
grid-area: copy;
}
.sfhero-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);
}
.sfhero-eyebrow::before {
content: '';
width: 26px;
height: 2px;
border-radius: 2px;
background: var(--yellow);
}
.sfhero-title {
margin: 0;
font-family: var(--font-head);
}
.sfhero-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;
}
.sfhero-lead {
margin: 22px 0 0;
max-width: 42ch;
font-size: clamp(16px, 1.3vw, 19px);
line-height: 1.6;
color: rgba(var(--white-rgb), 0.82);
}
.sfhero-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px 26px;
margin-top: 28px;
}
.sfhero-reviews {
display: inline-flex;
align-items: center;
gap: 9px;
text-decoration: none;
color: rgba(var(--white-rgb), 0.9);
transition: color var(--motion-fast);
}
.sfhero-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);
}
.sfhero-reviews-mark img {
display: block;
width: 15px;
height: auto;
}
.sfhero-reviews-stars {
display: inline-flex;
gap: 2px;
color: var(--yellow);
font-size: 11px;
}
.sfhero-reviews-label {
font-size: 13px;
font-weight: 600;
color: rgba(var(--white-rgb), 0.9);
}
@media (hover: hover) {
.sfhero-reviews:hover {
color: var(--text-inverse);
}
}
.sfhero-reviews:focus-visible {
outline: 2px solid var(--yellow);
outline-offset: 3px;
border-radius: var(--radius-sm);
}
/* Media */
.sfhero-media {
grid-area: media;
position: relative;
justify-self: end;
width: 100%;
max-width: 460px;
}
.sfhero-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;
}
.sfhero-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);
}
.sfhero-photo img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 28%;
}
/* ── Hero quick-picks ────────────────────────────────────────────────── */
.sfhero-picks {
grid-area: picks;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
padding-top: clamp(8px, 1.5vw, 16px);
border-top: 1px solid rgba(var(--white-rgb), 0.12);
}
.sfhero-pick {
display: flex;
align-items: center;
gap: 14px;
padding: 16px 18px;
border-radius: 18px;
background: rgba(var(--white-rgb), 0.05);
box-shadow: inset 0 0 0 1px rgba(var(--white-rgb), 0.1);
color: var(--text-inverse);
text-decoration: none;
transition:
background 0.22s ease,
box-shadow 0.22s ease,
transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
}
.sfhero-pick-glyph {
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
flex: 0 0 auto;
border-radius: 13px;
background: rgba(var(--accent-rgb), 0.16);
color: var(--yellow);
font-size: 17px;
}
.sfhero-pick-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.sfhero-pick-title {
font-family: var(--font-head);
font-size: 15px;
font-weight: 700;
letter-spacing: -0.01em;
line-height: 1.2;
}
.sfhero-pick-price {
font-size: 12px;
font-weight: 600;
color: rgba(var(--white-rgb), 0.66);
}
:global(.sfhero-pick-arrow) {
font-size: 12px;
color: var(--yellow);
flex: 0 0 auto;
transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) {
.sfhero-pick:hover {
background: rgba(var(--white-rgb), 0.09);
box-shadow: inset 0 0 0 1px rgba(var(--white-rgb), 0.22);
transform: translateY(-2px);
}
.sfhero-pick:hover :global(.sfhero-pick-arrow) {
transform: translateX(3px);
}
}
.sfhero-pick:focus-visible {
outline: 2px solid var(--yellow);
outline-offset: 3px;
}
/* Entrance — same choreography as the clarity hero. */
@media (prefers-reduced-motion: no-preference) {
.sfhero-copy,
.sfhero-picks {
animation: sfheroRise 0.6s var(--ease-out-soft) both;
}
.sfhero-picks {
animation-delay: 0.2s;
}
.sfhero-photo {
animation: sfheroPhoto 0.8s var(--ease-out-soft) both;
}
}
@keyframes sfheroRise {
from {
opacity: 0;
transform: translateY(14px);
}
}
@keyframes sfheroPhoto {
from {
opacity: 0;
transform: scale(1.04);
}
}
/* ── Assurance bar ─────────────────────────────────────────────────────
A single calm strip of facts — the "say it plainly" beat that replaces a
long grid of value cards. Warm cream divider between services and proof. */
.sfproof {
background: var(--surface-panel-cream);
border-top: 1px solid var(--border-soft);
border-bottom: 1px solid var(--border-soft);
}
.sfproof-row {
list-style: none;
margin: 0 auto;
max-width: var(--max-w);
padding: clamp(18px, 2.4vw, 26px) var(--space-container-x);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 14px clamp(24px, 4vw, 52px);
}
.sfproof-item {
display: inline-flex;
align-items: center;
gap: 10px;
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--text-heading);
}
:global(.sfproof-icon) {
font-size: 15px;
color: var(--gw-green);
}
/* ── Closing band ──────────────────────────────────────────────────────
Brand-green field that repeats the three service choices. Mirrors the
service-card styling so the page closes on the same call it opened with. */
.sfclose {
position: relative;
overflow: hidden;
background: var(--gw-green);
color: var(--text-inverse);
padding: var(--space-section-support-y) 0;
}
.sfclose::before {
content: '';
position: absolute;
bottom: -30%;
left: -8%;
width: 55%;
height: 90%;
background: radial-gradient(closest-side, rgba(var(--accent-rgb), 0.14), transparent 70%);
pointer-events: none;
z-index: 0;
}
.sfclose-inner {
position: relative;
z-index: 1;
max-width: min(1180px, calc(var(--max-w) - 40px));
margin: 0 auto;
padding: 0 var(--space-container-x);
}
.sfclose-head {
max-width: 46ch;
margin: 0 auto clamp(28px, 4vw, 44px);
text-align: center;
}
.sfclose-title {
margin: 0 0 12px;
font-family: var(--font-head);
font-size: clamp(28px, 4vw, 44px);
font-weight: 800;
line-height: 1.05;
letter-spacing: -0.03em;
color: var(--text-inverse);
text-wrap: balance;
}
.sfclose-lead {
margin: 0;
font-size: clamp(15px, 1.3vw, 18px);
line-height: 1.6;
color: rgba(var(--white-rgb), 0.82);
}
.sfclose-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20px;
}
.sfclose-card {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 28px 24px;
border-radius: 22px;
background: rgba(var(--white-rgb), 0.05);
box-shadow: inset 0 0 0 1px rgba(var(--white-rgb), 0.1);
color: var(--text-inverse);
text-decoration: none;
transition:
background 0.24s ease,
box-shadow 0.24s ease,
transform 0.24s cubic-bezier(0.22, 1, 0.36, 1);
}
.sfclose-card-glyph {
display: inline-flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
margin-bottom: 18px;
border-radius: 15px;
background: rgba(var(--accent-rgb), 0.16);
color: var(--yellow);
font-size: 20px;
}
.sfclose-card-title {
font-family: var(--font-head);
font-size: 19px;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.2;
}
.sfclose-card-body {
margin-top: 8px;
font-size: 14px;
line-height: 1.6;
color: rgba(var(--white-rgb), 0.78);
}
.sfclose-card-price {
margin-top: 14px;
font-family: var(--font-head);
font-size: 13px;
font-weight: 700;
color: var(--yellow);
}
.sfclose-card-cta {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 20px;
padding-top: 18px;
width: 100%;
border-top: 1px solid rgba(var(--white-rgb), 0.12);
font-family: var(--font-head);
font-size: 13px;
font-weight: 700;
color: var(--text-inverse);
}
:global(.sfclose-card-arrow) {
font-size: 11px;
color: var(--yellow);
transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
@media (hover: hover) {
.sfclose-card:hover {
background: rgba(var(--white-rgb), 0.09);
box-shadow: inset 0 0 0 1px rgba(var(--white-rgb), 0.22);
transform: translateY(-4px);
}
.sfclose-card:hover :global(.sfclose-card-arrow) {
transform: translateX(4px);
}
}
.sfclose-card:focus-visible {
outline: 2px solid var(--yellow);
outline-offset: 3px;
}
/* ── Mobile ────────────────────────────────────────────────────────────
Same single-column logic as the clarity hero: message, photo, action,
then the picks stack full width. */
@media (max-width: 900px) {
.sfhero-picks {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.sfhero-inner {
display: flex;
flex-direction: column;
padding: 28px var(--space-container-x-mobile) 36px;
row-gap: 0;
}
.sfhero-copy {
order: 1;
}
.sfhero-media {
order: 2;
justify-self: stretch;
max-width: none;
margin: 24px 0 26px;
}
.sfhero-picks {
order: 3;
margin-top: 4px;
}
.sfhero-photo {
aspect-ratio: 5 / 4;
}
.sfhero-lead {
max-width: none;
}
.sfhero-actions .btn {
width: 100%;
}
.sfclose-grid {
grid-template-columns: 1fr;
gap: 14px;
}
}
</style>
+4 -55
View File
@@ -123,17 +123,6 @@
// Computed legacy field for downstream consumers expecting a single full name.
$: fullName = `${ownerFirstName.trim()} ${ownerLastName.trim()}`.trim();
$: displayName = ownerFirstName.trim() || (userEmail ? userEmail.split('@')[0] : '');
$: accountInitials = (() => {
const first = ownerFirstName.trim();
const last = ownerLastName.trim();
if (first || last) {
return `${first.charAt(0)}${last.charAt(0)}`.toUpperCase() || first.charAt(0).toUpperCase();
}
return (userEmail.charAt(0) || '?').toUpperCase();
})();
let formStartedAt = 0;
let visitStartedAt = 0;
let pageEnteredAt = 0;
@@ -703,12 +692,10 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
</a>
</div>
<div class="onboarding-topbar-right">
{#if isAuthenticated && displayName}
{#if isAuthenticated}
<div class="onboarding-account" aria-label="Account">
<span class="onboarding-account-avatar" aria-hidden="true">{accountInitials}</span>
<span class="onboarding-account-text">
<span class="onboarding-account-prefix">Logged in as</span>
<span class="onboarding-account-name">{displayName}</span>
<span class="onboarding-account-avatar" aria-hidden="true">
<Icon name="fas fa-user" />
</span>
<button
type="button"
@@ -749,8 +736,6 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
<h1>
{#if onboardingCompleted}
Your submitted onboarding details.
{:else if displayName}
Hi {displayName}, let's get your dog onboarded.
{:else}
A few details so we can care for your dog properly.
{/if}
@@ -1399,7 +1384,7 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
</div>
</section>
<OnboardingFooter email={userEmail} on:logout={handleLogout} />
<OnboardingFooter on:logout={handleLogout} />
{/if}
</main>
@@ -1504,33 +1489,6 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
box-shadow: inset 0 0 0 1px rgba(33, 48, 33, 0.12);
}
.onboarding-account-text {
display: inline-flex;
flex-direction: column;
line-height: 1.15;
min-width: 0;
}
.onboarding-account-prefix {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.62);
}
.onboarding-account-name {
font-family: var(--font-head);
font-size: 13px;
font-weight: 700;
letter-spacing: -0.005em;
color: #fff;
max-width: 22ch;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.onboarding-account-logout {
display: inline-flex;
align-items: center;
@@ -1575,15 +1533,6 @@ function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
font-size: 11px;
}
.onboarding-account-prefix {
display: none;
}
.onboarding-account-name {
font-size: 12px;
max-width: 12ch;
}
.onboarding-account-logout-label {
display: none;
}
@@ -0,0 +1,557 @@
<script lang="ts">
/*
* v5 clarity treatment for the pricing page. Keeps each service prominent,
* like the live PricingPage, but strips the message right back: one photo,
* the name, a single outcome line, the "from" price, then the even plan
* cards. No chooser grid, lede, assurance, fit lists, or meta rows.
*
* View component only. Preview at /variants/our-pricing.
*/
import Icon from '$lib/components/ui/Icon.svelte';
import { reveal } from '$lib/actions/reveal';
import ServiceHero from '$lib/components/sections/ServiceHero.svelte';
import PricingPlanCard from '$lib/components/ui/PricingPlanCard.svelte';
import ServiceAreaCoverage from '$lib/components/sections/ServiceAreaCoverage.svelte';
import { decoratePlans } from '$lib/utils/pricing';
import { ourPricingContent } from '$lib/content/our-pricing';
import { packWalksContent } from '$lib/content/pack-walks';
import { dogWalkingContent } from '$lib/content/dog-walking';
import { puppyVisitsContent } from '$lib/content/puppy-visits';
import type { PricingPageSection, ServicePageContent } from '$lib/types';
type Block = { section: PricingPageSection; service: ServicePageContent; href: string };
// Pair each pricing section with its service (for the distilled per-plan copy)
// and its variant page. Order matches ourPricingContent.sections.
const blocks: Block[] = [
{ section: ourPricingContent.sections[0], service: packWalksContent, href: '/pack-walks' },
{ section: ourPricingContent.sections[1], service: dogWalkingContent, href: '/dog-walking' },
{ section: ourPricingContent.sections[2], service: puppyVisitsContent, href: '/puppy-visits' }
];
function fromValue(section: PricingPageSection): string {
return section.meta?.find((m) => m.label === 'From')?.value ?? '';
}
function plansFor(block: Block) {
const lines = block.service.clarity?.planLines ?? {};
return decoratePlans(
block.section.plans.map((plan) => ({
...plan,
features: lines[plan.title] ?? plan.features
}))
);
}
// At-a-glance comparison. Distilled, accurate facts per service; the "from"
// price is read from content (fromFor) so it stays single-sourced with the
// cards below and the pricing in each service page's schema.
const comparisonRows = [
{
href: '/pack-walks',
service: 'Tiny Gang Pack Walks',
bestFor: 'Sociable small & medium dogs',
format: 'Small group of 48 dogs',
length: '6075 min'
},
{
href: '/dog-walking',
service: 'Solo Walks',
bestFor: 'Reactive, anxious, senior or recovering dogs',
format: 'One dog, one-on-one',
length: '30 / 45 / 60 min'
},
{
href: '/puppy-visits',
service: 'Puppy Visits',
bestFor: 'Puppies too young for full walks',
format: 'In-home, one-on-one',
length: '20 / 45 / 60 min'
}
];
function fromFor(href: string): string {
const block = blocks.find((b) => b.href === href);
return block ? fromValue(block.section) : '';
}
</script>
<main class="pc">
<!-- Standard service hero (green copy left, photo right). -->
<ServiceHero
eyebrow={ourPricingContent.eyebrow ?? 'Pricing'}
title={ourPricingContent.title}
subtitle={ourPricingContent.subtitle}
imageUrl={ourPricingContent.heroImageUrl}
imageAlt={ourPricingContent.heroImageAlt}
chips={ourPricingContent.heroChips ?? []}
cta={{ label: 'Book a free Meet & Greet', href: '/contact-us', variant: 'yellow' }}
/>
<!-- Value before price: one line, given room to land. -->
<section class="pc-value" use:reveal={{ distance: 0 }} aria-labelledby="pc-value-quote">
<div class="pc-inner">
<span class="pc-value-rule" aria-hidden="true"></span>
<p id="pc-value-quote" class="pc-value-quote">
The best investment you&rsquo;ll ever make is in your dog&rsquo;s happiness.
</p>
</div>
</section>
<!-- At a glance: a compact comparison of the three services before the
detailed per-service blocks. A real table, so it reads cleanly and is
easy to lift as a unit. -->
<section class="pc-compare" use:reveal={{ distance: 0 }} aria-labelledby="pc-compare-heading">
<div class="pc-inner">
<div class="pc-compare-head">
<p class="pc-eyebrow">Compare at a glance</p>
<h2 id="pc-compare-heading" class="pc-h2">Three ways Goodwalk fits your dog</h2>
</div>
<div class="pc-table-wrap">
<table class="pc-table">
<thead>
<tr>
<th scope="col">Service</th>
<th scope="col">Best for</th>
<th scope="col">Format</th>
<th scope="col">Length</th>
<th scope="col">From</th>
</tr>
</thead>
<tbody>
{#each comparisonRows as row}
<tr>
<th scope="row"><a href={row.href}>{row.service}</a></th>
<td>{row.bestFor}</td>
<td>{row.format}</td>
<td>{row.length}</td>
<td class="pc-table-from">{fromFor(row.href)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</section>
<!-- One prominent block per service: photo + simplified message, then cards. -->
{#each blocks as block, i}
<section
class="pc-service"
class:pc-service-tint={i % 2 === 1}
use:reveal={{ distance: 0 }}
aria-labelledby={`pc-svc-${i}`}
>
<div class="pc-inner">
<div class="pc-row" class:pc-row-reverse={i % 2 === 1}>
{#if block.section.imageUrl}
<figure class="pc-media">
<img
src={block.section.imageUrl}
alt={block.section.imageAlt ?? block.section.title}
loading="lazy"
decoding="async"
/>
</figure>
{/if}
<div class="pc-copy">
{#if block.section.eyebrow}
<p class="pc-eyebrow">{block.section.eyebrow}</p>
{/if}
<h2 id={`pc-svc-${i}`} class="pc-h2">{block.section.title}</h2>
{#if block.section.outcome}
<p class="pc-outcome">{block.section.outcome}</p>
{/if}
{#if fromValue(block.section)}
<p class="pc-from"><span>From</span> <strong>{fromValue(block.section)}</strong></p>
{/if}
<a class="pc-link" href={block.href}>
See {block.section.title}
<Icon name="fas fa-arrow-right" />
</a>
</div>
</div>
<div class="pc-plans" class:pc-plans-three={block.section.plans.length === 3}>
{#each plansFor(block) as plan}
<PricingPlanCard {plan} variant="service" hideCtaOnMobile={true} />
{/each}
</div>
</div>
</section>
{/each}
<!-- Coverage -->
<div class="pc-area" use:reveal={{ distance: 0 }}>
<ServiceAreaCoverage
title="Where we work across Auckland Central"
lead="Free door-to-door pickup and drop-off on walks across Auckland Central and the suburbs around it. Tap your suburb for its local parks and routes."
/>
</div>
<!-- Close -->
<section class="pc-cta" use:reveal={{ distance: 0 }}>
<div class="pc-inner">
<h2 class="pc-cta-title">Not sure which one fits?</h2>
<p class="pc-cta-sub">Every Goodwalk routine starts with a free Meet &amp; Greet. We will tell you what fits, and what does not.</p>
<a class="btn btn-yellow" href="/contact-us">Book a free Meet &amp; Greet</a>
</div>
</section>
</main>
<style>
.pc {
--pc-max: 1180px;
background: var(--off-white);
color: var(--text-heading);
}
.pc-inner {
max-width: var(--pc-max);
margin: 0 auto;
padding-inline: var(--space-container-x);
}
.pc-service {
padding-block: clamp(48px, 6vw, 88px);
}
/* ── Value statement: one line, given room to land ── */
.pc-value {
padding-block: clamp(64px, 9vw, 128px);
background: var(--surface-panel-cream);
border-block: 1px solid var(--border-soft);
text-align: center;
}
.pc-value-rule {
display: block;
width: 40px;
height: 3px;
margin: 0 auto 30px;
border-radius: 3px;
background: var(--yellow);
}
.pc-value-quote {
margin: 0 auto;
max-width: 16ch;
font-family: var(--font-head);
font-size: clamp(30px, 4.8vw, 56px);
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.03em;
color: var(--gw-green);
text-wrap: balance;
}
/* ── At-a-glance comparison table ── */
.pc-compare {
padding-block: clamp(48px, 6vw, 88px);
}
.pc-compare-head {
max-width: 40rem;
margin: 0 auto clamp(28px, 4vw, 44px);
text-align: center;
}
.pc-compare-head .pc-eyebrow {
justify-content: center;
}
/* Scrolls on narrow screens rather than cramping five columns. */
.pc-table-wrap {
overflow-x: auto;
border-radius: var(--radius-xl);
box-shadow: var(--shadow-card);
-webkit-overflow-scrolling: touch;
}
.pc-table {
width: 100%;
min-width: 760px;
border-collapse: collapse;
background: var(--surface-panel);
text-align: left;
}
.pc-table thead th {
padding: 16px 20px;
background: var(--gw-green);
color: rgba(255, 248, 230, 0.96);
font-family: var(--font-head);
font-size: 14px;
font-weight: 700;
white-space: nowrap;
}
.pc-table tbody th,
.pc-table tbody td {
padding: 18px 20px;
border-top: 1px solid var(--border-soft);
font-size: 15px;
line-height: 1.5;
color: var(--text-heading-soft);
vertical-align: top;
}
.pc-table tbody th {
font-family: var(--font-head);
font-weight: 700;
color: var(--text-heading);
}
.pc-table tbody th a {
color: var(--gw-green);
text-decoration: none;
}
.pc-table tbody th a:hover {
text-decoration: underline;
}
.pc-table-from {
font-family: var(--font-head);
font-weight: 700;
color: var(--gw-green);
white-space: nowrap;
}
/* Alternating tint for rhythm, the same trick the service pages use. */
.pc-service-tint,
.pc-area {
background: var(--surface-panel-cream);
border-block: 1px solid var(--border-soft);
}
/* ── Prominent service row: photo + simplified message ── */
.pc-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: clamp(28px, 5vw, 64px);
align-items: center;
margin-bottom: clamp(32px, 4.5vw, 56px);
}
.pc-media {
order: 0;
margin: 0;
border-radius: var(--radius-xl);
overflow: hidden;
box-shadow: var(--shadow-card);
background: var(--surface-brand);
aspect-ratio: 4 / 3;
}
.pc-row-reverse .pc-media {
order: 1;
}
.pc-media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.pc-copy {
order: 1;
}
.pc-row-reverse .pc-copy {
order: 0;
}
.pc-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);
}
.pc-eyebrow::before {
content: '';
width: 24px;
height: 2px;
border-radius: 2px;
background: var(--yellow);
}
.pc-h2 {
margin: 0;
font-family: var(--font-head);
font-size: clamp(28px, 3.4vw, 44px);
font-weight: 700;
line-height: 1.06;
letter-spacing: -0.03em;
color: var(--text-heading);
text-wrap: balance;
}
/* The simplified message: one line, carried with weight. */
.pc-outcome {
margin: 16px 0 0;
max-width: 34ch;
font-family: var(--font-head);
font-size: clamp(18px, 1.9vw, 23px);
font-weight: 600;
line-height: 1.32;
letter-spacing: -0.01em;
color: var(--gw-green);
text-wrap: balance;
}
.pc-from {
display: inline-flex;
align-items: baseline;
gap: 7px;
margin: 22px 0 0;
padding: 9px 16px;
border-radius: var(--radius-pill);
background: var(--surface-brand-soft);
color: var(--gw-green);
font-size: 15px;
}
.pc-from span {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.8;
}
.pc-from strong {
font-family: var(--font-head);
font-weight: 700;
}
.pc-link {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 22px;
font-family: var(--font-head);
font-size: 15px;
font-weight: 700;
color: var(--gw-green);
text-decoration: none;
}
.pc-from + .pc-link {
margin-left: 16px;
}
.pc-link:hover {
text-decoration: underline;
}
/* ── Plans ── */
.pc-plans {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 24px;
}
.pc-plans-three {
grid-template-columns: repeat(3, minmax(0, 1fr));
max-width: 1040px;
margin-inline: auto;
}
.pc-plans :global(.plan-card) {
padding: 40px 28px 34px;
}
.pc-plans :global(.plan-card--featured) {
padding-top: 60px;
}
.pc-plans :global(.plan-card__cta) {
display: flex;
width: 100%;
min-width: 0;
align-self: stretch;
padding-inline: 12px;
}
.pc-plans :global(.plan-card__cta .icon) {
display: none;
}
/* ── Close CTA ── */
.pc-cta {
padding-block: clamp(48px, 6vw, 88px);
background: var(--gw-green);
color: rgba(255, 248, 230, 0.96);
text-align: center;
}
.pc-cta-title {
margin: 0;
font-family: var(--font-head);
font-size: clamp(26px, 3.2vw, 40px);
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--yellow);
text-wrap: balance;
}
.pc-cta-sub {
margin: 14px auto 24px;
max-width: 38rem;
font-size: 16px;
line-height: 1.6;
color: rgba(255, 248, 230, 0.9);
}
/* ── Responsive ── */
@media (max-width: 860px) {
.pc-row {
grid-template-columns: 1fr;
gap: clamp(20px, 5vw, 32px);
}
.pc-media,
.pc-row-reverse .pc-media {
order: 0;
aspect-ratio: 16 / 10;
}
.pc-copy,
.pc-row-reverse .pc-copy {
order: 1;
}
}
@media (max-width: 980px) {
.pc-plans,
.pc-plans-three {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.pc-plans,
.pc-plans-three {
grid-template-columns: 1fr;
}
.pc-plans :global(.plan-card) {
padding: 30px 24px 26px;
}
.pc-plans :global(.plan-card--featured) {
padding-top: 46px;
}
}
</style>
@@ -0,0 +1,725 @@
<script lang="ts">
/*
* Shared v5 "clarity" service page. One design system, one message, said
* once in order: what it is -> what's different -> how it works -> fit ->
* proof -> price -> FAQ -> where we cover -> book.
*
* Drives all three service pages (Tiny Gang, Solo Walks, Puppy Visits) from
* content. Page-specific copy lives in each content file's `clarity` block;
* see docs/v5.md. View component only: the route owns Header/Footer/SEO.
*/
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 ServiceAreaCoverage from '$lib/components/sections/ServiceAreaCoverage.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 = '/';
$: clarity = pageContent.clarity;
$: traits = clarity?.traits ?? [];
$: steps = clarity?.steps ?? [];
$: planLines = clarity?.planLines ?? {};
$: priceNotes = clarity?.priceNotes ?? [];
$: 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: shared service-page hero (green copy left, photo bleeds right). -->
<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 it is: a plain, self-contained definition that opens the page. -->
{#if clarity?.definition}
<section class="tg-section tg-section-def" use:reveal={{ distance: 0 }} aria-labelledby="tg-def-heading">
<div class="tg-inner tg-inner-narrow">
<div class="tg-head">
<p class="tg-eyebrow">{clarity.definition.eyebrow ?? 'In plain terms'}</p>
<h2 id="tg-def-heading" class="tg-h2">{clarity.definition.heading}</h2>
</div>
<p class="tg-def-body">{clarity.definition.body}</p>
</div>
</section>
{/if}
<!-- What makes it different (said once) -->
{#if traits.length}
<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">{clarity?.diffEyebrow ?? 'What makes it different'}</p>
<h2 id="tg-diff-heading" class="tg-h2">{clarity?.diffHeading ?? ''}</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>
{/if}
<!-- How it works (the process, explicit) -->
{#if steps.length}
<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">{clarity?.stepsEyebrow ?? 'How it works'}</p>
<h2 id="tg-steps-heading" class="tg-h2">{clarity?.stepsHeading ?? ''}</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>
{/if}
<!-- Image beat: one deliberate photo that breaks the longest text run. -->
{#if clarity?.imageBand}
<div class="tg-band-wrap" use:reveal={{ distance: 0 }}>
<figure class="tg-band">
<img
class="tg-band-img"
src={clarity.imageBand.imageUrl}
alt={clarity.imageBand.imageAlt}
loading="lazy"
decoding="async"
/>
<figcaption class="tg-band-cap">{clarity.imageBand.caption}</figcaption>
</figure>
</div>
{/if}
<!-- 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">{clarity?.fitEyebrow ?? 'Honest fit check'}</p>
<h2 id="tg-fit-heading" class="tg-h2">{pageContent.decision.title ?? 'Is this 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 with another option 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 -->
<TestimonialsSection
heading={clarity?.testimonialsHeading ?? pageContent.testimonialsHeading}
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">{clarity?.pricingEyebrow ?? 'Simple pricing'}</p>
<h2 id="tg-price-heading" class="tg-h2">{pageContent.pricing.title}</h2>
{#if clarity?.pricingTagline}
<p class="tg-price-tagline">{clarity.pricingTagline}</p>
{/if}
{#if pageContent.pricing.intro}
<p class="tg-head-sub">{pageContent.pricing.intro}</p>
{/if}
</div>
<div class="tg-plans" class:tg-plans-three={pricingPlans.length === 3}>
{#each pricingPlans as plan}
<PricingPlanCard {plan} variant="service" hideCtaOnMobile={true} />
{/each}
</div>
{#if pageContent.pricing.scarcityNote || priceNotes.length}
<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}
{#each priceNotes as note}
<p class="tg-note"><Icon name={note.icon} />{note.label}</p>
{/each}
</div>
{/if}
<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 }} aria-labelledby="tg-faq-heading">
<div class="tg-inner tg-inner-narrow">
<div class="tg-head">
<p class="tg-eyebrow">{clarity?.faqEyebrow ?? 'Common questions'}</p>
<h2 id="tg-faq-heading" class="tg-h2">{pageContent.faq?.title ?? 'FAQs'}</h2>
{#if pageContent.faq?.intro}
<p class="tg-head-sub">{pageContent.faq.intro}</p>
{/if}
</div>
<div class="tg-faq">
<FaqSection title={pageContent.faq?.title ?? 'FAQs'} faqs={faqItems} showHeading={false} />
</div>
</div>
</section>
{/if}
<!-- Service area: a coverage field that works on every screen. -->
<div class="tg-section-area" use:reveal={{ distance: 0 }}>
<ServiceAreaCoverage
title={clarity?.areaTitle ?? 'Where we walk across Auckland Central'}
lead={clarity?.areaLead ??
'Free door-to-door pickup and drop-off across Auckland Central and the suburbs around it. Tap your suburb for its local parks and routes.'}
stats={clarity?.areaStats ?? null}
/>
</div>
<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,
.tg-section-area {
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;
}
/* ── Definition lede: one calm, readable paragraph under the opening head ── */
.tg-section-def {
padding-bottom: clamp(8px, 2vw, 24px);
}
.tg-def-body {
max-width: 46rem;
margin: 0 auto;
color: var(--text-heading-soft);
font-size: clamp(17px, 1.7vw, 19px);
line-height: 1.7;
text-align: center;
text-wrap: pretty;
}
.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;
}
/* ── Image beat: a contained cinematic band between sections ───────────── */
.tg-band-wrap {
max-width: var(--tg-max);
margin: 0 auto;
padding-inline: var(--space-container-x);
padding-block: clamp(8px, 2vw, 28px);
}
.tg-band {
position: relative;
margin: 0;
border-radius: var(--radius-xl);
overflow: hidden;
box-shadow: var(--shadow-card);
background: var(--surface-brand);
}
.tg-band-img {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 7;
object-fit: cover;
object-position: center 42%;
}
/* Caption rides on the photo: legible over the image, keyword-bearing copy
that reads as a moment, not a label. */
.tg-band-cap {
position: absolute;
inset-inline: 0;
bottom: 0;
margin: 0;
padding: clamp(28px, 5vw, 44px) clamp(18px, 4vw, 32px) clamp(16px, 2.4vw, 22px);
background: linear-gradient(to top, rgba(33, 48, 33, 0.78), rgba(33, 48, 33, 0));
color: rgba(255, 248, 230, 0.96);
font-family: var(--font-head);
font-size: clamp(15px, 1.8vw, 19px);
font-weight: 600;
line-height: 1.35;
letter-spacing: -0.01em;
text-wrap: balance;
}
/* ── Pricing ───────────────────────────────────────────────────────── */
.tg-plans {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 24px;
}
/* Three-plan pages (solo walks, puppy visits) centre nicely at three-up. */
.tg-plans-three {
grid-template-columns: repeat(3, minmax(0, 1fr));
max-width: 1040px;
margin-inline: auto;
}
/* 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 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: 12px;
}
/* The narrow 4-up cards can't fit "Book a Meet & Greet" plus the arrow on
one line, and the card clips overflow — so the label was chopped. Drop the
arrow here and let the label use the whole width on its own. */
.tg-plans :global(.plan-card__cta .icon) {
display: none;
}
.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;
}
/* ── FAQ: a calm ruled list, not a stack of cards ──────────────────────
The shared FaqSection renders each question as a bordered, shadowed white
card. On this off-white page that reads as visual noise. Here we strip the
card chrome to hairline dividers so the questions sit as one quiet column,
matching the page's restraint. Scoped via .tg-faq; every other page keeps
the card treatment. */
.tg-faq :global(.faq details) {
border: 0;
border-top: 1px solid var(--border-soft);
border-radius: 0;
background: transparent;
margin: 0;
box-shadow: none;
}
.tg-faq :global(.faq details:last-child) {
border-bottom: 1px solid var(--border-soft);
}
.tg-faq :global(.faq details[open]) {
box-shadow: none;
}
/* Full-bleed rows: no card inset, generous vertical breathing room. */
.tg-faq :global(.faq summary) {
padding: 22px 4px;
border-radius: 0;
font-size: 17px;
color: var(--text-heading);
transition: color 0.18s ease;
}
/* Hover lifts the question to brand green instead of filling a card. */
.tg-faq :global(.faq summary:hover) {
background: transparent;
color: var(--gw-green);
}
.tg-faq :global(.faq details[open] summary) {
border-bottom: 0;
border-radius: 0;
color: var(--gw-green);
}
.tg-faq :global(.faq details p) {
padding: 0 44px 24px 4px;
}
/* ── Responsive ────────────────────────────────────────────────────── */
@media (max-width: 980px) {
.tg-plans,
.tg-plans-three {
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-band-img {
aspect-ratio: 3 / 2;
}
.tg-plans,
.tg-plans-three {
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>