Content Rewrite
This commit is contained in:
@@ -18,9 +18,9 @@
|
||||
<h1>Contact Us</h1>
|
||||
<p class="booking-page-sub">
|
||||
{#if allowGeneralEnquiry}
|
||||
Fill in the form below to book a Meet & Greet or send a general enquiry.
|
||||
Book a Meet & Greet or send a general enquiry. We’ll come back within 24 hours.
|
||||
{:else}
|
||||
Fill in the form below and we'll be in touch to arrange a free introduction.
|
||||
Tell us a little about your dog and we’ll be in touch within 24 hours to arrange a free Meet & Greet.
|
||||
{/if}
|
||||
</p>
|
||||
<div class="booking-page-contact">
|
||||
@@ -46,7 +46,7 @@
|
||||
}
|
||||
|
||||
.booking-page-hero {
|
||||
background: var(--green);
|
||||
background: var(--gw-green);
|
||||
color: #fff;
|
||||
padding: 64px 0 72px;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,38 @@
|
||||
type EnquiryType = 'booking' | 'general';
|
||||
const visitStartedStorageKey = 'goodwalk_visit_started_at';
|
||||
const journeyStorageKey = 'goodwalk_journey';
|
||||
const requestedServiceStorageKey = 'goodwalk_requested_service';
|
||||
const maxJourneyEntries = 8;
|
||||
const servicePrompts: Record<
|
||||
string,
|
||||
{
|
||||
intro: string;
|
||||
messageLabel: string;
|
||||
messagePlaceholder: string;
|
||||
}
|
||||
> = {
|
||||
'Pack Walks': {
|
||||
intro:
|
||||
'Tell us about your dog, your area, and how they feel around other dogs so we can see if Pack Walks are the right fit.',
|
||||
messageLabel: 'Pack Walks fit',
|
||||
messagePlaceholder:
|
||||
'How old is your dog, how do they feel in groups, and is there anything about confidence, recall, or social behaviour we should know?'
|
||||
},
|
||||
'1:1 Walks': {
|
||||
intro:
|
||||
'Tell us about your dog, your area, and what you want from one-on-one walks so we can plan the right routine.',
|
||||
messageLabel: '1:1 walk needs',
|
||||
messagePlaceholder:
|
||||
'Tell us about your dog’s size, pace, leash manners, confidence, and anything else that would help us tailor a one-on-one walk.'
|
||||
},
|
||||
'Puppy Visits': {
|
||||
intro:
|
||||
'Tell us about your puppy, your area, and the kind of support you need at home so we can plan the right visit.',
|
||||
messageLabel: 'Puppy visit details',
|
||||
messagePlaceholder:
|
||||
'Tell us your puppy’s age, routine, toilet needs, feeding schedule, and anything important we should know before visiting.'
|
||||
}
|
||||
};
|
||||
|
||||
let step = 1;
|
||||
$: headingParts = splitBookingTitle(booking.title);
|
||||
@@ -76,7 +107,9 @@
|
||||
const defaultGeneralSubtitle =
|
||||
'Almost there — just your contact details so we can reply properly to your message.';
|
||||
|
||||
$: dogIntro = booking.dogIntro?.trim() || defaultDogIntro;
|
||||
$: primarySelectedService = selectedServices[0] ?? '';
|
||||
$: activeServicePrompt = servicePrompts[primarySelectedService];
|
||||
$: dogIntro = activeServicePrompt?.intro || booking.dogIntro?.trim() || defaultDogIntro;
|
||||
$: generalIntro = booking.generalIntro?.trim() || defaultGeneralIntro;
|
||||
$: hasServices = booking.serviceOptions.length > 0;
|
||||
$: if (!allowGeneralEnquiry && enquiryType === 'general') {
|
||||
@@ -90,6 +123,16 @@
|
||||
$: dogStepLabel = booking.dogStepLabel?.trim() || 'Your dog';
|
||||
$: detailsStepLabel = isGeneralEnquiry ? 'Your enquiry' : dogStepLabel;
|
||||
$: detailsStepIntro = isGeneralEnquiry ? generalIntro : dogIntro;
|
||||
$: bookingEyebrow = isGeneralEnquiry ? 'Friendly contact' : primarySelectedService || 'Free Meet & Greet';
|
||||
$: bookingIntro = isGeneralEnquiry
|
||||
? 'Send us the details and we’ll point you in the right direction.'
|
||||
: 'Tell us a little about your dog first. We’ll come back within 24 hours with the right next step.';
|
||||
$: detailsMessageLabel = isGeneralEnquiry
|
||||
? 'Your Message'
|
||||
: activeServicePrompt?.messageLabel || 'About Your Dog';
|
||||
$: detailsMessagePlaceholder = isGeneralEnquiry
|
||||
? 'Tell us if this is feedback, a complaint, a business enquiry, or anything else we should know.'
|
||||
: activeServicePrompt?.messagePlaceholder || 'Describe your pet, any special needs, or anything we should know.';
|
||||
$: successPetName = petName.trim() || 'your dog';
|
||||
|
||||
onMount(() => {
|
||||
@@ -98,6 +141,19 @@
|
||||
pageEnteredAt = now;
|
||||
visitStartedAt = readOrCreateVisitStartedAt(now);
|
||||
journey = updateJourneySnapshot(window.location.pathname, window.location.search);
|
||||
|
||||
applyRequestedService();
|
||||
|
||||
const handleRequestedService = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ service?: string }>;
|
||||
applyRequestedService(customEvent.detail?.service?.trim());
|
||||
};
|
||||
|
||||
window.addEventListener('goodwalk:service-selected', handleRequestedService as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('goodwalk:service-selected', handleRequestedService as EventListener);
|
||||
};
|
||||
});
|
||||
|
||||
function splitBookingTitle(title: string) {
|
||||
@@ -169,11 +225,35 @@
|
||||
errors = {};
|
||||
}
|
||||
|
||||
function applyRequestedService(service?: string) {
|
||||
const requestedService =
|
||||
service ||
|
||||
(() => {
|
||||
try {
|
||||
return window.sessionStorage.getItem(requestedServiceStorageKey)?.trim() || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
|
||||
if (!requestedService || !booking.serviceOptions.includes(requestedService)) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedServices = [requestedService];
|
||||
|
||||
try {
|
||||
window.sessionStorage.removeItem(requestedServiceStorageKey);
|
||||
} catch {
|
||||
// Ignore storage cleanup failures.
|
||||
}
|
||||
}
|
||||
|
||||
function toggleService(service: string, checked: boolean) {
|
||||
noteInteraction();
|
||||
|
||||
if (checked) {
|
||||
selectedServices = [...selectedServices, service];
|
||||
selectedServices = [service, ...selectedServices.filter((item) => item !== service)];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,10 +422,22 @@
|
||||
{/if}
|
||||
|
||||
<div class="booking-header">
|
||||
<span class="booking-eyebrow">{bookingEyebrow}</span>
|
||||
<h2 class="booking-title">
|
||||
<span class="booking-title-plain">{headingParts.plain}</span>{' '}
|
||||
<span class="booking-title-highlight">{headingParts.highlight}</span>
|
||||
</h2>
|
||||
<p class="booking-intro">{bookingIntro}</p>
|
||||
<div class="booking-trust-row" aria-label="Booking highlights">
|
||||
<span class="booking-trust-chip">
|
||||
<Icon name="fas fa-comment-dots" />
|
||||
Reply within 24 hours
|
||||
</span>
|
||||
<span class="booking-trust-chip">
|
||||
<Icon name="fas fa-paw" />
|
||||
Free, no-obligation Meet & Greet
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="booking-stepper" aria-label="Booking form steps">
|
||||
<button
|
||||
@@ -438,113 +530,138 @@
|
||||
{/if}
|
||||
|
||||
{#if !isGeneralEnquiry}
|
||||
<div class="booking-field-card" class:booking-field-card-invalid={errors.petName}>
|
||||
<label for="petName">
|
||||
<Icon name="fas fa-dog" /> Dog's Name <span class="booking-required">*</span>
|
||||
</label>
|
||||
<input
|
||||
bind:this={petNameInput}
|
||||
bind:value={petName}
|
||||
type="text"
|
||||
id="petName"
|
||||
name="petName"
|
||||
required
|
||||
placeholder="Your dog's name"
|
||||
class:input-invalid={errors.petName}
|
||||
on:input={() => clearError('petName')}
|
||||
/>
|
||||
{#if errors.petName}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.petName}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="booking-field-card booking-field-card-group booking-field-card-full">
|
||||
<div class="booking-field-group booking-field-group-dog">
|
||||
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.petName}>
|
||||
<label for="petName">
|
||||
<Icon name="fas fa-dog" /> Dog's Name <span class="booking-required">*</span>
|
||||
</label>
|
||||
<input
|
||||
bind:this={petNameInput}
|
||||
bind:value={petName}
|
||||
type="text"
|
||||
id="petName"
|
||||
name="petName"
|
||||
required
|
||||
placeholder="Your dog's name"
|
||||
class:input-invalid={errors.petName}
|
||||
on:input={() => clearError('petName')}
|
||||
/>
|
||||
{#if errors.petName}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.petName}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.location}>
|
||||
<label for="location">
|
||||
<Icon name="fas fa-location-dot" /> Location <span class="booking-required">*</span>
|
||||
</label>
|
||||
<input
|
||||
bind:this={locationInput}
|
||||
bind:value={location}
|
||||
type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
required
|
||||
placeholder="Suburb, street..."
|
||||
class:input-invalid={errors.location}
|
||||
on:input={() => clearError('location')}
|
||||
/>
|
||||
{#if errors.location}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.location}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="booking-field-stack booking-field-stack-full"
|
||||
class:booking-field-stack-invalid={errors.message}
|
||||
>
|
||||
<label for="message">
|
||||
<Icon name="fas fa-comment" /> {detailsMessageLabel}
|
||||
</label>
|
||||
<textarea
|
||||
bind:value={message}
|
||||
id="message"
|
||||
name="message"
|
||||
rows="4"
|
||||
placeholder={detailsMessagePlaceholder}
|
||||
class:input-invalid={errors.message}
|
||||
on:input={() => clearError('message')}
|
||||
></textarea>
|
||||
{#if errors.message}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.message}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if hasServices}
|
||||
<div class="booking-field-stack booking-field-stack-full">
|
||||
<span class="booking-service-label"><Icon name="fas fa-paw" /> Services</span>
|
||||
<div class="booking-service-options">
|
||||
{#each booking.serviceOptions as service}
|
||||
<label class="booking-check-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="services"
|
||||
value={service}
|
||||
checked={selectedServices.includes(service)}
|
||||
on:change={(event) =>
|
||||
toggleService(service, (event.currentTarget as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span class="booking-check-box" aria-hidden="true"></span>
|
||||
<span>{service}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isGeneralEnquiry}
|
||||
<div
|
||||
class="booking-field-card booking-field-card-wide"
|
||||
class:booking-field-card-invalid={errors.location}
|
||||
class="booking-field-card booking-field-card-full"
|
||||
class:booking-field-card-invalid={errors.message}
|
||||
>
|
||||
<label for="location">
|
||||
<Icon name="fas fa-location-dot" /> Location <span class="booking-required">*</span>
|
||||
<label for="message">
|
||||
<Icon name="fas fa-comment" /> {detailsMessageLabel}
|
||||
<span class="booking-required">*</span>
|
||||
</label>
|
||||
<input
|
||||
bind:this={locationInput}
|
||||
bind:value={location}
|
||||
type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
required
|
||||
placeholder="Suburb, street..."
|
||||
class:input-invalid={errors.location}
|
||||
on:input={() => clearError('location')}
|
||||
/>
|
||||
{#if errors.location}
|
||||
<textarea
|
||||
bind:value={message}
|
||||
id="message"
|
||||
name="message"
|
||||
rows="4"
|
||||
placeholder={detailsMessagePlaceholder}
|
||||
class:input-invalid={errors.message}
|
||||
on:input={() => clearError('message')}
|
||||
></textarea>
|
||||
{#if errors.message}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.location}
|
||||
{errors.message}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="booking-field-card booking-field-card-full"
|
||||
class:booking-field-card-invalid={errors.message}
|
||||
>
|
||||
<label for="message">
|
||||
<Icon name="fas fa-comment" /> {isGeneralEnquiry ? 'Your Message' : 'About Your Dog'}
|
||||
{#if isGeneralEnquiry}<span class="booking-required">*</span>{/if}
|
||||
</label>
|
||||
<textarea
|
||||
bind:value={message}
|
||||
id="message"
|
||||
name="message"
|
||||
rows="4"
|
||||
placeholder={isGeneralEnquiry
|
||||
? 'Tell us if this is feedback, a complaint, a business enquiry, or anything else we should know.'
|
||||
: 'Describe your pet, any special needs, or anything we should know.'}
|
||||
class:input-invalid={errors.message}
|
||||
on:input={() => clearError('message')}
|
||||
></textarea>
|
||||
{#if errors.message}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.message}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if hasServices && !isGeneralEnquiry}
|
||||
<div class="booking-service-row">
|
||||
<span class="booking-service-label"><Icon name="fas fa-paw" /> Services</span>
|
||||
<div class="booking-service-options">
|
||||
{#each booking.serviceOptions as service}
|
||||
<label class="booking-check-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="services"
|
||||
value={service}
|
||||
checked={selectedServices.includes(service)}
|
||||
on:change={(event) =>
|
||||
toggleService(service, (event.currentTarget as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span class="booking-check-box" aria-hidden="true"></span>
|
||||
<span>{service}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="booking-actions booking-actions-next">
|
||||
<button type="button" class="btn btn-yellow booking-next-button" on:click={goToOwnerStep}>
|
||||
{ownerStepLabel}
|
||||
Next: {ownerStepLabel.toLowerCase()}
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</button>
|
||||
<p class="booking-next-note">Response from us within 24 hours</p>
|
||||
<p class="booking-next-note">No payment, no pressure, just the right starting point for your dog.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<input type="hidden" name="fullName" value={fullName} />
|
||||
@@ -652,7 +769,13 @@
|
||||
Back
|
||||
</button>
|
||||
<button type="submit" class="btn btn-yellow booking-submit-button" disabled={submitting}>
|
||||
{#if submitting}Sending…{:else}Send <Icon name="fas fa-arrow-right" />{/if}
|
||||
{#if submitting}
|
||||
Sending…
|
||||
{:else if isGeneralEnquiry}
|
||||
Send enquiry <Icon name="fas fa-arrow-right" />
|
||||
{:else}
|
||||
Request Meet & Greet <Icon name="fas fa-arrow-right" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
margin: 0 0 12px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #213021;
|
||||
color: var(--gw-green);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
border-radius: 14px;
|
||||
background: #f7f6f1;
|
||||
border: 1px solid #ebe9df;
|
||||
color: #213021;
|
||||
color: var(--gw-green);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
@@ -187,7 +187,7 @@
|
||||
.modal-email-address {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #213021;
|
||||
color: var(--gw-green);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
}
|
||||
|
||||
.modal-btn-primary {
|
||||
background: #213021;
|
||||
background: var(--gw-green);
|
||||
color: #ffd100;
|
||||
}
|
||||
|
||||
@@ -237,13 +237,13 @@
|
||||
|
||||
.modal-btn-secondary {
|
||||
background: transparent;
|
||||
color: #213021;
|
||||
color: var(--gw-green);
|
||||
border: 1px solid #d4d2c6;
|
||||
}
|
||||
|
||||
.modal-btn-secondary:hover {
|
||||
background: #f2f2f0;
|
||||
border-color: #213021;
|
||||
border-color: var(--gw-green);
|
||||
}
|
||||
|
||||
@keyframes backdrop-in {
|
||||
|
||||
@@ -25,6 +25,14 @@
|
||||
connector: ''
|
||||
};
|
||||
}
|
||||
|
||||
function linkTarget(external?: boolean) {
|
||||
return external ? '_blank' : undefined;
|
||||
}
|
||||
|
||||
function linkRel(external?: boolean) {
|
||||
return external ? 'noopener' : undefined;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section id="hero">
|
||||
@@ -71,8 +79,22 @@
|
||||
{/if}
|
||||
|
||||
<div class="hero-buttons">
|
||||
<a href={hero.primaryCta.href} class="btn btn-yellow">{hero.primaryCta.label}</a>
|
||||
<a href={hero.secondaryCta.href} class="btn btn-outline">{hero.secondaryCta.label}</a>
|
||||
<a
|
||||
href={hero.primaryCta.href}
|
||||
target={linkTarget(hero.primaryCta.external)}
|
||||
rel={linkRel(hero.primaryCta.external)}
|
||||
class="btn btn-yellow"
|
||||
>
|
||||
{hero.primaryCta.label}
|
||||
</a>
|
||||
<a
|
||||
href={hero.secondaryCta.href}
|
||||
target={linkTarget(hero.secondaryCta.external)}
|
||||
rel={linkRel(hero.secondaryCta.external)}
|
||||
class="btn btn-outline"
|
||||
>
|
||||
{hero.secondaryCta.label}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import type { HowItWorksContent } from '$lib/types';
|
||||
|
||||
export let content: HowItWorksContent;
|
||||
@@ -17,28 +16,22 @@
|
||||
|
||||
<div class="how-it-works-flow" aria-label="How it works">
|
||||
{#each content.steps as step, index}
|
||||
<article class="how-it-works-step">
|
||||
<div class="how-it-works-badge" aria-hidden="true">
|
||||
<span class="how-it-works-count">0{index + 1}</span>
|
||||
<article class:how-it-works-step-payoff={index === content.steps.length - 1} class="how-it-works-step">
|
||||
<div class="how-it-works-rail-node" aria-hidden="true">
|
||||
<span class="how-it-works-rail-dot"></span>
|
||||
</div>
|
||||
{#if step.icon}
|
||||
<div class="how-it-works-icon-bubble">
|
||||
<Icon name={step.icon} className="how-it-works-icon" />
|
||||
</div>
|
||||
{/if}
|
||||
<h3>{step.title}</h3>
|
||||
<p>{step.body}</p>
|
||||
<div class="how-it-works-step-top">
|
||||
<span class="how-it-works-count">{`0${index + 1}`}</span>
|
||||
<span class="how-it-works-phase">{step.phase || `Step ${index + 1}`}</span>
|
||||
</div>
|
||||
<div class="how-it-works-copy">
|
||||
<h3>{step.title}</h3>
|
||||
{#if step.benefit}
|
||||
<p class="how-it-works-benefit">{step.benefit}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="how-it-works-body">{step.body}</p>
|
||||
</article>
|
||||
|
||||
{#if index < content.steps.length - 1}
|
||||
<div class="how-it-works-connector" aria-hidden="true">
|
||||
<span class="how-it-works-connector-line"></span>
|
||||
<div class="how-it-works-connector-bubble">
|
||||
<Icon name="fas fa-paw" className="how-it-works-connector-icon" />
|
||||
</div>
|
||||
<span class="how-it-works-connector-line"></span>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,108 +77,113 @@
|
||||
|
||||
.how-it-works-flow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
position: relative;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 28px;
|
||||
align-items: stretch;
|
||||
margin-top: 34px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.how-it-works-flow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
right: 18px;
|
||||
top: 22px;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(33, 48, 33, 0.12) 0%,
|
||||
rgba(33, 48, 33, 0.28) 50%,
|
||||
rgba(33, 48, 33, 0.12) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.how-it-works-step {
|
||||
position: relative;
|
||||
padding: 26px 24px;
|
||||
border-radius: 28px;
|
||||
background:
|
||||
radial-gradient(circle at top center, rgba(255, 209, 0, 0.18), transparent 36%),
|
||||
linear-gradient(180deg, #fffaf0 0%, #f8f4ea 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
|
||||
0 14px 28px rgba(17, 20, 24, 0.04);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 18px 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.how-it-works-badge {
|
||||
display: inline-flex;
|
||||
.how-it-works-step-payoff {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.how-it-works-rail-node {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
|
||||
min-height: 44px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.how-it-works-rail-dot {
|
||||
display: inline-flex;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
background: var(--gw-green);
|
||||
box-shadow:
|
||||
0 0 0 7px #fff,
|
||||
0 0 0 8px rgba(33, 48, 33, 0.12);
|
||||
}
|
||||
|
||||
.how-it-works-step-top {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.how-it-works-count {
|
||||
color: var(--green);
|
||||
color: rgba(33, 48, 33, 0.3);
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
.how-it-works-icon-bubble {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: 0 auto 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--green);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
|
||||
0 16px 28px rgba(33, 48, 33, 0.16);
|
||||
.how-it-works-phase {
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
:global(.how-it-works-icon.icon) {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
.how-it-works-copy {
|
||||
padding: 22px 22px 20px;
|
||||
border-radius: 24px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 250, 240, 0.92) 0%, rgba(248, 244, 234, 0.92) 100%);
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.how-it-works-step h3 {
|
||||
margin: 0 0 10px;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
line-height: 1.18;
|
||||
}
|
||||
|
||||
.how-it-works-step p {
|
||||
margin: 0;
|
||||
.how-it-works-benefit {
|
||||
margin: 10px 0 0;
|
||||
color: #6b5830;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.how-it-works-body {
|
||||
margin: 14px 0 0;
|
||||
padding: 0 4px 0 22px;
|
||||
color: #4c5056;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.how-it-works-connector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.how-it-works-connector-line {
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.22);
|
||||
}
|
||||
|
||||
.how-it-works-connector-bubble {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #254129 0%, #213021 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
|
||||
0 14px 24px rgba(33, 48, 33, 0.14);
|
||||
}
|
||||
|
||||
:global(.how-it-works-connector-icon.icon) {
|
||||
color: var(--green);
|
||||
color: #ffd54a;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#how-it-works {
|
||||
padding-top: 6px;
|
||||
@@ -202,58 +200,66 @@
|
||||
|
||||
.how-it-works-flow {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
gap: 24px;
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.how-it-works-flow::before {
|
||||
left: 5px;
|
||||
right: auto;
|
||||
top: 22px;
|
||||
bottom: 22px;
|
||||
width: 1px;
|
||||
height: auto;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(33, 48, 33, 0.12) 0%,
|
||||
rgba(33, 48, 33, 0.28) 50%,
|
||||
rgba(33, 48, 33, 0.12) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.how-it-works-step {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-areas:
|
||||
'icon title badge'
|
||||
'body body body';
|
||||
column-gap: 14px;
|
||||
row-gap: 10px;
|
||||
padding: 20px 18px;
|
||||
text-align: left;
|
||||
padding: 0 0 0 28px;
|
||||
}
|
||||
|
||||
.how-it-works-badge {
|
||||
grid-area: badge;
|
||||
justify-self: end;
|
||||
align-self: start;
|
||||
margin-bottom: 0;
|
||||
padding: 7px 10px;
|
||||
.how-it-works-step-payoff {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.how-it-works-icon-bubble {
|
||||
grid-area: icon;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0;
|
||||
align-self: start;
|
||||
.how-it-works-rail-node {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-height: 0;
|
||||
margin: 18px 0 0;
|
||||
}
|
||||
|
||||
.how-it-works-rail-dot {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
|
||||
0 12px 22px rgba(33, 48, 33, 0.14);
|
||||
0 0 0 5px #fff,
|
||||
0 0 0 6px rgba(33, 48, 33, 0.12);
|
||||
}
|
||||
|
||||
.how-it-works-step-top {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.how-it-works-step h3 {
|
||||
grid-area: title;
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.how-it-works-step p {
|
||||
grid-area: body;
|
||||
.how-it-works-benefit {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.how-it-works-body {
|
||||
margin-top: 2px;
|
||||
padding: 14px 2px 0 18px;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.how-it-works-connector {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
|
||||
0 10px 24px rgba(17, 20, 24, 0.04);
|
||||
color: #213021;
|
||||
color: var(--gw-green);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
.info-nearby-kicker {
|
||||
display: inline-block;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
@@ -119,7 +119,7 @@
|
||||
min-height: 48px;
|
||||
padding: 12px 18px;
|
||||
border-radius: 999px;
|
||||
background: var(--green);
|
||||
background: var(--gw-green);
|
||||
color: #fff;
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
export let instagram: HomePageContent['instagram'];
|
||||
|
||||
const dogCutoutSrc = '/images/smiling-dogs-instagram-cta.png';
|
||||
const dogCutoutSrc = '/images/dog-cutout.png';
|
||||
</script>
|
||||
|
||||
<aside id="instagram" aria-label="Follow Goodwalk on Instagram">
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<style>
|
||||
#instagram {
|
||||
overflow: visible;
|
||||
overflow: hidden;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
@@ -89,12 +89,12 @@
|
||||
|
||||
.instagram-dog-wrap {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
bottom: 0;
|
||||
right: -56px;
|
||||
bottom: -14px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
width: 280px;
|
||||
width: 380px;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -149,8 +149,8 @@
|
||||
.instagram-dog-wrap {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
bottom: -80px;
|
||||
width: min(260px, calc(100% - 40px));
|
||||
bottom: -96px;
|
||||
width: min(300px, calc(100% - 32px));
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
.legal-section h2 {
|
||||
margin: 0 0 16px;
|
||||
padding-left: 14px;
|
||||
border-left: 3px solid var(--green);
|
||||
border-left: 3px solid var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(14px, 1.4vw, 17px);
|
||||
line-height: 1.3;
|
||||
@@ -140,7 +140,7 @@
|
||||
content: '–';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,26 @@
|
||||
const scrollDepthThreshold = 0.65;
|
||||
const desktopPromptMediaQuery = '(min-width: 769px)';
|
||||
|
||||
function numericPrice(price: string) {
|
||||
const value = Number(price.replace(/[^0-9.]/g, ''));
|
||||
return Number.isFinite(value) ? value : Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
function decoratePlans<T extends { price: string }>(plans: T[]) {
|
||||
const sorted = [...plans]
|
||||
.map((plan, index) => ({ plan, index, value: numericPrice(plan.price) }))
|
||||
.sort((a, b) => a.value - b.value || a.index - b.index);
|
||||
|
||||
const cheapestIndex = sorted[0]?.index ?? -1;
|
||||
const mobileOrder = new Map(sorted.map((entry, order) => [entry.index, order]));
|
||||
|
||||
return plans.map((plan, index) => ({
|
||||
...plan,
|
||||
isPopular: index === cheapestIndex,
|
||||
mobileOrder: mobileOrder.get(index) ?? index
|
||||
}));
|
||||
}
|
||||
|
||||
let showMeetGreetPrompt = false;
|
||||
let dismissMeetGreetPrompt = false;
|
||||
let bookingInView = false;
|
||||
@@ -147,9 +167,13 @@
|
||||
</div>
|
||||
|
||||
<div class:pricing-plan-grid-three={section.plans.length === 3} class="pricing-plan-grid">
|
||||
{#each section.plans as plan}
|
||||
<article class:pricing-plan-popular={plan.popular} class="pricing-plan-card">
|
||||
{#if plan.popular}
|
||||
{#each decoratePlans(section.plans) as plan}
|
||||
<article
|
||||
class:pricing-plan-popular={plan.isPopular}
|
||||
class="pricing-plan-card"
|
||||
style={`--mobile-order:${plan.mobileOrder};`}
|
||||
>
|
||||
{#if plan.isPopular}
|
||||
<span class="pricing-plan-ribbon">Popular</span>
|
||||
{/if}
|
||||
|
||||
@@ -226,7 +250,7 @@
|
||||
}
|
||||
|
||||
.pricing-page-hero {
|
||||
background: var(--green);
|
||||
background: var(--gw-green);
|
||||
padding: 56px 0 64px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -320,7 +344,7 @@
|
||||
height: 56px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 16px;
|
||||
background: var(--green);
|
||||
background: var(--gw-green);
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
}
|
||||
@@ -469,7 +493,7 @@
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
@@ -635,7 +659,7 @@
|
||||
}
|
||||
|
||||
.pricing-plan-popular {
|
||||
order: -1;
|
||||
order: var(--mobile-order, 0);
|
||||
}
|
||||
|
||||
.pricing-plan-card {
|
||||
@@ -672,7 +696,7 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -28,14 +28,16 @@
|
||||
</div>
|
||||
|
||||
<div class="promise-img">
|
||||
<img
|
||||
src={promise.imageUrl}
|
||||
alt={promise.imageAlt}
|
||||
width={promiseImage?.width}
|
||||
height={promiseImage?.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div class="promise-img-frame">
|
||||
<img
|
||||
src={promise.imageUrl}
|
||||
alt={promise.imageAlt}
|
||||
width={promiseImage?.width}
|
||||
height={promiseImage?.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -10,9 +10,35 @@
|
||||
export let pageContent: ServicePageContent;
|
||||
export let currentPath = '';
|
||||
|
||||
function numericPrice(price: string) {
|
||||
const value = Number(price.replace(/[^0-9.]/g, ''));
|
||||
return Number.isFinite(value) ? value : Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
function decoratePlans<T extends { price: string }>(plans: T[]) {
|
||||
const sorted = [...plans]
|
||||
.map((plan, index) => ({ plan, index, value: numericPrice(plan.price) }))
|
||||
.sort((a, b) => a.value - b.value || a.index - b.index);
|
||||
|
||||
const cheapestIndex = sorted[0]?.index ?? -1;
|
||||
const mobileOrder = new Map(sorted.map((entry, order) => [entry.index, order]));
|
||||
|
||||
return plans.map((plan, index) => ({
|
||||
...plan,
|
||||
isPopular: index === cheapestIndex,
|
||||
mobileOrder: mobileOrder.get(index) ?? index
|
||||
}));
|
||||
}
|
||||
|
||||
$: heroImage = getImageMetadata(pageContent.hero.imageUrl);
|
||||
$: highlightImage = pageContent.highlight ? getImageMetadata(pageContent.highlight.imageUrl) : null;
|
||||
$: highlightCollageImages =
|
||||
pageContent.highlight?.collageImages?.map((image) => ({
|
||||
...image,
|
||||
meta: getImageMetadata(image.imageUrl)
|
||||
})) ?? [];
|
||||
$: relatedServices = content.services.filter((s) => s.href && s.href !== currentPath);
|
||||
$: pricingPlans = decoratePlans(pageContent.pricing.plans);
|
||||
|
||||
$: relatedCards = [
|
||||
...relatedServices.map((s) => ({
|
||||
@@ -68,16 +94,33 @@
|
||||
</div>
|
||||
|
||||
<div class="service-inner">
|
||||
<div class="service-highlight-image">
|
||||
<img
|
||||
src={pageContent.highlight.imageUrl}
|
||||
alt={pageContent.highlight.imageAlt}
|
||||
width={highlightImage?.width}
|
||||
height={highlightImage?.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
{#if highlightCollageImages.length}
|
||||
<div class="service-highlight-collage" aria-label={pageContent.highlight.title}>
|
||||
{#each highlightCollageImages as image, index}
|
||||
<figure class={`service-collage-card service-collage-card-${index + 1}`}>
|
||||
<img
|
||||
src={image.imageUrl}
|
||||
alt={image.imageAlt}
|
||||
width={image.meta?.width}
|
||||
height={image.meta?.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</figure>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="service-highlight-image">
|
||||
<img
|
||||
src={pageContent.highlight.imageUrl}
|
||||
alt={pageContent.highlight.imageAlt}
|
||||
width={highlightImage?.width}
|
||||
height={highlightImage?.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
@@ -92,9 +135,13 @@
|
||||
</div>
|
||||
|
||||
<div class:service-plan-grid-three={pageContent.pricing.plans.length === 3} class="service-plan-grid">
|
||||
{#each pageContent.pricing.plans as plan}
|
||||
<article class:service-plan-popular={plan.popular} class="service-plan-card">
|
||||
{#if plan.popular}
|
||||
{#each pricingPlans as plan}
|
||||
<article
|
||||
class:service-plan-popular={plan.isPopular}
|
||||
class="service-plan-card"
|
||||
style={`--mobile-order:${plan.mobileOrder};`}
|
||||
>
|
||||
{#if plan.isPopular}
|
||||
<span class="service-plan-ribbon">Popular</span>
|
||||
{/if}
|
||||
|
||||
@@ -263,11 +310,11 @@
|
||||
}
|
||||
|
||||
.service-related-tint-1 {
|
||||
--card-accent: var(--green);
|
||||
--card-accent: var(--gw-green);
|
||||
}
|
||||
.service-related-tint-1 .service-related-icon {
|
||||
background: #dce6dc;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
.service-related-tint-2 {
|
||||
@@ -275,7 +322,7 @@
|
||||
}
|
||||
.service-related-tint-2 .service-related-icon {
|
||||
background: #efe4d1;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
.service-related-tint-3 {
|
||||
@@ -305,7 +352,7 @@
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: #efe4d1;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -334,7 +381,7 @@
|
||||
.service-related-price {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -352,7 +399,7 @@
|
||||
.service-related-link {
|
||||
margin-top: auto;
|
||||
padding-top: 6px;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
@@ -386,18 +433,69 @@
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.service-hero-media,
|
||||
.service-highlight-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 28px;
|
||||
background: #f4efe7;
|
||||
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.08);
|
||||
}
|
||||
|
||||
.service-hero-media {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
.service-highlight-image {
|
||||
aspect-ratio: 4 / 3;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.service-highlight-collage {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
max-width: 780px;
|
||||
margin: 0 auto;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.service-collage-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 28px;
|
||||
background: #f4efe7;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
|
||||
0 16px 40px rgba(17, 20, 24, 0.08);
|
||||
}
|
||||
|
||||
.service-collage-card img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.service-collage-card-1 {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.service-collage-card-2 {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.service-collage-card-3 {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.service-hero-media img,
|
||||
.service-highlight-image img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-radius: 28px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.08);
|
||||
}
|
||||
|
||||
.service-hero-media img {
|
||||
aspect-ratio: 4 / 3;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.service-highlight {
|
||||
@@ -418,10 +516,6 @@
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.service-highlight-image img {
|
||||
max-height: 620px;
|
||||
}
|
||||
|
||||
.service-pricing,
|
||||
.service-benefits {
|
||||
padding: 0 0 96px;
|
||||
@@ -460,6 +554,13 @@
|
||||
border-color 0.22s ease;
|
||||
}
|
||||
|
||||
.service-plan-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.service-plan-popular {
|
||||
border: 2px solid var(--yellow);
|
||||
}
|
||||
@@ -521,7 +622,7 @@
|
||||
font-family: var(--font-head);
|
||||
font-size: 44px;
|
||||
line-height: 1;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
.service-plan-period {
|
||||
@@ -537,6 +638,7 @@
|
||||
margin: 24px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.service-plan-features li {
|
||||
@@ -583,7 +685,7 @@
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.06);
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -662,7 +764,7 @@
|
||||
.service-extra-price {
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -680,7 +782,7 @@
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: #efe4d1;
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
font-size: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
@@ -702,6 +804,16 @@
|
||||
.service-hero-grid {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.service-highlight-collage {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.service-collage-card-2,
|
||||
.service-collage-card-3,
|
||||
.service-collage-card-1 {
|
||||
min-height: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -723,7 +835,7 @@
|
||||
}
|
||||
|
||||
.service-plan-popular {
|
||||
order: -1;
|
||||
order: var(--mobile-order, 0);
|
||||
}
|
||||
|
||||
.service-highlight,
|
||||
@@ -748,5 +860,18 @@
|
||||
margin: 18px auto 0;
|
||||
font-family: var(--font-head);
|
||||
}
|
||||
|
||||
.service-highlight-collage {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.service-collage-card-1,
|
||||
.service-collage-card-2,
|
||||
.service-collage-card-3 {
|
||||
min-height: 220px;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,12 +5,35 @@
|
||||
|
||||
export let services: IconCard[];
|
||||
export let heading = 'What we do';
|
||||
export let intro =
|
||||
'Choose the walk style that fits your dog best, then book a free Meet & Greet when you are ready.';
|
||||
|
||||
const requestedServiceStorageKey = 'goodwalk_requested_service';
|
||||
|
||||
function bookingHref() {
|
||||
return '#newlead';
|
||||
}
|
||||
|
||||
function primeBookingService(serviceTitle: string) {
|
||||
try {
|
||||
window.sessionStorage.setItem(requestedServiceStorageKey, serviceTitle);
|
||||
} catch {
|
||||
// Ignore storage failures and continue with the link target.
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('goodwalk:service-selected', {
|
||||
detail: { service: serviceTitle }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<section id="services" use:reveal={{ delay: 20 }} class="reveal-block">
|
||||
<div class="services-inner">
|
||||
<h2 class="section-heading">{heading}</h2>
|
||||
<p class="services-intro">{intro}</p>
|
||||
|
||||
<div class="services-grid">
|
||||
{#each services as service}
|
||||
@@ -26,10 +49,16 @@
|
||||
{/if}
|
||||
|
||||
{#if service.href}
|
||||
<a href={service.href} class="btn btn-green">
|
||||
<span>See {service.title} pricing</span>
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</a>
|
||||
<div class="service-card-actions">
|
||||
<a href={bookingHref()} class="btn btn-green" on:click={() => primeBookingService(service.title)}>
|
||||
<span>Book {service.title}</span>
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</a>
|
||||
|
||||
<a href={service.href} class="service-card-link">
|
||||
View details & pricing
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -81,4 +110,42 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.services-intro {
|
||||
max-width: 700px;
|
||||
margin: 18px auto 0;
|
||||
text-align: center;
|
||||
color: #4c5056;
|
||||
font-size: 17px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.service-card-actions {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.service-card-actions :global(.btn) {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.service-card-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 28px;
|
||||
color: var(--gw-green);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.service-card-link:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.18em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let email: string;
|
||||
export let enquiryType: 'booking' | 'general' = 'booking';
|
||||
export let onClose: () => void;
|
||||
const gwGreenHex = '#213021';
|
||||
|
||||
$: isGeneralEnquiry = enquiryType === 'general';
|
||||
|
||||
@@ -20,7 +21,7 @@
|
||||
angle: 60,
|
||||
spread: 65,
|
||||
origin: { x: 0, y: 0.75 },
|
||||
colors: ['#FFD100', '#213021', '#ffffff', '#7aaa7a', '#ffeaa0'],
|
||||
colors: ['#FFD100', gwGreenHex, '#ffffff', '#7aaa7a', '#ffeaa0'],
|
||||
gravity: 0.9,
|
||||
scalar: 1.1,
|
||||
});
|
||||
@@ -29,7 +30,7 @@
|
||||
angle: 120,
|
||||
spread: 65,
|
||||
origin: { x: 1, y: 0.75 },
|
||||
colors: ['#FFD100', '#213021', '#ffffff', '#7aaa7a', '#ffeaa0'],
|
||||
colors: ['#FFD100', gwGreenHex, '#ffffff', '#7aaa7a', '#ffeaa0'],
|
||||
gravity: 0.9,
|
||||
scalar: 1.1,
|
||||
});
|
||||
@@ -157,7 +158,7 @@
|
||||
margin: 0 0 14px;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #213021;
|
||||
color: var(--gw-green);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@@ -186,7 +187,7 @@
|
||||
.modal-btn {
|
||||
display: inline-block;
|
||||
padding: 14px 36px;
|
||||
background: #213021;
|
||||
background: var(--gw-green);
|
||||
color: #FFD100;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
import type { TestimonialContent } from '$lib/types';
|
||||
|
||||
export let testimonials: TestimonialContent[];
|
||||
export let heading = 'Why people choose us!';
|
||||
export let blurb = 'Busy parents get peace of mind. Dogs come home tired and happy. See why 30+ Auckland families trust the Tiny Gang — follow along on Instagram for daily adventures, wagging tails and the odd zoomie.';
|
||||
export let eyebrow = '30+ five-star reviews';
|
||||
export let heading = 'Proof your dog is in good hands';
|
||||
export let blurb = 'Peace of mind for busy Auckland dog owners. Happier dogs, smoother routines, and a team owners trust with the important stuff.';
|
||||
export let instagramHref = 'https://www.instagram.com/goodwalk.nz/';
|
||||
export let instagramLabel = 'goodwalk.nz';
|
||||
|
||||
@@ -30,7 +31,7 @@
|
||||
},
|
||||
Ross: {
|
||||
reviewer: 'Ross',
|
||||
detail: "Otis's Dad",
|
||||
detail: "Otis's dad",
|
||||
quote:
|
||||
'Truly the best dog walker in Auckland! I feel so lucky to have found Aless and my little terrier Otis absolutely adores her. He enjoys his regular weekly walks and always comes back happy & tired. Love the updates on social media so I can see how my dog is enjoying his day! Aless makes logistics so easy too. Highly highly recommend, there’s a reason she has 5 stars!',
|
||||
imageUrl: '/images/otis-auckland-dog-walking-review.png'
|
||||
@@ -123,6 +124,7 @@
|
||||
|
||||
<section id="testimonials" use:reveal={{ delay: 40 }} class="reveal-block">
|
||||
<div class="testimonials-inner">
|
||||
<span class="testimonials-eyebrow">{eyebrow}</span>
|
||||
<h2 class="section-heading">{heading}</h2>
|
||||
<div class="testimonials-intro">
|
||||
<p>{blurb}</p>
|
||||
@@ -244,6 +246,21 @@
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.testimonials-eyebrow {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: 0 auto 10px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
color: var(--gw-green);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.testimonials-intro {
|
||||
max-width: 760px;
|
||||
margin: 18px auto 0;
|
||||
@@ -266,7 +283,7 @@
|
||||
padding: 10px 16px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.06);
|
||||
color: var(--green);
|
||||
color: var(--gw-green);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
|
||||
@@ -297,9 +314,16 @@
|
||||
.testimonials-carousel {
|
||||
position: relative;
|
||||
margin-top: 48px;
|
||||
padding: 0 38px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.testimonials-eyebrow {
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.testimonials-intro {
|
||||
margin-top: 14px;
|
||||
}
|
||||
@@ -555,11 +579,11 @@
|
||||
}
|
||||
|
||||
.testimonial-arrow-left {
|
||||
left: -38px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.testimonial-arrow-right {
|
||||
right: -38px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
@@ -588,6 +612,7 @@
|
||||
@media (max-width: 767px) {
|
||||
.testimonials-carousel {
|
||||
margin-top: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.testimonial-stage {
|
||||
|
||||
@@ -21,7 +21,11 @@
|
||||
|
||||
<section id="values">
|
||||
<div class="values-inner">
|
||||
<h2 class="section-heading">Where dogs come first</h2>
|
||||
<span class="values-eyebrow">Why owners stay</span>
|
||||
<h2 class="section-heading">Calmer dogs. Clearer routines. Less worry.</h2>
|
||||
<p class="values-intro">
|
||||
Everything is designed to make life easier for busy Auckland dog owners and safer, happier for the dogs in our care.
|
||||
</p>
|
||||
|
||||
<div class="values-grid">
|
||||
{#each orderedValues as value}
|
||||
@@ -34,3 +38,43 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.values-eyebrow {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: 0 auto 10px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--yellow);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.values-intro {
|
||||
max-width: 760px;
|
||||
margin: 18px auto 0;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 17px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.values-eyebrow {
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.values-intro {
|
||||
margin-top: 14px;
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user