Design language tweaks, improvements
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { accordion } from '$lib/actions/accordion';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import CtaCard from '$lib/components/CtaCard.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import type { AboutPageContent } from '$lib/types';
|
||||
@@ -46,7 +46,7 @@
|
||||
class="about-section reveal-block"
|
||||
class:about-section-gradient={section.accent === 'gradient'}
|
||||
>
|
||||
<div class="about-inner about-section-grid" class:about-section-reverse={section.reverse}>
|
||||
<div class="page-inner about-section-grid" class:about-section-reverse={section.reverse}>
|
||||
<div class="about-copy">
|
||||
{#if section.eyebrow}
|
||||
<span class="about-eyebrow">{section.eyebrow}</span>
|
||||
@@ -71,7 +71,7 @@
|
||||
{#if founderSection}
|
||||
{@const founderEnhanced = getEnhancedImage(founderSection.imageUrl)}
|
||||
<section use:reveal={{ delay: 50 }} class="about-founder reveal-block">
|
||||
<div class="about-inner about-founder-grid">
|
||||
<div class="page-inner about-founder-grid">
|
||||
<div class="about-founder-media">
|
||||
{#if founderEnhanced}
|
||||
<enhanced:img
|
||||
@@ -116,7 +116,7 @@
|
||||
<!-- ── FAQs ── -->
|
||||
{#if pageContent.faqs && pageContent.faqs.length}
|
||||
<section use:reveal={{ delay: 30 }} class="about-faq reveal-block">
|
||||
<div class="about-inner">
|
||||
<div class="page-inner">
|
||||
<div class="about-faq-header">
|
||||
<span class="about-eyebrow">FAQ</span>
|
||||
<h2>{pageContent.faqTitle ?? 'Common questions'}</h2>
|
||||
@@ -135,28 +135,16 @@
|
||||
|
||||
<!-- ── Contact CTA ── -->
|
||||
<section use:reveal={{ delay: 50 }} class="about-contact reveal-block">
|
||||
<div class="about-inner">
|
||||
<div class="about-contact-card">
|
||||
<span class="about-contact-eyebrow">Get in touch</span>
|
||||
<h2>{pageContent.contact.title}</h2>
|
||||
<p class="about-contact-desc">Questions, pricing, or your first Meet & Greet — start here and we'll reply within 24 hours.</p>
|
||||
<a class="btn btn-yellow btn-mobile-center about-contact-btn" href={pageContent.contact.cta.href}>
|
||||
{pageContent.contact.cta.label}
|
||||
</a>
|
||||
<div class="about-contact-links">
|
||||
<a class="about-contact-link" href={`mailto:${pageContent.contact.email}`}>
|
||||
<Icon name="fas fa-envelope" />
|
||||
{pageContent.contact.email}
|
||||
</a>
|
||||
<a
|
||||
class="about-contact-link"
|
||||
href={`tel:${pageContent.contact.phone.replace(/[^0-9+]/g, '')}`}
|
||||
>
|
||||
<Icon name="fas fa-phone" />
|
||||
{pageContent.contact.phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-inner">
|
||||
<CtaCard
|
||||
title={pageContent.contact.title}
|
||||
description="Questions, pricing, or your first Meet & Greet — start here and we'll reply within 24 hours."
|
||||
ctaHref={pageContent.contact.cta.href}
|
||||
ctaLabel={pageContent.contact.cta.label}
|
||||
email={pageContent.contact.email}
|
||||
phone={pageContent.contact.phone}
|
||||
showIcons={true}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -167,36 +155,21 @@
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.about-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
/* ── Shared eyebrow ── */
|
||||
.about-eyebrow,
|
||||
.about-contact-eyebrow {
|
||||
/* ── Eyebrow ── */
|
||||
.about-eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
color: var(--gw-green);
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.about-eyebrow {
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
color: var(--gw-green);
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.about-contact-eyebrow {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.about-chip-stars {
|
||||
color: var(--yellow);
|
||||
letter-spacing: 1px;
|
||||
@@ -394,75 +367,6 @@
|
||||
padding: 0 0 88px;
|
||||
}
|
||||
|
||||
.about-contact-card {
|
||||
background: var(--gw-green);
|
||||
color: #fff;
|
||||
border-radius: 28px;
|
||||
padding: 56px 48px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 48px rgba(33, 48, 33, 0.18);
|
||||
}
|
||||
|
||||
.about-contact-card h2 {
|
||||
margin: 0 0 10px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(28px, 3vw, 42px);
|
||||
font-weight: 800;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.about-contact-desc {
|
||||
max-width: 440px;
|
||||
margin: 0 auto 28px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.about-contact-btn {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.about-contact-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.about-contact-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.about-contact-link:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Reveal ── */
|
||||
:global(.reveal-ready.reveal-block) {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, var(--reveal-distance, 24px), 0);
|
||||
transition:
|
||||
opacity 0.55s ease,
|
||||
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
transition-delay: var(--reveal-delay, 0ms);
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block) {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
/* ── Tablet ── */
|
||||
@media (max-width: 1024px) {
|
||||
.about-section-grid,
|
||||
@@ -483,10 +387,6 @@
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.about-inner {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.about-section {
|
||||
padding: 60px 0;
|
||||
}
|
||||
@@ -537,17 +437,6 @@
|
||||
.about-contact {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.about-contact-card {
|
||||
padding: 36px 24px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.about-contact-links {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
|
||||
export let eyebrow = 'Get in touch';
|
||||
export let title: string;
|
||||
export let description: string;
|
||||
export let ctaHref: string;
|
||||
export let ctaLabel: string;
|
||||
export let email: string | undefined = undefined;
|
||||
export let phone: string | undefined = undefined;
|
||||
export let phoneHref: string | undefined = undefined;
|
||||
export let showIcons = false;
|
||||
|
||||
$: resolvedPhoneHref = phoneHref ?? (phone ? `tel:${phone.replace(/[^0-9+]/g, '')}` : undefined);
|
||||
</script>
|
||||
|
||||
<div class="cta-card">
|
||||
<span class="cta-card__eyebrow">{eyebrow}</span>
|
||||
<h2>{title}</h2>
|
||||
<p class="cta-card__desc">{description}</p>
|
||||
<a class="btn btn-yellow btn-mobile-center cta-card__btn" href={ctaHref}>{ctaLabel}</a>
|
||||
{#if email || phone}
|
||||
<div class="cta-card__links">
|
||||
{#if email}
|
||||
<a class="cta-card__link" href="mailto:{email}">
|
||||
{#if showIcons}<Icon name="fas fa-envelope" />{/if}
|
||||
{email}
|
||||
</a>
|
||||
{/if}
|
||||
{#if phone && resolvedPhoneHref}
|
||||
<a class="cta-card__link" href={resolvedPhoneHref}>
|
||||
{#if showIcons}<Icon name="fas fa-phone" />{/if}
|
||||
{phone}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cta-card {
|
||||
background: var(--gw-green);
|
||||
color: #fff;
|
||||
border-radius: 28px;
|
||||
padding: 56px 48px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 48px rgba(33, 48, 33, 0.18);
|
||||
}
|
||||
|
||||
.cta-card__eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.cta-card h2 {
|
||||
margin: 0 0 10px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(28px, 3vw, 42px);
|
||||
font-weight: 800;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cta-card__desc {
|
||||
max-width: 460px;
|
||||
margin: 0 auto 28px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.cta-card__btn {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cta-card__links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.cta-card__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.cta-card__link:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cta-card {
|
||||
padding: 36px 24px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.cta-card__links {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import ModalShell from '$lib/components/ModalShell.svelte';
|
||||
|
||||
export let email = 'info@goodwalk.co.nz';
|
||||
export let enquiryType: 'booking' | 'general' = 'booking';
|
||||
export let onClose: () => void;
|
||||
@@ -11,29 +13,12 @@
|
||||
`?subject=${encodeURIComponent(isGeneralEnquiry ? 'General enquiry' : 'Booking enquiry')}` +
|
||||
`&body=${encodeURIComponent(
|
||||
isGeneralEnquiry
|
||||
? 'Hi Aless,\n\nI tried to submit the contact form but it didn’t go through. Here are my details:\n\nName:\nPhone:\nMessage:\n\nThanks!'
|
||||
: 'Hi Aless,\n\nI tried to submit the booking form but it didn’t go through. Here are my details:\n\nName:\nPhone:\nDog’s name:\nLocation:\n\nThanks!'
|
||||
? "Hi Aless,\n\nI tried to submit the contact form but it didn't go through. Here are my details:\n\nName:\nPhone:\nMessage:\n\nThanks!"
|
||||
: "Hi Aless,\n\nI tried to submit the booking form but it didn't go through. Here are my details:\n\nName:\nPhone:\nDog's name:\nLocation:\n\nThanks!"
|
||||
)}`;
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="modal-backdrop"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="error-modal-heading"
|
||||
on:click|self={onClose}
|
||||
on:keydown={handleKeydown}
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="modal-card">
|
||||
<button class="modal-close" type="button" aria-label="Close" on:click={onClose}>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<ModalShell {onClose} ariaLabelledBy="error-modal-heading">
|
||||
<div class="modal-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 9v4" />
|
||||
@@ -69,61 +54,9 @@
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
|
||||
<style>
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(10, 20, 10, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
animation: backdrop-in 0.25s ease;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 52px 48px 40px;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 24px 80px rgba(10, 20, 10, 0.22);
|
||||
text-align: center;
|
||||
animation: card-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 20px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #f2f2f0;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: #e8e8e4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
@@ -246,41 +179,7 @@
|
||||
border-color: var(--gw-green);
|
||||
}
|
||||
|
||||
@keyframes backdrop-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes card-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.88) translateY(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.4);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.modal-card {
|
||||
padding: 44px 28px 32px;
|
||||
}
|
||||
.modal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
<script lang="ts">
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import type { FounderStoryContent } from '$lib/types';
|
||||
|
||||
export let founderStory: FounderStoryContent;
|
||||
|
||||
$: founderStoryEnhanced = getEnhancedImage(founderStory.imageUrl);
|
||||
</script>
|
||||
|
||||
<section id="promise">
|
||||
<div class="promise-inner">
|
||||
<div class="promise-text">
|
||||
<span class="promise-kicker">Founder story</span>
|
||||
<div class="promise-mobile-intro">
|
||||
<div class="promise-mobile-avatar">
|
||||
{#if founderStoryEnhanced}
|
||||
<enhanced:img
|
||||
src={founderStoryEnhanced}
|
||||
alt={founderStory.imageAlt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<img src={founderStory.imageUrl} alt={founderStory.imageAlt} loading="lazy" decoding="async" />
|
||||
{/if}
|
||||
</div>
|
||||
<p class="promise-mobile-caption">Auckland Central walks led personally by Aless.</p>
|
||||
</div>
|
||||
|
||||
<h2 class="promise-heading">
|
||||
<span class="promise-heading-desktop">
|
||||
<span class="promise-title-main">{founderStory.title}</span>
|
||||
<br />
|
||||
<span class="promise-title-highlight">{founderStory.subtitle}</span>
|
||||
</span>
|
||||
<span class="promise-heading-mobile">
|
||||
<span class="promise-title-main">{founderStory.title}</span>
|
||||
<span class="promise-title-highlight">{founderStory.subtitle}</span>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{#each founderStory.body as paragraph, idx}
|
||||
<p>
|
||||
{paragraph}
|
||||
{#if idx === founderStory.body.length - 1}
|
||||
<strong>{founderStory.emphasis}</strong>
|
||||
{/if}
|
||||
</p>
|
||||
{/each}
|
||||
|
||||
<a href={founderStory.cta.href} class="btn btn-green">{founderStory.cta.label}</a>
|
||||
</div>
|
||||
|
||||
<div class="promise-img">
|
||||
<div class="promise-img-frame">
|
||||
{#if founderStoryEnhanced}
|
||||
<enhanced:img
|
||||
src={founderStoryEnhanced}
|
||||
alt={founderStory.imageAlt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<img src={founderStory.imageUrl} alt={founderStory.imageAlt} loading="lazy" decoding="async" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.promise-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 14px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.07);
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.promise-heading {
|
||||
margin: 0 0 22px;
|
||||
max-width: 14ch;
|
||||
}
|
||||
|
||||
.promise-mobile-intro {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.promise-heading-desktop {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.promise-heading-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.promise-heading-mobile .promise-title-main,
|
||||
.promise-heading-mobile .promise-title-highlight {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.promise-title-main {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(13, 26, 13, 0.68);
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(15px, 1.3vw, 18px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.promise-title-highlight {
|
||||
display: block;
|
||||
color: #0d1a0d;
|
||||
font-size: clamp(42px, 5.2vw, 64px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.05em;
|
||||
line-height: 0.96;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.promise-text {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#promise {
|
||||
padding-top: 42px;
|
||||
padding-bottom: var(--space-section-featured-y);
|
||||
}
|
||||
|
||||
.promise-kicker {
|
||||
margin-bottom: 12px;
|
||||
padding: 7px 12px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.promise-text {
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.promise-heading {
|
||||
max-width: none;
|
||||
margin-bottom: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.promise-heading-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.promise-heading-mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.promise-heading-mobile .promise-title-main,
|
||||
.promise-heading-mobile .promise-title-highlight {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.promise-mobile-intro {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin: 0 0 16px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(180deg, #fbf6e8 0%, #efe4c8 100%);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.65),
|
||||
0 10px 22px rgba(17, 20, 24, 0.06),
|
||||
inset 0 0 0 1px rgba(242, 191, 47, 0.12);
|
||||
}
|
||||
|
||||
.promise-mobile-avatar {
|
||||
flex: 0 0 auto;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
overflow: hidden;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 18px rgba(17, 20, 24, 0.08);
|
||||
}
|
||||
|
||||
.promise-mobile-avatar img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center 20%;
|
||||
}
|
||||
|
||||
.promise-mobile-caption {
|
||||
margin: 0;
|
||||
color: #34363a;
|
||||
font-family: var(--font-head);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.promise-title-main {
|
||||
margin-bottom: 0;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.promise-title-highlight {
|
||||
font-size: clamp(36px, 11vw, 54px);
|
||||
line-height: 0.98;
|
||||
}
|
||||
|
||||
.promise-text p,
|
||||
.promise-text .btn {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.promise-text p {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.promise-text .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.promise-img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -50,7 +50,7 @@
|
||||
<style>
|
||||
#how-it-works {
|
||||
background: var(--off-white);
|
||||
padding: 80px 0;
|
||||
padding: var(--space-section-page-y) 0;
|
||||
}
|
||||
|
||||
.hiw-inner {
|
||||
@@ -227,12 +227,8 @@
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
#how-it-works {
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.hiw-inner {
|
||||
padding: 0 24px;
|
||||
padding: 0 var(--space-container-x-mobile);
|
||||
}
|
||||
|
||||
.hiw-header {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { sharedServices } from '$lib/content/services';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import CtaCard from '$lib/components/CtaCard.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import { getSeededTestimonialIndex } from '$lib/testimonials';
|
||||
@@ -55,7 +56,7 @@
|
||||
|
||||
<!-- ── Hero ── -->
|
||||
<section class="loc-hero">
|
||||
<div class="loc-inner">
|
||||
<div class="page-inner">
|
||||
<span class="loc-hero-eyebrow">Auckland Central Dog Walking</span>
|
||||
<h1>Dog walkers in {location.suburb}</h1>
|
||||
<p class="loc-hero-desc">{location.intro}</p>
|
||||
@@ -80,7 +81,7 @@
|
||||
</section>
|
||||
|
||||
<section class="loc-highlights" aria-label={`Goodwalk highlights in ${location.suburb}`}>
|
||||
<div class="loc-inner">
|
||||
<div class="page-inner">
|
||||
<div class="loc-highlights-grid">
|
||||
{#each locationHighlights as highlight}
|
||||
<div class="loc-highlight-card">
|
||||
@@ -100,7 +101,7 @@
|
||||
|
||||
<!-- ── Parks ── -->
|
||||
<section use:reveal={{ delay: 30 }} class="loc-parks reveal-block">
|
||||
<div class="loc-inner">
|
||||
<div class="page-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="loc-eyebrow">Where we walk</span>
|
||||
<h2>Parks & walks in {location.suburb}</h2>
|
||||
@@ -125,7 +126,7 @@
|
||||
|
||||
{#if parksWithImages.length > 0}
|
||||
<section use:reveal={{ delay: 30 }} class="loc-gallery reveal-block">
|
||||
<div class="loc-inner">
|
||||
<div class="page-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="loc-eyebrow">Local parks</span>
|
||||
<h2>Park photos from {location.suburb}</h2>
|
||||
@@ -158,7 +159,7 @@
|
||||
|
||||
<!-- ── Services ── -->
|
||||
<section use:reveal={{ delay: 30 }} class="loc-services reveal-block">
|
||||
<div class="loc-inner">
|
||||
<div class="page-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="loc-eyebrow">What we offer</span>
|
||||
<h2>Goodwalk services in {location.suburb}</h2>
|
||||
@@ -184,7 +185,7 @@
|
||||
<!-- ── Testimonial ── -->
|
||||
{#if featuredTestimonial}
|
||||
<section use:reveal={{ delay: 30 }} class="loc-review reveal-block">
|
||||
<div class="loc-inner">
|
||||
<div class="page-inner">
|
||||
<div class="loc-review-card">
|
||||
<span class="loc-review-stars" aria-hidden="true">★★★★★</span>
|
||||
<blockquote class="loc-review-quote">"{featuredTestimonial.quote}"</blockquote>
|
||||
@@ -201,19 +202,16 @@
|
||||
|
||||
<!-- ── CTA ── -->
|
||||
<section use:reveal={{ delay: 30 }} class="loc-cta reveal-block">
|
||||
<div class="loc-inner">
|
||||
<div class="loc-cta-card">
|
||||
<span class="loc-cta-eyebrow">Get in touch</span>
|
||||
<h2>Ready to get started in {location.suburb}?</h2>
|
||||
<p class="loc-cta-desc">
|
||||
A free Meet & Greet is the first step — no commitment, no pressure. We meet your dog, answer your questions, and see if Goodwalk is the right fit.
|
||||
</p>
|
||||
<a class="btn btn-yellow btn-mobile-center loc-cta-btn" href="/contact-us">Book a free Meet & Greet</a>
|
||||
<div class="loc-cta-links">
|
||||
<a class="loc-cta-link" href="mailto:info@goodwalk.co.nz">info@goodwalk.co.nz</a>
|
||||
<a class="loc-cta-link" href="tel:+64226421011">(022) 642 1011</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-inner">
|
||||
<CtaCard
|
||||
title="Ready to get started in {location.suburb}?"
|
||||
description="A free Meet & Greet is the first step — no commitment, no pressure. We meet your dog, answer your questions, and see if Goodwalk is the right fit."
|
||||
ctaHref="/contact-us"
|
||||
ctaLabel="Book a free Meet & Greet"
|
||||
email="info@goodwalk.co.nz"
|
||||
phone="(022) 642 1011"
|
||||
phoneHref="tel:+64226421011"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -224,16 +222,9 @@
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.loc-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
/* ── Eyebrow ── */
|
||||
.loc-eyebrow,
|
||||
.loc-hero-eyebrow,
|
||||
.loc-cta-eyebrow {
|
||||
.loc-hero-eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
padding: 7px 12px;
|
||||
@@ -250,8 +241,7 @@
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.loc-hero-eyebrow,
|
||||
.loc-cta-eyebrow {
|
||||
.loc-hero-eyebrow {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -696,74 +686,6 @@
|
||||
padding: 0 0 88px;
|
||||
}
|
||||
|
||||
.loc-cta-card {
|
||||
background: var(--gw-green);
|
||||
color: #fff;
|
||||
border-radius: 28px;
|
||||
padding: 56px 48px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 48px rgba(33, 48, 33, 0.18);
|
||||
}
|
||||
|
||||
.loc-cta-card h2 {
|
||||
margin: 0 0 10px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(26px, 3vw, 40px);
|
||||
font-weight: 800;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.loc-cta-desc {
|
||||
max-width: 460px;
|
||||
margin: 0 auto 28px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.loc-cta-btn {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loc-cta-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.loc-cta-link {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.loc-cta-link:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Reveal ── */
|
||||
:global(.reveal-ready.reveal-block) {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, var(--reveal-distance, 24px), 0);
|
||||
transition:
|
||||
opacity 0.55s ease,
|
||||
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
transition-delay: var(--reveal-delay, 0ms);
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block) {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
/* ── Tablet ── */
|
||||
@media (max-width: 1024px) {
|
||||
.loc-highlights-grid,
|
||||
@@ -775,10 +697,6 @@
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.loc-inner {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.loc-hero {
|
||||
padding: 56px 0 48px;
|
||||
}
|
||||
@@ -849,15 +767,5 @@
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.loc-cta-card {
|
||||
padding: 36px 24px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.loc-cta-links {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
export let onClose: () => void;
|
||||
export let ariaLabel: string | undefined = undefined;
|
||||
export let ariaLabelledBy: string | undefined = undefined;
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="modal-backdrop"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
on:click|self={onClose}
|
||||
on:keydown={handleKeydown}
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="modal-card">
|
||||
<button class="modal-close" type="button" aria-label="Close" on:click={onClose}>
|
||||
✕
|
||||
</button>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(10, 20, 10, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
animation: backdrop-in 0.25s ease;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 52px 48px 44px;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 24px 80px rgba(10, 20, 10, 0.22);
|
||||
text-align: center;
|
||||
animation: card-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 20px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #f2f2f0;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: #e8e8e4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@keyframes backdrop-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes card-in {
|
||||
from { opacity: 0; transform: scale(0.88) translateY(16px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes bounce-in {
|
||||
from { opacity: 0; transform: scale(0.4); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.modal-card {
|
||||
padding: 44px 28px 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,40 +4,21 @@
|
||||
import BookingSection from '$lib/components/BookingSection.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import PricingPlanCard from '$lib/components/PricingPlanCard.svelte';
|
||||
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte';
|
||||
import { decoratePlans } from '$lib/utils/pricing';
|
||||
import type { PricingPageContent, SiteSharedContent } from '$lib/types';
|
||||
|
||||
export let content: SiteSharedContent;
|
||||
export let pageContent: PricingPageContent;
|
||||
|
||||
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;
|
||||
let promptShown = false;
|
||||
let canShowDesktopPrompt = false;
|
||||
const desktopPromptMediaQuery = '(min-width: 769px)';
|
||||
const scrollDepthThreshold = 0.65;
|
||||
|
||||
function revealMeetGreetPrompt() {
|
||||
if (dismissMeetGreetPrompt || bookingInView || promptShown || !canShowDesktopPrompt) {
|
||||
@@ -138,7 +119,7 @@
|
||||
|
||||
{#each pageContent.sections as section, index}
|
||||
<section use:reveal class="pricing-section reveal-block">
|
||||
<div class="pricing-inner">
|
||||
<div class="page-inner">
|
||||
<div class="pricing-section-heading">
|
||||
{#if section.icon}
|
||||
<div class="pricing-section-icon">
|
||||
@@ -162,27 +143,7 @@
|
||||
|
||||
<div class:pricing-plan-grid-three={section.plans.length === 3} class="pricing-plan-grid">
|
||||
{#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}
|
||||
|
||||
<h3>{plan.title}</h3>
|
||||
<div class="pricing-plan-price">{plan.price}</div>
|
||||
<p class="pricing-plan-period">{plan.period}</p>
|
||||
|
||||
<ul class="pricing-plan-features">
|
||||
{#each plan.features as feature}
|
||||
<li>{feature}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<a class="btn btn-yellow pricing-plan-cta" href="#newlead">Book a Meet & Greet</a>
|
||||
</article>
|
||||
<PricingPlanCard {plan} variant="pricing" />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -241,12 +202,6 @@
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.pricing-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
.pricing-trust {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -351,87 +306,6 @@
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.pricing-plan-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
border-radius: 28px;
|
||||
padding: 30px 26px;
|
||||
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
|
||||
transition:
|
||||
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.22s ease,
|
||||
border-color 0.22s ease;
|
||||
}
|
||||
|
||||
.pricing-plan-popular {
|
||||
border: 2px solid var(--yellow);
|
||||
}
|
||||
|
||||
.pricing-plan-ribbon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--yellow);
|
||||
color: #000;
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pricing-plan-card h3 {
|
||||
margin: 0;
|
||||
font-family: var(--font-head);
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.pricing-plan-price {
|
||||
margin-top: 22px;
|
||||
font-family: var(--font-head);
|
||||
font-size: 52px;
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.05em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.pricing-plan-period {
|
||||
margin: 10px 0 0;
|
||||
color: #5e6167;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.pricing-plan-features {
|
||||
width: 100%;
|
||||
margin: 24px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.pricing-plan-features li {
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid rgba(17, 20, 24, 0.08);
|
||||
color: #34363a;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pricing-plan-cta {
|
||||
margin-top: 24px;
|
||||
font-family: var(--font-head);
|
||||
}
|
||||
|
||||
.pricing-section-mobile-cta,
|
||||
.pricing-mobile-consult {
|
||||
display: none;
|
||||
@@ -556,31 +430,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.pricing-plan-card:hover {
|
||||
transform: translateY(-6px) scale(1.012);
|
||||
box-shadow: 0 22px 44px rgba(17, 20, 24, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.pricing-plan-card:active {
|
||||
transform: translateY(-2px) scale(0.992);
|
||||
}
|
||||
|
||||
:global(.reveal-ready.reveal-block) {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, var(--reveal-distance, 24px), 0);
|
||||
transition:
|
||||
opacity 0.55s ease,
|
||||
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
transition-delay: var(--reveal-delay, 0ms);
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block) {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.pricing-plan-grid,
|
||||
.pricing-plan-grid-three {
|
||||
@@ -589,18 +438,6 @@
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.pricing-inner {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.pricing-page-hero {
|
||||
padding: 56px 0 20px;
|
||||
}
|
||||
|
||||
.pricing-page-hero h1 {
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.pricing-trust {
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
@@ -635,22 +472,6 @@
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.pricing-plan-card {
|
||||
order: var(--mobile-order, 0);
|
||||
}
|
||||
|
||||
.pricing-plan-card {
|
||||
padding: 28px 22px;
|
||||
}
|
||||
|
||||
.pricing-plan-price {
|
||||
font-size: 46px;
|
||||
}
|
||||
|
||||
.pricing-plan-cta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pricing-section-mobile-cta {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
<script lang="ts">
|
||||
export let plan: {
|
||||
title: string;
|
||||
price: string;
|
||||
period: string;
|
||||
features: string[];
|
||||
isPopular: boolean;
|
||||
mobileOrder: number;
|
||||
};
|
||||
export let variant: 'pricing' | 'service' = 'service';
|
||||
</script>
|
||||
|
||||
<article
|
||||
class="plan-card"
|
||||
class:plan-card--popular={plan.isPopular}
|
||||
class:plan-card--pricing={variant === 'pricing'}
|
||||
class:plan-card--service={variant === 'service'}
|
||||
style="--mobile-order:{plan.mobileOrder};"
|
||||
>
|
||||
{#if plan.isPopular}
|
||||
<span class="plan-card__ribbon">Popular</span>
|
||||
{/if}
|
||||
<h3>{plan.title}</h3>
|
||||
<div class="plan-card__price">{plan.price}</div>
|
||||
<p class="plan-card__period">{plan.period}</p>
|
||||
<ul class="plan-card__features">
|
||||
{#each plan.features as feature}
|
||||
<li>{feature}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<a class="btn btn-yellow plan-card__cta" href="#newlead">Book a Meet & Greet</a>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
/* ── Base ── */
|
||||
.plan-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 28px;
|
||||
padding: 30px 26px;
|
||||
transition:
|
||||
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.22s ease,
|
||||
border-color 0.22s ease;
|
||||
}
|
||||
|
||||
.plan-card--popular {
|
||||
border: 2px solid var(--yellow);
|
||||
}
|
||||
|
||||
/* ── Service variant ── */
|
||||
.plan-card--service {
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(247, 248, 246, 0.98) 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.045),
|
||||
0 8px 40px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* ── Pricing variant ── */
|
||||
.plan-card--pricing {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
/* ── Ribbon ── */
|
||||
.plan-card__ribbon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--yellow);
|
||||
color: #000;
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ── Heading ── */
|
||||
.plan-card h3 {
|
||||
margin: 0;
|
||||
font-family: var(--font-head);
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* ── Price ── */
|
||||
.plan-card__price {
|
||||
font-family: var(--font-head);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.plan-card--service .plan-card__price {
|
||||
margin-top: 20px;
|
||||
font-size: 44px;
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
.plan-card--pricing .plan-card__price {
|
||||
margin-top: 22px;
|
||||
font-size: 52px;
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.05em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* ── Period ── */
|
||||
.plan-card__period {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.plan-card--service .plan-card__period {
|
||||
color: #5d6166;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.plan-card--pricing .plan-card__period {
|
||||
color: #5e6167;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
/* ── Features ── */
|
||||
.plan-card__features {
|
||||
margin: 24px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.plan-card--service .plan-card__features {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* Service: bullet style */
|
||||
.plan-card--service .plan-card__features li {
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
color: #34363a;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.plan-card--service .plan-card__features li + li {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.plan-card--service .plan-card__features li::before {
|
||||
content: '•';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 0;
|
||||
color: var(--yellow);
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Pricing: divider style */
|
||||
.plan-card--pricing .plan-card__features {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.plan-card--pricing .plan-card__features li {
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid rgba(17, 20, 24, 0.08);
|
||||
color: #34363a;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ── CTA ── */
|
||||
.plan-card__cta {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin: 28px auto 0;
|
||||
font-family: var(--font-head);
|
||||
}
|
||||
|
||||
/* ── Hover ── */
|
||||
@media (hover: hover) {
|
||||
.plan-card--service:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.055),
|
||||
0 10px 40px rgba(0, 0, 0, 0.08);
|
||||
filter: brightness(1.015);
|
||||
}
|
||||
|
||||
.plan-card--pricing:hover {
|
||||
transform: translateY(-6px) scale(1.012);
|
||||
box-shadow: 0 22px 44px rgba(17, 20, 24, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.plan-card:active {
|
||||
transform: translateY(-2px) scale(0.992);
|
||||
}
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.plan-card {
|
||||
order: var(--mobile-order, 0);
|
||||
padding: 28px 22px;
|
||||
}
|
||||
|
||||
.plan-card--pricing .plan-card__price {
|
||||
font-size: 46px;
|
||||
}
|
||||
|
||||
.plan-card__cta {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,123 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import type { PromiseContent } from '$lib/types';
|
||||
|
||||
export let promise: PromiseContent;
|
||||
|
||||
$: promiseEnhanced = getEnhancedImage(promise.imageUrl);
|
||||
</script>
|
||||
|
||||
<section id="promise">
|
||||
<div class="promise-inner">
|
||||
<div class="promise-text">
|
||||
<h2 class="promise-heading">
|
||||
<span class="promise-heading-desktop">
|
||||
<span class="promise-title-main">{promise.title}</span>
|
||||
<br />
|
||||
<span class="promise-title-highlight">{promise.subtitle}</span>
|
||||
</span>
|
||||
<span class="promise-heading-mobile">
|
||||
<span class="promise-title-main">{promise.title}</span>
|
||||
<span class="promise-title-highlight">{promise.subtitle}</span>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{#each promise.body as paragraph, idx}
|
||||
<p>
|
||||
{paragraph}
|
||||
{#if idx === promise.body.length - 1}
|
||||
<strong>{promise.emphasis}</strong>
|
||||
{/if}
|
||||
</p>
|
||||
{/each}
|
||||
|
||||
<a href={promise.cta.href} class="btn btn-green">{promise.cta.label}</a>
|
||||
</div>
|
||||
|
||||
<div class="promise-img">
|
||||
<div class="promise-img-frame">
|
||||
{#if promiseEnhanced}
|
||||
<enhanced:img
|
||||
src={promiseEnhanced}
|
||||
alt={promise.imageAlt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<img src={promise.imageUrl} alt={promise.imageAlt} loading="lazy" decoding="async" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.promise-heading-desktop {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.promise-heading-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.promise-heading-mobile .promise-title-main,
|
||||
.promise-heading-mobile .promise-title-highlight {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.promise-title-main {
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.promise-title-highlight {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.promise-title-highlight::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: -6px;
|
||||
bottom: -16px;
|
||||
height: 24px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 34' fill='none'%3E%3Cpath d='M4 24C67 10 131 4 198 5c43 1 82 6 118 18' stroke='%23192419' stroke-width='8' stroke-linecap='round'/%3E%3C/svg%3E")
|
||||
center/contain no-repeat;
|
||||
transform-origin: left center;
|
||||
animation: promise-underline-draw 0.9s cubic-bezier(0.22, 1, 0.36, 1) 0.2s both;
|
||||
}
|
||||
|
||||
@keyframes promise-underline-draw {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scaleX(0.2) translateY(6px) rotate(-1.5deg);
|
||||
}
|
||||
|
||||
65% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1.04) translateY(0) rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1) translateY(0) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.promise-heading-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.promise-heading-mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.promise-title-highlight::after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,36 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from 'svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import BookingSection from '$lib/components/BookingSection.svelte';
|
||||
import PricingPlanCard from '$lib/components/PricingPlanCard.svelte';
|
||||
import ServiceHero from '$lib/components/ServiceHero.svelte';
|
||||
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import { decoratePlans } from '$lib/utils/pricing';
|
||||
import type { ServicePageContent, SiteSharedContent } from '$lib/types';
|
||||
|
||||
export let content: SiteSharedContent;
|
||||
export let pageContent: ServicePageContent;
|
||||
export let currentPath = '';
|
||||
let benefitScroller: HTMLDivElement | null = null;
|
||||
|
||||
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 activeBenefitIndex = 0;
|
||||
let mobileBenefitObserver: IntersectionObserver | null = null;
|
||||
|
||||
$: highlightEnhanced = pageContent.highlight ? getEnhancedImage(pageContent.highlight.imageUrl) : null;
|
||||
$: highlightCollageImages =
|
||||
@@ -66,14 +51,90 @@
|
||||
}
|
||||
];
|
||||
|
||||
function scrollBenefits(direction: -1 | 1) {
|
||||
if (!benefitScroller) return;
|
||||
function isMobileViewport() {
|
||||
return typeof window !== 'undefined' && window.innerWidth <= 768;
|
||||
}
|
||||
|
||||
benefitScroller.scrollBy({
|
||||
left: direction * Math.round(benefitScroller.clientWidth * 0.86),
|
||||
behavior: 'smooth'
|
||||
async function scrollBenefits(direction: -1 | 1) {
|
||||
if (!benefitScroller || !isMobileViewport()) return;
|
||||
|
||||
const nextIndex = Math.max(0, Math.min(activeBenefitIndex + direction, benefitCards.length - 1));
|
||||
await scrollBenefitTo(nextIndex);
|
||||
}
|
||||
|
||||
async function scrollBenefitTo(index: number) {
|
||||
if (!benefitScroller || !isMobileViewport()) return;
|
||||
|
||||
const cards = benefitScroller.querySelectorAll<HTMLElement>('.service-benefit-card');
|
||||
const targetCard = cards[index];
|
||||
if (!targetCard) return;
|
||||
|
||||
activeBenefitIndex = index;
|
||||
await tick();
|
||||
targetCard.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'start'
|
||||
});
|
||||
}
|
||||
|
||||
function bindMobileBenefitObserver() {
|
||||
mobileBenefitObserver?.disconnect();
|
||||
|
||||
if (!benefitScroller || !isMobileViewport()) return;
|
||||
|
||||
const cards = benefitScroller.querySelectorAll<HTMLElement>('.service-benefit-card');
|
||||
if (!cards.length) return;
|
||||
|
||||
mobileBenefitObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visibleEntry = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
||||
|
||||
if (!visibleEntry) return;
|
||||
|
||||
const nextIndex = [...cards].indexOf(visibleEntry.target as HTMLElement);
|
||||
if (nextIndex >= 0) {
|
||||
activeBenefitIndex = nextIndex;
|
||||
}
|
||||
},
|
||||
{
|
||||
root: benefitScroller,
|
||||
threshold: [0.6, 0.75, 0.9]
|
||||
}
|
||||
);
|
||||
|
||||
cards.forEach((card) => mobileBenefitObserver?.observe(card));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const handleResize = () => {
|
||||
if (!benefitScroller) return;
|
||||
|
||||
if (isMobileViewport()) {
|
||||
if (activeBenefitIndex === 0) {
|
||||
benefitScroller.scrollTo({ left: 0, behavior: 'auto' });
|
||||
}
|
||||
bindMobileBenefitObserver();
|
||||
void scrollBenefitTo(activeBenefitIndex);
|
||||
} else {
|
||||
mobileBenefitObserver?.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
if (benefitScroller && isMobileViewport()) {
|
||||
benefitScroller.scrollTo({ left: 0, behavior: 'auto' });
|
||||
bindMobileBenefitObserver();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
mobileBenefitObserver?.disconnect();
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="service-page">
|
||||
@@ -89,7 +150,7 @@
|
||||
|
||||
{#if pageContent.highlight}
|
||||
<section use:reveal class="service-highlight reveal-block">
|
||||
<div class="service-inner">
|
||||
<div class="page-inner">
|
||||
<div class:service-highlight-layout-points={pageContent.highlight.points?.length} class="service-highlight-layout">
|
||||
<div class="service-highlight-copy">
|
||||
<p class="eyebrow service-highlight-eyebrow">{pageContent.highlight.eyebrow}</p>
|
||||
@@ -144,7 +205,7 @@
|
||||
{/if}
|
||||
|
||||
<section use:reveal class="service-benefits reveal-block">
|
||||
<div class="service-inner">
|
||||
<div class="page-inner">
|
||||
<div class="service-section-heading">
|
||||
<h2>{pageContent.benefits.title}</h2>
|
||||
{#if pageContent.benefits.intro}
|
||||
@@ -154,8 +215,12 @@
|
||||
|
||||
<div class="service-benefit-shell">
|
||||
<div bind:this={benefitScroller} class="service-benefit-grid">
|
||||
{#each benefitCards as benefit}
|
||||
<article class:service-benefit-card-featured={benefit.featured} class={`service-benefit-card ${benefit.tintClass}`}>
|
||||
{#each benefitCards as benefit, index}
|
||||
<article
|
||||
class:active={index === activeBenefitIndex}
|
||||
class:service-benefit-card-featured={benefit.featured}
|
||||
class={`service-benefit-card ${benefit.tintClass}`}
|
||||
>
|
||||
<div class="service-benefit-icon" aria-hidden="true">
|
||||
<Icon name={benefit.icon ?? 'fas fa-paw'} />
|
||||
</div>
|
||||
@@ -169,10 +234,34 @@
|
||||
</div>
|
||||
|
||||
<div class="service-benefit-mobile-controls" aria-label="Benefit cards navigation">
|
||||
<button type="button" class="service-benefit-mobile-button" aria-label="Previous benefit" on:click={() => scrollBenefits(-1)}>
|
||||
<button
|
||||
type="button"
|
||||
class="service-benefit-mobile-button"
|
||||
aria-label="Previous benefit"
|
||||
disabled={activeBenefitIndex === 0}
|
||||
on:click={() => scrollBenefits(-1)}
|
||||
>
|
||||
<Icon name="fas fa-chevron-left" />
|
||||
</button>
|
||||
<button type="button" class="service-benefit-mobile-button" aria-label="Next benefit" on:click={() => scrollBenefits(1)}>
|
||||
<div class="service-benefit-mobile-pager" aria-label="Current benefit">
|
||||
{#each benefitCards as _, index}
|
||||
<button
|
||||
type="button"
|
||||
class:active={index === activeBenefitIndex}
|
||||
class="service-benefit-mobile-dot"
|
||||
aria-label={`Go to benefit ${index + 1}`}
|
||||
aria-pressed={index === activeBenefitIndex}
|
||||
on:click={() => scrollBenefitTo(index)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="service-benefit-mobile-button"
|
||||
aria-label="Next benefit"
|
||||
disabled={activeBenefitIndex === benefitCards.length - 1}
|
||||
on:click={() => scrollBenefits(1)}
|
||||
>
|
||||
<Icon name="fas fa-chevron-right" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -181,7 +270,7 @@
|
||||
</section>
|
||||
|
||||
<section use:reveal class="service-pricing reveal-block">
|
||||
<div class="service-inner">
|
||||
<div class="page-inner">
|
||||
<div class="service-section-heading">
|
||||
<h2>{pageContent.pricing.title}</h2>
|
||||
{#if pageContent.pricing.intro}
|
||||
@@ -191,27 +280,7 @@
|
||||
|
||||
<div class:service-plan-grid-three={pageContent.pricing.plans.length === 3} class="service-plan-grid">
|
||||
{#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}
|
||||
|
||||
<h3>{plan.title}</h3>
|
||||
<div class="service-plan-price">{plan.price}</div>
|
||||
<p class="service-plan-period">{plan.period}</p>
|
||||
|
||||
<ul class="service-plan-features">
|
||||
{#each plan.features as feature}
|
||||
<li>{feature}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<a class="btn btn-yellow service-plan-cta" href="#newlead">Book a Meet & Greet</a>
|
||||
</article>
|
||||
<PricingPlanCard {plan} variant="service" />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -250,7 +319,7 @@
|
||||
|
||||
{#if showRelatedServices}
|
||||
<section use:reveal class="service-related reveal-block" aria-label="Other services">
|
||||
<div class="service-inner">
|
||||
<div class="page-inner">
|
||||
<div class="service-section-heading">
|
||||
<h2>Explore our other services</h2>
|
||||
</div>
|
||||
@@ -294,14 +363,8 @@
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.service-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
.service-related {
|
||||
padding: 0 0 96px;
|
||||
padding: 0 0 var(--space-section-featured-y);
|
||||
}
|
||||
|
||||
.service-related-grid {
|
||||
@@ -495,7 +558,7 @@
|
||||
}
|
||||
|
||||
.service-highlight {
|
||||
padding: 80px 0 96px;
|
||||
padding: var(--space-section-page-y) 0 var(--space-section-featured-y);
|
||||
}
|
||||
|
||||
.service-highlight-layout {
|
||||
@@ -556,7 +619,7 @@
|
||||
|
||||
.service-pricing,
|
||||
.service-benefits {
|
||||
padding: 0 0 96px;
|
||||
padding: 0 0 var(--space-section-featured-y);
|
||||
}
|
||||
|
||||
.service-benefit-shell {
|
||||
@@ -564,7 +627,8 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.service-benefit-mobile-controls {
|
||||
.service-benefit-mobile-controls,
|
||||
.service-benefit-mobile-pager {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -588,7 +652,6 @@
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.service-plan-card,
|
||||
.service-benefit-card {
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(247, 248, 246, 0.98) 100%);
|
||||
@@ -603,63 +666,10 @@
|
||||
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);
|
||||
}
|
||||
|
||||
.service-plan-ribbon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--yellow);
|
||||
color: #000;
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.service-plan-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.055),
|
||||
0 10px 40px rgba(0, 0, 0, 0.08);
|
||||
filter: brightness(1.015);
|
||||
}
|
||||
}
|
||||
|
||||
.service-plan-card:active,
|
||||
.service-benefit-card:active {
|
||||
transform: translateY(-2px) scale(0.992);
|
||||
}
|
||||
|
||||
:global(.reveal-ready.reveal-block) {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, var(--reveal-distance, 24px), 0);
|
||||
transition:
|
||||
opacity 0.55s ease,
|
||||
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
transition-delay: var(--reveal-delay, 0ms);
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block) {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.service-plan-card h3,
|
||||
.service-benefit-card h3 {
|
||||
margin: 0;
|
||||
font-family: var(--font-head);
|
||||
@@ -668,59 +678,6 @@
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.service-plan-price {
|
||||
margin-top: 20px;
|
||||
font-family: var(--font-head);
|
||||
font-size: 44px;
|
||||
line-height: 1;
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
.service-plan-period {
|
||||
margin: 8px 0 0;
|
||||
color: #5d6166;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.service-plan-features {
|
||||
margin: 24px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.service-plan-features li {
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
color: #34363a;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.service-plan-features li + li {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.service-plan-features li::before {
|
||||
content: '•';
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
top: 0;
|
||||
color: var(--yellow);
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.service-plan-cta {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin: 28px auto 0;
|
||||
font-family: var(--font-head);
|
||||
}
|
||||
|
||||
.service-plan-mobile-cta {
|
||||
display: none;
|
||||
}
|
||||
@@ -1030,9 +987,6 @@
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.service-inner {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.service-plan-grid,
|
||||
.service-plan-grid-three,
|
||||
@@ -1043,16 +997,18 @@
|
||||
|
||||
.service-benefit-grid {
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(272px, 84vw);
|
||||
grid-auto-columns: calc(100% - 64px);
|
||||
grid-template-columns: none;
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-x: contain;
|
||||
scroll-snap-type: x proximity;
|
||||
scroll-padding-left: 24px;
|
||||
padding: 0 24px 8px 0;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-padding-left: 8px;
|
||||
padding: 0 14px 8px 8px;
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-x pinch-zoom;
|
||||
}
|
||||
|
||||
.service-benefit-grid::-webkit-scrollbar {
|
||||
@@ -1060,20 +1016,34 @@
|
||||
}
|
||||
|
||||
.service-benefit-card {
|
||||
padding: 24px 22px 22px;
|
||||
min-height: clamp(230px, 42svh, 320px);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 20px 18px 22px;
|
||||
border-radius: 24px;
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
touch-action: pan-x;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
transition:
|
||||
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.22s ease,
|
||||
border-color 0.22s ease,
|
||||
background 0.24s ease;
|
||||
}
|
||||
|
||||
.service-plan-card {
|
||||
order: var(--mobile-order, 0);
|
||||
.service-benefit-card.active {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
|
||||
0 12px 24px rgba(17, 20, 24, 0.08);
|
||||
}
|
||||
|
||||
.service-highlight {
|
||||
padding-top: 56px;
|
||||
padding-bottom: 72px;
|
||||
.service-benefit-card:last-child {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
|
||||
.service-highlight-layout {
|
||||
gap: 24px;
|
||||
}
|
||||
@@ -1103,7 +1073,7 @@
|
||||
.service-pricing,
|
||||
.service-benefits,
|
||||
.service-related {
|
||||
padding-bottom: 72px;
|
||||
padding-bottom: var(--space-section-featured-y);
|
||||
}
|
||||
|
||||
.service-section-heading h2 {
|
||||
@@ -1111,27 +1081,30 @@
|
||||
}
|
||||
|
||||
.service-benefit-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-bottom: 18px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 18px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.service-benefit-card h3 {
|
||||
max-width: none;
|
||||
font-size: 20px;
|
||||
font-size: 21px;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.service-benefit-card p {
|
||||
margin-top: 12px;
|
||||
font-size: 15px;
|
||||
line-height: 1.68;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.service-benefit-mobile-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.service-benefit-mobile-button {
|
||||
@@ -1147,9 +1120,41 @@
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
|
||||
transition:
|
||||
background 0.18s ease,
|
||||
opacity 0.18s ease,
|
||||
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.service-benefit-mobile-button:disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.service-benefit-mobile-pager {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.service-benefit-mobile-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.2);
|
||||
transition:
|
||||
width 0.22s ease,
|
||||
background 0.22s ease,
|
||||
transform 0.22s ease;
|
||||
}
|
||||
|
||||
.service-benefit-mobile-dot.active {
|
||||
width: 28px;
|
||||
background: var(--yellow);
|
||||
}
|
||||
|
||||
.service-benefit-mobile-button:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
@@ -1180,9 +1185,6 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.service-plan-cta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.service-plan-mobile-cta {
|
||||
display: flex;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import confetti from 'canvas-confetti';
|
||||
import ModalShell from '$lib/components/ModalShell.svelte';
|
||||
|
||||
export let firstName: string;
|
||||
export let petName: string;
|
||||
@@ -44,20 +45,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="modal-backdrop"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={isGeneralEnquiry ? 'Enquiry confirmed' : 'Booking confirmed'}
|
||||
on:click|self={onClose}
|
||||
on:keydown={(e) => e.key === 'Escape' && onClose()}
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="modal-card">
|
||||
<button class="modal-close" type="button" aria-label="Close" on:click={onClose}>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<ModalShell {onClose} ariaLabel={isGeneralEnquiry ? 'Enquiry confirmed' : 'Booking confirmed'}>
|
||||
<div class="modal-paw" aria-hidden="true">🐾</div>
|
||||
|
||||
<h2 class="modal-heading">
|
||||
@@ -94,59 +82,9 @@
|
||||
<button class="modal-btn" type="button" on:click={onClose}>
|
||||
Sounds great!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalShell>
|
||||
|
||||
<style>
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(10, 20, 10, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
animation: backdrop-in 0.25s ease;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 52px 48px 44px;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 24px 80px rgba(10, 20, 10, 0.22);
|
||||
text-align: center;
|
||||
animation: card-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 20px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #f2f2f0;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background: #e8e8e4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-paw {
|
||||
font-size: 52px;
|
||||
line-height: 1;
|
||||
@@ -206,18 +144,4 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@keyframes backdrop-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes card-in {
|
||||
from { opacity: 0; transform: scale(0.88) translateY(16px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes bounce-in {
|
||||
from { opacity: 0; transform: scale(0.4); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
export let values: IconCard[];
|
||||
let valuesScroller: HTMLDivElement | undefined;
|
||||
let activeIndex = 0;
|
||||
let mobileCardsObserver: IntersectionObserver | null = null;
|
||||
|
||||
$: orderedValues = values
|
||||
.map((value, index) => ({ value, index }))
|
||||
@@ -24,30 +26,102 @@
|
||||
return typeof window !== 'undefined' && window.innerWidth <= 768;
|
||||
}
|
||||
|
||||
function cardScrollLeft(card: HTMLElement) {
|
||||
return Math.max(0, card.offsetLeft - 8);
|
||||
}
|
||||
|
||||
async function scrollValues(direction: 1 | -1) {
|
||||
if (!valuesScroller || !isMobileViewport()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstCard = valuesScroller.querySelector<HTMLElement>('.value-card');
|
||||
if (!firstCard) {
|
||||
return;
|
||||
const nextIndex = Math.max(0, Math.min(activeIndex + direction, orderedValues.length - 1));
|
||||
await scrollToValue(nextIndex, 'smooth');
|
||||
}
|
||||
|
||||
const cardStyle = window.getComputedStyle(valuesScroller);
|
||||
const gap = Number.parseFloat(cardStyle.columnGap || cardStyle.gap || '0') || 0;
|
||||
const step = firstCard.offsetWidth + gap;
|
||||
|
||||
await tick();
|
||||
valuesScroller.scrollBy({ left: direction * step, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
async function scrollToValue(index: number, behavior: ScrollBehavior = 'smooth') {
|
||||
if (!valuesScroller || !isMobileViewport()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cards = valuesScroller.querySelectorAll<HTMLElement>('.value-card');
|
||||
const targetCard = cards[index];
|
||||
if (!targetCard) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeIndex = index;
|
||||
await tick();
|
||||
valuesScroller.scrollTo({
|
||||
left: cardScrollLeft(targetCard),
|
||||
behavior
|
||||
});
|
||||
}
|
||||
|
||||
function bindMobileCardObserver() {
|
||||
mobileCardsObserver?.disconnect();
|
||||
|
||||
if (!valuesScroller || !isMobileViewport()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cards = valuesScroller.querySelectorAll<HTMLElement>('.value-card');
|
||||
if (!cards.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
mobileCardsObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visibleEntry = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
||||
|
||||
if (!visibleEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = cards.length ? [...cards].indexOf(visibleEntry.target as HTMLElement) : -1;
|
||||
if (nextIndex >= 0) {
|
||||
activeIndex = nextIndex;
|
||||
}
|
||||
},
|
||||
{
|
||||
root: valuesScroller,
|
||||
threshold: [0.6, 0.75, 0.9]
|
||||
}
|
||||
);
|
||||
|
||||
cards.forEach((card) => mobileCardsObserver?.observe(card));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const handleResize = () => {
|
||||
if (!valuesScroller) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMobileViewport()) {
|
||||
if (activeIndex === 0) {
|
||||
valuesScroller.scrollTo({ left: 0, behavior: 'auto' });
|
||||
}
|
||||
bindMobileCardObserver();
|
||||
void scrollToValue(activeIndex, 'auto');
|
||||
} else {
|
||||
mobileCardsObserver?.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
if (valuesScroller && isMobileViewport()) {
|
||||
valuesScroller.scrollTo({ left: 0, behavior: 'auto' });
|
||||
bindMobileCardObserver();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
mobileCardsObserver?.disconnect();
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -61,8 +135,8 @@
|
||||
|
||||
<div class="values-shell">
|
||||
<div bind:this={valuesScroller} class="values-grid">
|
||||
{#each orderedValues as value}
|
||||
<div class="value-card">
|
||||
{#each orderedValues as value, index}
|
||||
<div class:active={index === activeIndex} class="value-card">
|
||||
<div class="value-icon-wrap">
|
||||
<Icon name={value.icon} className="value-card-icon" />
|
||||
</div>
|
||||
@@ -75,10 +149,34 @@
|
||||
</div>
|
||||
|
||||
<div class="values-mobile-controls" aria-label="Value cards navigation">
|
||||
<button type="button" class="values-mobile-button" aria-label="Previous value" on:click={() => scrollValues(-1)}>
|
||||
<button
|
||||
type="button"
|
||||
class="values-mobile-button"
|
||||
aria-label="Previous value"
|
||||
disabled={activeIndex === 0}
|
||||
on:click={() => scrollValues(-1)}
|
||||
>
|
||||
<Icon name="fas fa-chevron-left" />
|
||||
</button>
|
||||
<button type="button" class="values-mobile-button" aria-label="Next value" on:click={() => scrollValues(1)}>
|
||||
<div class="values-mobile-pager" aria-label="Current value">
|
||||
{#each orderedValues as _, index}
|
||||
<button
|
||||
type="button"
|
||||
class:active={index === activeIndex}
|
||||
class="values-mobile-dot"
|
||||
aria-label={`Go to value ${index + 1}`}
|
||||
aria-pressed={index === activeIndex}
|
||||
on:click={() => scrollToValue(index)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="values-mobile-button"
|
||||
aria-label="Next value"
|
||||
disabled={activeIndex === orderedValues.length - 1}
|
||||
on:click={() => scrollValues(1)}
|
||||
>
|
||||
<Icon name="fas fa-chevron-right" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -117,21 +215,27 @@
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.values-shell {
|
||||
margin-top: 32px;
|
||||
margin-top: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.values-grid {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(272px, 84vw);
|
||||
grid-auto-columns: calc(100% - 64px);
|
||||
grid-template-columns: none;
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
margin-top: 0;
|
||||
border-top: none;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-x: contain;
|
||||
scroll-snap-type: x proximity;
|
||||
scroll-padding-left: 24px;
|
||||
padding: 0 24px 8px 0;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-padding-left: 8px;
|
||||
padding: 0 14px 8px 8px;
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-x pinch-zoom;
|
||||
}
|
||||
|
||||
.values-grid::-webkit-scrollbar {
|
||||
@@ -140,9 +244,10 @@
|
||||
|
||||
.values-mobile-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.values-mobile-button {
|
||||
@@ -158,19 +263,116 @@
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
transition:
|
||||
background 0.18s ease,
|
||||
opacity 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.values-mobile-button:disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.values-mobile-pager {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.values-mobile-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
transition:
|
||||
width 0.22s ease,
|
||||
background 0.22s ease,
|
||||
transform 0.22s ease;
|
||||
}
|
||||
|
||||
.values-mobile-dot.active {
|
||||
width: 28px;
|
||||
background: var(--yellow);
|
||||
}
|
||||
|
||||
.value-card {
|
||||
min-height: 100%;
|
||||
padding: 24px 22px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
min-height: clamp(230px, 42svh, 320px);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 20px 18px 22px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.07));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||
0 6px 16px rgba(0, 0, 0, 0.06);
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 14px;
|
||||
transition:
|
||||
background 0.24s ease,
|
||||
box-shadow 0.24s ease,
|
||||
border-color 0.24s ease;
|
||||
touch-action: pan-x;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.value-card.active {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.09));
|
||||
border-color: rgba(255, 255, 255, 0.16);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||
0 10px 22px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.value-card:nth-child(odd) {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.value-card:last-child {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.value-icon-wrap {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 18px;
|
||||
margin-top: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.value-card .value-card-icon {
|
||||
font-size: 23px;
|
||||
}
|
||||
|
||||
.value-text {
|
||||
max-width: 30ch;
|
||||
min-width: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.value-text h3 {
|
||||
margin-bottom: 8px;
|
||||
font-size: 21px;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.value-card p {
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.values-eyebrow {
|
||||
|
||||
@@ -53,14 +53,15 @@ export const homepageContent: HomePageContent = {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
promise: {
|
||||
title: 'Meet Aless,',
|
||||
subtitle: 'the heart of Goodwalk',
|
||||
founderStory: {
|
||||
title: 'Not just dog walking.',
|
||||
subtitle: 'Built around trust.',
|
||||
body: [
|
||||
'Goodwalk was built for owners who want more than a basic walk. Alessandra leads the business with a calm, hands-on approach shaped by years of experience, a love of small dogs, and a real focus on trust, routine, and safety.',
|
||||
"From house keys to nervous first walks, we take the responsibility seriously. You'll know who is walking your dog, your dog will know who is at the door, and you'll get a reliable team that treats your dog like family. Ready to join the"
|
||||
'Most dog walking companies sell walks. Goodwalk was built for owners who want a calmer, more personal experience for their dog, especially small dogs who thrive on routine, familiarity, and gentle handling.',
|
||||
'That means familiar walkers, safe group dynamics, reliable communication, and a team your dog genuinely builds a relationship with. We know we are not just collecting dogs for a walk. We are being trusted with part of your family and access to your home.',
|
||||
'You know exactly who is caring for your dog. Your dog knows who is at the door. And you come home to a calmer, happier dog. Ready to'
|
||||
],
|
||||
emphasis: 'TINY GANG?',
|
||||
emphasis: 'join the Tiny Gang?',
|
||||
cta: { label: 'Book a free Meet & Greet', href: '/contact-us', variant: 'green' },
|
||||
imageUrl: '/images/goodwalk-dog-walker-alessandra.png',
|
||||
imageAlt: 'Alessandra from Goodwalk with a dog in Auckland'
|
||||
|
||||
@@ -84,3 +84,18 @@ textarea {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* use:reveal action — applied via Svelte action on section elements */
|
||||
.reveal-ready.reveal-block {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, var(--reveal-distance, 24px), 0);
|
||||
transition:
|
||||
opacity 0.55s ease,
|
||||
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
transition-delay: var(--reveal-delay, 0ms);
|
||||
}
|
||||
|
||||
.reveal-visible.reveal-block {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
@@ -91,11 +91,18 @@ nav,
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Shared inner wrapper for page-level sections */
|
||||
.page-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-container-x);
|
||||
}
|
||||
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
padding: 16px 50px;
|
||||
padding: 16px var(--space-container-x);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@media (max-width: 1024px) {
|
||||
nav,
|
||||
.mobile-menu {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-left: var(--space-container-x-tablet);
|
||||
padding-right: var(--space-container-x-tablet);
|
||||
}
|
||||
|
||||
.promise-inner,
|
||||
@@ -11,13 +11,13 @@
|
||||
.testimonials-inner,
|
||||
.info-inner,
|
||||
.form-inner {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-left: var(--space-container-x-tablet);
|
||||
padding-right: var(--space-container-x-tablet);
|
||||
}
|
||||
|
||||
footer {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-left: var(--space-container-x-tablet);
|
||||
padding-right: var(--space-container-x-tablet);
|
||||
}
|
||||
|
||||
.hero-text h1 {
|
||||
@@ -483,7 +483,7 @@
|
||||
|
||||
.promise-inner {
|
||||
flex-direction: column;
|
||||
padding: 0 24px;
|
||||
padding: 0 var(--space-container-x-mobile);
|
||||
}
|
||||
|
||||
.promise-text {
|
||||
@@ -494,7 +494,7 @@
|
||||
order: 2;
|
||||
flex: none;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
max-width: 360px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -700,7 +700,11 @@
|
||||
.testimonials-inner,
|
||||
.info-inner,
|
||||
.form-inner {
|
||||
padding: 0 24px;
|
||||
padding: 0 var(--space-container-x-mobile);
|
||||
}
|
||||
|
||||
.page-inner {
|
||||
padding: 0 var(--space-container-x-mobile);
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
@@ -708,7 +712,7 @@
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 40px 24px 24px;
|
||||
padding: 40px var(--space-container-x-mobile) var(--space-container-x-mobile);
|
||||
}
|
||||
|
||||
.footer-inner {
|
||||
@@ -820,4 +824,9 @@
|
||||
:root {
|
||||
--sh-copy-left-pad: clamp(420px, 22vw, 550px);
|
||||
}
|
||||
|
||||
.hero-img img {
|
||||
width: 54%;
|
||||
object-position: center 8%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
/* Featured sections — more breathing room */
|
||||
#promise,
|
||||
#services {
|
||||
padding: 96px 0;
|
||||
padding: var(--space-section-featured-y) 0;
|
||||
}
|
||||
|
||||
/* Supporting sections */
|
||||
#values,
|
||||
#testimonials,
|
||||
#info {
|
||||
padding: 72px 0;
|
||||
padding: var(--space-section-support-y) 0;
|
||||
}
|
||||
|
||||
/* Booking / lead form — neutral */
|
||||
#newlead {
|
||||
padding: 80px 0;
|
||||
padding: var(--space-section-form-y) 0;
|
||||
}
|
||||
|
||||
#hero {
|
||||
@@ -53,7 +53,7 @@
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 0 50px 32px;
|
||||
padding: 0 var(--space-container-x) var(--space-hero-inner-bottom);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
:root {
|
||||
/* Spacing scale */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-7: 32px;
|
||||
--space-8: 40px;
|
||||
--space-9: 48px;
|
||||
--space-10: 56px;
|
||||
--space-11: 64px;
|
||||
--space-12: 72px;
|
||||
--space-13: 80px;
|
||||
--space-14: 96px;
|
||||
|
||||
/* Brand greens */
|
||||
--gw-green: #213021;
|
||||
--green-mid: #2d4230; /* hover states, subtle accents */
|
||||
@@ -17,12 +33,30 @@
|
||||
|
||||
/* Layout */
|
||||
--max-w: 1280px;
|
||||
--space-container-x: var(--space-9);
|
||||
--space-container-x-tablet: 30px;
|
||||
--space-container-x-mobile: var(--space-6);
|
||||
--space-section-featured-y: var(--space-14);
|
||||
--space-section-support-y: var(--space-12);
|
||||
--space-section-form-y: var(--space-13);
|
||||
--space-section-page-y: var(--space-13);
|
||||
--space-section-mobile-y: var(--space-11);
|
||||
--space-hero-inner-bottom: var(--space-7);
|
||||
|
||||
/* Typography */
|
||||
--font-body: 'Readex Pro', sans-serif;
|
||||
--font-head: 'Unbounded', sans-serif;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--space-section-featured-y: var(--space-section-mobile-y);
|
||||
--space-section-support-y: var(--space-section-mobile-y);
|
||||
--space-section-form-y: var(--space-section-mobile-y);
|
||||
--space-section-page-y: var(--space-section-mobile-y);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1800px) {
|
||||
:root {
|
||||
--max-w: 1408px;
|
||||
|
||||
+2
-2
@@ -57,7 +57,7 @@ export interface IntroContent {
|
||||
reviewCta: CallToAction;
|
||||
}
|
||||
|
||||
export interface PromiseContent {
|
||||
export interface FounderStoryContent {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
body: string[];
|
||||
@@ -290,7 +290,7 @@ export interface HomePageContent {
|
||||
navigation: NavigationContent;
|
||||
hero: HeroContent;
|
||||
intro: IntroContent;
|
||||
promise: PromiseContent;
|
||||
founderStory: FounderStoryContent;
|
||||
services: IconCard[];
|
||||
howItWorks: HowItWorksContent;
|
||||
values: IconCard[];
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export function numericPrice(price: string): number {
|
||||
const value = Number(price.replace(/[^0-9.]/g, ''));
|
||||
return Number.isFinite(value) ? value : Number.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
export 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
|
||||
}));
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
import InfoSection from '$lib/components/InfoSection.svelte';
|
||||
import InstagramSection from '$lib/components/InstagramSection.svelte';
|
||||
import BookingSection from '$lib/components/BookingSection.svelte';
|
||||
import PromiseSection from '$lib/components/PromiseSection.svelte';
|
||||
import FounderStorySection from '$lib/components/FounderStorySection.svelte';
|
||||
import ServicesSection from '$lib/components/ServicesSection.svelte';
|
||||
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte';
|
||||
import ValuesSection from '$lib/components/ValuesSection.svelte';
|
||||
@@ -130,7 +130,7 @@
|
||||
|
||||
<Header navigation={content.navigation} />
|
||||
<HeroSection hero={content.hero} reviewCta={content.intro.reviewCta} />
|
||||
<PromiseSection promise={content.promise} />
|
||||
<FounderStorySection founderStory={content.founderStory} />
|
||||
<ServicesSection services={content.services} />
|
||||
<HowItWorksSection content={content.howItWorks} />
|
||||
<TestimonialsSection testimonials={content.testimonials} seedKey="/" />
|
||||
|
||||
Reference in New Issue
Block a user