Files
gw-svelte/src/lib/components/BookingSection.svelte
T
2026-05-18 09:43:29 +12:00

2388 lines
70 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { onMount } from 'svelte';
import Icon from '$lib/components/Icon.svelte';
import { reveal } from '$lib/actions/reveal';
import type { BookingContent } from '$lib/types';
type SuccessModalComponentType = typeof import('$lib/components/SuccessModal.svelte').default;
type ErrorModalComponentType = typeof import('$lib/components/ErrorModal.svelte').default;
export let booking: BookingContent;
export let allowGeneralEnquiry = false;
export let variant: 'default' | 'minimal-premium' | 'card-stepper' | 'contact-modern' = 'default';
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;
}
> = {
'Tiny Gang Pack Walks': {
intro:
'Tell us about your dog, your suburb, and how they are around other dogs so we can see whether Tiny Gang Pack Walks are the right fit.',
messageLabel: 'A bit about your dog',
messagePlaceholder:
'For example: age, confidence around other dogs, recall, and anything else that would help us place them well.'
},
'1:1 Walks': {
intro:
'Tell us about your dog, your suburb, and what you want from one-to-one walks so we can plan the right routine.',
messageLabel: 'What your dog needs',
messagePlaceholder:
'For example: size, pace, lead manners, confidence, and anything else that would help us tailor a one-to-one walk.'
},
'Puppy Visits': {
intro:
'Tell us about your puppy, your suburb, and the support you need at home so we can plan the right visit.',
messageLabel: 'A bit about your puppy',
messagePlaceholder:
'For example: age, routine, toilet needs, feeding schedule, and anything important we should know before visiting.'
}
};
const bookingAvatarDogs = [
{ image: '/images/archie-goodwalk-dog-walking-review-auckland.webp', alt: 'Archie' },
{ image: '/images/monty-goodwalk-dog-walking-review-auckland.webp', alt: 'Monty' },
{ image: '/images/otis-goodwalk-dog-walking-review-auckland.webp', alt: 'Otis' }
];
let step = 1;
$: headingParts = splitBookingTitle(booking.title);
let enquiryType: EnquiryType = 'booking';
let fullName = '';
let email = '';
let phone = '';
let petName = '';
let location = '';
let message = '';
let selectedServices: string[] = [];
let website = '';
let formStartedAt = 0;
let visitStartedAt = 0;
let pageEnteredAt = 0;
let firstInteractionAt = 0;
let sendClickedAt = 0;
let stepChanges = 0;
let journey: string[] = [];
let fullNameInput: HTMLInputElement;
let emailInput: HTMLInputElement;
let phoneInput: HTMLInputElement;
let petNameInput: HTMLInputElement;
let locationInput: HTMLInputElement;
let errors: Record<string, string> = {};
let submitting = false;
let submitted = false;
let showErrorModal = false;
let submitErrorDetail = '';
let showServicePicker = false;
let SuccessModalComponent: SuccessModalComponentType | null = null;
let ErrorModalComponent: ErrorModalComponentType | null = null;
async function ensureSuccessModal() {
if (SuccessModalComponent) return;
SuccessModalComponent = (await import('$lib/components/SuccessModal.svelte')).default;
}
async function ensureErrorModal() {
if (ErrorModalComponent) return;
ErrorModalComponent = (await import('$lib/components/ErrorModal.svelte')).default;
}
function validateEmail(raw: string): string {
const value = raw.trim();
if (!value) return 'Please enter your email address';
if (!value.includes('@')) return 'Email is missing the @ sign';
const [local, ...domainParts] = value.split('@');
const domain = domainParts.join('@');
if (domainParts.length > 1) return 'Email can only contain one @ sign';
if (!local) return 'Please add the part before the @';
if (!domain) return 'Please add a domain after the @, like @gmail.com';
if (!domain.includes('.')) return 'Please include a domain ending, like @gmail.com';
const tld = domain.split('.').pop() ?? '';
if (tld.length < 2) return 'That domain ending looks too short';
if (/\s/.test(value)) return 'Email cannot contain spaces';
const re = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
if (!re.test(value)) return 'That email doesnt look quite right';
return '';
}
const defaultDogIntro =
'Tell us about your dog and your suburb so we can plan the right Meet & Greet.';
const defaultGeneralIntro =
'Need help with something else? Choose general enquiry and tell us what you need.';
const defaultGeneralSubtitle =
'Almost there, just your contact details so we can reply properly.';
$: 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') {
enquiryType = 'booking';
}
$: isGeneralEnquiry = allowGeneralEnquiry && enquiryType === 'general';
$: ownerIntro = isGeneralEnquiry
? booking.generalSubtitle?.trim() || defaultGeneralSubtitle
: booking.subtitle;
$: enquiryModeLegend = 'What do you need help with?';
$: ownerStepLabel = booking.ownerStepLabel?.trim() || 'Owner Details';
$: 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 will point you in the right direction.'
: 'Start with a few details about your dog. We will come back within 24 hours with the right next step.';
$: detailsMessageLabel = isGeneralEnquiry
? 'How can we help?'
: activeServicePrompt?.messageLabel || 'Anything we should know?';
$: detailsMessagePlaceholder = isGeneralEnquiry
? 'Tell us what you need help with and any details that would help us reply properly.'
: activeServicePrompt?.messagePlaceholder || 'Tell us about your dog, any support they need, and anything else that would help us prepare.';
function capitalizeFirst(value: string) {
if (!value) return value;
return value.charAt(0).toLocaleUpperCase() + value.slice(1);
}
$: trimmedPetName = petName.trim();
$: displayPetName = capitalizeFirst(trimmedPetName);
$: successPetName = displayPetName || 'your dog';
$: serviceChoiceLocked = !isGeneralEnquiry && selectedServices.length === 1 && !showServicePicker;
$: isCardStepper = variant === 'card-stepper';
$: isContactModern = variant === 'contact-modern';
$: usesStepperShell = isCardStepper || isContactModern;
$: isSingleStep = usesStepperShell;
$: detailsStepComplete = isGeneralEnquiry
? Boolean(message.trim())
: Boolean(petName.trim() && location.trim() && (!hasServices || selectedServices.length > 0));
$: bookingSocialProofTitle = isGeneralEnquiry
? 'A real reply from a real person.'
: trimmedPetName
? `${displayPetName} could be a great fit for Goodwalk.`
: 'Trusted by Goodwalk dog owners across Auckland.';
$: bookingSocialProofNote = isGeneralEnquiry
? 'Tell us what you need and we will reply with the right next step.'
: primarySelectedService
? `${primarySelectedService}, planned around your dog rather than a generic routine.`
: 'Tell us a little about your dog and we will help you find the right fit.';
$: bookingStepContext = step === 1
? isGeneralEnquiry
? 'Start with the basics and we will send this to the right place.'
: 'Start with your dog, then we will ask where to reply.'
: fullName.trim()
? `Almost there, ${fullName.trim().split(' ')[0]}.`
: 'Almost there, where should we reply?';
$: bookingStepAside = isGeneralEnquiry
? 'A friendly reply, usually within 24 hours.'
: primarySelectedService
? `${primarySelectedService}, with a calm and personal start.`
: '';
$: detailsStepStatus = step === 1 ? 'Current step' : 'Completed';
$: ownerStepStatus = step === 2 ? 'Current step' : detailsStepComplete ? 'Ready next' : 'Complete step 1 first';
$: if (submitted) {
ensureSuccessModal();
}
$: if (showErrorModal) {
ensureErrorModal();
}
onMount(() => {
const now = Date.now();
formStartedAt = now;
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) {
const trimmed = title.trim();
const lastSpace = trimmed.lastIndexOf(' ');
if (lastSpace === -1) {
return { plain: trimmed, highlight: '' };
}
return {
plain: trimmed.slice(0, lastSpace),
highlight: trimmed.slice(lastSpace + 1)
};
}
function clearError(field: string) {
if (errors[field]) {
errors = { ...errors, [field]: '' };
}
}
function readOrCreateVisitStartedAt(fallback: number) {
try {
const raw = window.sessionStorage.getItem(visitStartedStorageKey);
const parsed = raw ? Number(raw) : NaN;
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
window.sessionStorage.setItem(visitStartedStorageKey, String(fallback));
} catch {
return fallback;
}
return fallback;
}
function updateJourneySnapshot(pathname: string, search: string) {
const nextEntry = `${pathname}${search}`;
try {
const raw = window.sessionStorage.getItem(journeyStorageKey);
const previous = raw ? (JSON.parse(raw) as string[]) : [];
const cleaned = previous.filter((value) => typeof value === 'string' && value.trim());
const deduped = cleaned[cleaned.length - 1] === nextEntry ? cleaned : [...cleaned, nextEntry];
const nextJourney = deduped.slice(-maxJourneyEntries);
window.sessionStorage.setItem(journeyStorageKey, JSON.stringify(nextJourney));
return nextJourney;
} catch {
return [nextEntry];
}
}
function noteInteraction() {
if (!firstInteractionAt) {
firstInteractionAt = Date.now();
}
}
function setStep(nextStep: number, trackTransition = false) {
if (step !== nextStep && trackTransition) {
stepChanges += 1;
}
step = nextStep;
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];
showServicePicker = false;
try {
window.sessionStorage.removeItem(requestedServiceStorageKey);
} catch {
// Ignore storage cleanup failures.
}
}
function sortSelectedServices(services: string[]) {
return [...services].sort((a, b) => {
const indexA = booking.serviceOptions.indexOf(a);
const indexB = booking.serviceOptions.indexOf(b);
if (indexA === -1 && indexB === -1) return a.localeCompare(b);
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
});
}
function toggleService(service: string, checked: boolean) {
noteInteraction();
clearError('services');
if (checked) {
selectedServices = [service];
showServicePicker = false;
return;
}
selectedServices = selectedServices.filter((item) => item !== service);
}
function setEnquiryType(nextType: EnquiryType) {
noteInteraction();
enquiryType = nextType;
if (nextType === 'general') {
petName = '';
location = '';
selectedServices = [];
showServicePicker = false;
}
errors = {};
}
function serviceInputId(service: string) {
return `booking-service-${service
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')}`;
}
function validateOwnerStep(): boolean {
const next: Record<string, string> = {};
if (!fullName.trim()) next.fullName = 'Please enter your full name';
const emailError = validateEmail(email);
if (emailError) next.email = emailError;
if (!phone.trim()) next.phone = 'Please enter your contact number';
errors = next;
if (next.fullName) {
fullNameInput?.focus();
return false;
}
if (next.email) {
emailInput?.focus();
return false;
}
if (next.phone) {
phoneInput?.focus();
return false;
}
return true;
}
function validateDetailsStep(): boolean {
const next: Record<string, string> = {};
if (isGeneralEnquiry) {
if (!message.trim()) next.message = 'Please tell us how we can help';
} else {
if (!petName.trim()) next.petName = "Please enter your dog's name";
if (!location.trim()) next.location = 'Please enter your location';
if (hasServices && selectedServices.length === 0) {
next.services = 'Please choose a service so we can guide you properly';
}
}
errors = next;
if (next.petName) {
petNameInput?.focus();
return false;
}
if (next.location) {
locationInput?.focus();
return false;
}
if (next.message) {
return false;
}
if (next.services) {
return false;
}
return true;
}
function goToOwnerStep() {
noteInteraction();
if (!validateDetailsStep()) return;
setStep(2, true);
}
async function handleSubmit(event: SubmitEvent) {
event.preventDefault();
if (isSingleStep) {
if (!validateDetailsStep()) return;
if (!validateOwnerStep()) return;
} else {
if (step === 1) {
goToOwnerStep();
return;
}
if (!validateOwnerStep()) {
return;
}
}
errors = {};
noteInteraction();
sendClickedAt = Date.now();
submitting = true;
submitErrorDetail = '';
showErrorModal = false;
try {
const res = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enquiryType,
fullName,
email,
phone,
petName: isGeneralEnquiry ? '' : petName,
location: isGeneralEnquiry ? '' : location,
message,
services: isGeneralEnquiry ? [] : selectedServices,
website,
formStartedAt,
visitStartedAt,
pageEnteredAt,
firstInteractionAt,
sendClickedAt,
stepChanges,
journey,
referrer: document.referrer,
page: window.location.href
})
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
const detail =
typeof body?.detail === 'string'
? body.detail
: body?.detail?.message ?? body?.message ?? `Server responded with ${res.status}`;
throw new Error(detail);
}
submitted = true;
} catch (err: unknown) {
submitErrorDetail = err instanceof Error ? err.message : String(err);
showErrorModal = true;
} finally {
submitting = false;
}
}
</script>
<section
id="newlead"
use:reveal={{ delay: 70 }}
class="booking-shell reveal-block"
class:booking-shell--minimal-premium={variant === 'minimal-premium'}
class:booking-shell--card-stepper={variant === 'card-stepper'}
class:booking-shell--contact-modern={variant === 'contact-modern'}
>
<div class="form-inner">
{#if submitted && SuccessModalComponent}
<svelte:component
this={SuccessModalComponent}
firstName={fullName.split(' ')[0]}
petName={successPetName}
{email}
{enquiryType}
onClose={() => (submitted = false)}
/>
{/if}
{#if showErrorModal && ErrorModalComponent}
<svelte:component
this={ErrorModalComponent}
detail={submitErrorDetail}
{enquiryType}
onClose={() => (showErrorModal = false)}
onRetry={() => (showErrorModal = false)}
/>
{/if}
<div class="booking-header">
<span class="eyebrow 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>
{#if !usesStepperShell}
<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 &amp; Greet
</span>
</div>
<div class="booking-stepper" aria-label="Booking form steps">
<button
type="button"
class:active={step === 1}
class="booking-step"
on:click={() => setStep(1, step !== 1)}
>
<span class="booking-step-number">1</span>
<span class="booking-step-label">{detailsStepLabel}</span>
</button>
<span class="booking-step-divider" aria-hidden="true"></span>
<button
type="button"
class:active={step === 2}
class="booking-step"
disabled={!detailsStepComplete}
aria-disabled={!detailsStepComplete}
on:click={goToOwnerStep}
>
<span class="booking-step-number">2</span>
<span class="booking-step-label">{ownerStepLabel}</span>
</button>
</div>
{/if}
</div>
<form
class="booking-form"
id="bookingForm"
novalidate
on:submit={handleSubmit}
on:focusin={noteInteraction}
on:input={noteInteraction}
on:change={noteInteraction}
>
<div class="booking-honeypot" aria-hidden="true">
<label for="website">Website</label>
<input
bind:value={website}
type="text"
id="website"
name="website"
tabindex="-1"
autocomplete="new-password"
/>
</div>
<div class:booking-form-shell={usesStepperShell}>
{#if usesStepperShell}
<div class="booking-social-proof" aria-label="Goodwalk dog families">
<div class="booking-avatar-group" aria-hidden="true">
{#each bookingAvatarDogs as dog}
<span class="booking-avatar-bubble">
<img src={dog.image} alt="" loading="lazy" />
</span>
{/each}
</div>
<div class="booking-social-proof-copy">
<p>{bookingSocialProofTitle}</p>
<span class="booking-social-proof-note">{bookingSocialProofNote}</span>
</div>
</div>
{/if}
{#if step === 1 || isSingleStep}
<input type="hidden" name="enquiryType" value={enquiryType} />
<div class="booking-panel">
{#if detailsStepIntro && !usesStepperShell}
<div class="booking-panel-banner">{detailsStepIntro}</div>
{/if}
<div
class:booking-card-grid-with-banner={Boolean(detailsStepIntro) && !usesStepperShell}
class="booking-card-grid booking-card-grid-dog"
>
{#if allowGeneralEnquiry}
<div class="booking-field-card booking-mode-card booking-field-card-full">
<fieldset class="booking-mode-group">
<legend class="booking-service-label">
<Icon name="fas fa-compass" />&nbsp;{enquiryModeLegend}
</legend>
<div class="booking-mode-options">
<label class="booking-mode-option" class:booking-mode-option-active={!isGeneralEnquiry}>
<input
type="radio"
name="enquiryMode"
value="booking"
checked={!isGeneralEnquiry}
on:change={() => setEnquiryType('booking')}
/>
<span class="booking-mode-option-title">Book a Meet &amp; Greet</span>
<span class="booking-mode-option-copy">
Tell us about your dog, your suburb, and the service you are interested in.
</span>
</label>
<label class="booking-mode-option" class:booking-mode-option-active={isGeneralEnquiry}>
<input
type="radio"
name="enquiryMode"
value="general"
checked={isGeneralEnquiry}
on:change={() => setEnquiryType('general')}
/>
<span class="booking-mode-option-title">General enquiry</span>
<span class="booking-mode-option-copy">
Use this for feedback, complaints, or business questions.
</span>
</label>
</div>
</fieldset>
</div>
{/if}
{#if !isGeneralEnquiry}
<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" />&nbsp;Your dog's name <span class="booking-required">*</span>
</label>
<input
bind:this={petNameInput}
bind:value={petName}
type="text"
id="petName"
name="petName"
required
placeholder="For example, Teddy"
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" />&nbsp;Your suburb <span class="booking-required">*</span>
</label>
<input
bind:this={locationInput}
bind:value={location}
type="text"
id="location"
name="location"
required
placeholder="For example, Grey Lynn"
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>
{#if hasServices}
<div
class="booking-field-stack booking-field-stack-full"
class:booking-field-stack-invalid={errors.services}
>
<div class="booking-selected-service-row">
<span class="booking-service-label"><Icon name="fas fa-paw" />&nbsp;Which service are you interested in?</span>
{#if serviceChoiceLocked}
<button type="button" class="booking-inline-link" on:click={() => (showServicePicker = true)}>
Change service
</button>
{/if}
</div>
{#if serviceChoiceLocked}
<div class="booking-selected-service-chip">{selectedServices[0]}</div>
{:else}
<div class="booking-service-options">
{#each booking.serviceOptions as service}
<label class="booking-check-option" for={serviceInputId(service)}>
<input
id={serviceInputId(service)}
type="radio"
name="service"
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>
{/if}
{#if errors.services}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.services}
</p>
{/if}
</div>
{/if}
<div
class="booking-field-stack booking-field-stack-full"
class:booking-field-stack-invalid={errors.message}
>
<label for="message">
<Icon name="fas fa-comment" />&nbsp;{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>
</div>
</div>
{/if}
{#if isGeneralEnquiry}
<div
class="booking-field-card booking-field-card-full"
class:booking-field-card-invalid={errors.message}
>
<label for="message">
<Icon name="fas fa-comment" />&nbsp;{detailsMessageLabel}
<span class="booking-required">*</span>
</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}
</div>
</div>
{#if !isSingleStep}
<div class="booking-actions booking-actions-next">
<button type="button" class="btn btn-yellow btn-with-arrow booking-next-button" on:click={goToOwnerStep}>
Continue to {ownerStepLabel.toLowerCase()}
<Icon name="fas fa-arrow-right" />
</button>
<p class="booking-next-note">No payment, no pressure, just the right starting point for your dog.</p>
</div>
{/if}
{/if}
{#if step === 2 || isSingleStep}
{#if !isSingleStep}
<input type="hidden" name="fullName" value={fullName} />
<input type="hidden" name="email" value={email} />
<input type="hidden" name="phone" value={phone} />
<input type="hidden" name="petName" value={petName} />
<input type="hidden" name="location" value={location} />
<input type="hidden" name="message" value={message} />
{/if}
<div class="booking-panel">
{#if ownerIntro && !usesStepperShell}
<div class="booking-panel-banner">{ownerIntro}</div>
{/if}
<div
class:booking-card-grid-with-banner={Boolean(ownerIntro) && !usesStepperShell}
class="booking-card-grid booking-card-grid-owner"
>
<div class="booking-field-card booking-field-card-group booking-field-card-full">
<div class="booking-field-group booking-field-group-owner">
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.fullName}>
<label for="fullName">
<Icon name="fas fa-user" />&nbsp;Full Name <span class="booking-required">*</span>
</label>
<input
bind:this={fullNameInput}
bind:value={fullName}
type="text"
id="fullName"
name="fullName"
required
placeholder="Your full name"
class:input-invalid={errors.fullName}
on:input={() => clearError('fullName')}
/>
{#if errors.fullName}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.fullName}
</p>
{/if}
</div>
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.email}>
<label for="email">
<Icon name="fas fa-envelope" />&nbsp;Email address <span class="booking-required">*</span>
</label>
<input
bind:this={emailInput}
bind:value={email}
type="email"
id="email"
name="email"
required
placeholder="you@example.com"
class:input-invalid={errors.email}
on:input={() => clearError('email')}
on:blur={() => {
if (!email.trim()) return;
const msg = validateEmail(email);
errors = { ...errors, email: msg };
}}
/>
{#if errors.email}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.email}
</p>
{/if}
</div>
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.phone}>
<label for="phone">
<Icon name="fas fa-phone" />&nbsp;Phone number <span class="booking-required">*</span>
</label>
<input
bind:this={phoneInput}
bind:value={phone}
type="tel"
id="phone"
name="phone"
required
placeholder="For example, 021 123 4567"
class:input-invalid={errors.phone}
on:input={() => clearError('phone')}
/>
{#if errors.phone}
<p class="field-error">
<Icon name="fas fa-circle-exclamation" />
{errors.phone}
</p>
{/if}
</div>
</div>
</div>
</div>
</div>
<div class="booking-actions booking-actions-final" class:booking-actions-final-single={isSingleStep}>
{#if !isSingleStep}
<button
type="button"
class="btn btn-outline btn-outline-green"
on:click={() => setStep(1, true)}
>
Back
</button>
{/if}
<button type="submit" class="btn btn-yellow btn-with-arrow booking-submit-button" disabled={submitting}>
{#if submitting}
Sending…
{:else if isGeneralEnquiry}
Send enquiry <Icon name="fas fa-arrow-right" />
{:else}
Send Meet &amp; Greet request <Icon name="fas fa-arrow-right" />
{/if}
</button>
</div>
{/if}
</div>
</form>
</div>
</section>
<style>
:global(.reveal-ready.reveal-block) {
opacity: 0;
transform: translate3d(0, var(--reveal-distance, 16px), 0);
transition:
opacity 0.3s ease,
transform 0.45s 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);
}
.booking-honeypot {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
.booking-mode-card {
padding: 22px 24px;
}
.booking-mode-group {
margin: 0;
padding: 0;
border: 0;
min-width: 0;
}
.booking-mode-options {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.booking-mode-option {
display: grid;
gap: 6px;
min-height: 44px;
padding: 16px 18px;
border-radius: 20px;
border: 2px solid var(--border-brand-strong);
background: var(--surface-page);
color: var(--text-heading-soft);
cursor: pointer;
transition:
border-color 0.2s,
background 0.2s,
box-shadow 0.2s ease,
transform 0.16s cubic-bezier(0.22, 1, 0.36, 1);
}
.booking-mode-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.booking-mode-option-title {
font-family: var(--font-head);
font-size: 15px;
font-weight: 700;
line-height: 1.2;
color: var(--text-brand);
}
.booking-mode-option-copy {
font-size: 14px;
line-height: 1.5;
color: var(--text-muted);
}
.booking-mode-option-active {
border-color: var(--text-brand);
background: var(--surface-brand-selected);
box-shadow: var(--shadow-inset-soft);
}
@media (hover: hover) {
.booking-mode-option:hover {
border-color: var(--border-brand-hover);
transform: translateY(-1px);
}
}
.booking-mode-option:focus-within {
border-color: var(--text-brand);
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.14);
}
.booking-shell--card-stepper {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(248, 247, 242, 0.9));
}
.booking-shell--card-stepper :global(.form-inner) {
max-width: 1040px;
}
.booking-shell--card-stepper .booking-header {
max-width: 840px;
margin: 0 auto 34px;
}
.booking-shell--card-stepper .booking-eyebrow {
padding: 7px 14px;
background: rgba(33, 48, 33, 0.05);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
text-transform: none;
letter-spacing: 0.04em;
}
.booking-shell--card-stepper .booking-title {
margin-bottom: 20px;
font-size: clamp(38px, 5vw, 58px);
line-height: 1.02;
}
.booking-shell--card-stepper .booking-intro {
max-width: 42rem;
margin: 0 auto;
}
.booking-shell--card-stepper .booking-form-shell {
position: relative;
max-width: 900px;
margin: 0 auto;
padding: 0 24px 28px;
border-radius: 30px;
background: rgba(255, 255, 255, 0.94);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 22px 52px rgba(17, 20, 24, 0.08);
}
.booking-shell--card-stepper .booking-social-proof {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin: 0 -24px;
padding: 16px 24px;
border-radius: 30px 30px 0 0;
background:
linear-gradient(180deg, rgba(33, 48, 33, 0.06), rgba(33, 48, 33, 0.035));
box-shadow: inset 0 -1px 0 rgba(17, 20, 24, 0.08);
}
.booking-shell--card-stepper .booking-social-proof-copy {
display: grid;
gap: 6px;
justify-items: center;
}
.booking-shell--card-stepper .booking-social-proof p {
margin: 0;
color: var(--gw-green);
font-family: var(--font-head);
font-size: 17px;
font-weight: 600;
line-height: 1.3;
text-wrap: balance;
text-align: center;
}
.booking-shell--card-stepper .booking-social-proof-note {
color: var(--text-subtle);
font-size: 14px;
font-weight: 500;
line-height: 1.45;
text-align: center;
text-wrap: balance;
}
.booking-shell--card-stepper .booking-avatar-group {
display: flex;
align-items: center;
flex: none;
padding-left: 14px;
}
.booking-shell--card-stepper .booking-avatar-bubble {
display: inline-flex;
width: 50px;
height: 50px;
margin-left: -14px;
overflow: hidden;
border: 3px solid rgba(255, 255, 255, 0.96);
border-radius: 50%;
background: rgba(255, 255, 255, 0.16);
box-shadow: 0 8px 18px rgba(17, 20, 24, 0.08);
}
.booking-shell--card-stepper .booking-avatar-bubble img {
width: 100%;
height: 100%;
object-fit: cover;
}
.booking-shell--card-stepper .booking-stepper {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
margin-top: 0;
padding: 0;
background: transparent;
box-shadow: none;
}
.booking-shell--card-stepper .booking-step {
min-width: 0;
min-height: 0;
align-items: start;
gap: 8px;
padding: 0 0 14px;
border-radius: 0;
border-bottom: 1px solid rgba(17, 20, 24, 0.12);
background: transparent;
}
.booking-shell--card-stepper .booking-step:disabled {
cursor: not-allowed;
opacity: 0.64;
}
.booking-shell--card-stepper .booking-step.active {
border-bottom-color: color-mix(in srgb, var(--gw-green) 56%, white);
background: transparent;
}
.booking-shell--card-stepper .booking-step-number {
display: inline-flex;
align-items: center;
gap: 8px;
width: auto;
height: auto;
padding: 7px 10px;
border: 1px solid color-mix(in srgb, var(--gw-green) 82%, black);
border-radius: 999px;
background: var(--gw-green);
color: var(--yellow);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.16em;
line-height: 1;
text-transform: uppercase;
}
.booking-shell--card-stepper .booking-step-number :global(.icon) {
color: var(--yellow);
font-size: 12px;
}
.booking-shell--card-stepper .booking-step.active .booking-step-number {
border-color: color-mix(in srgb, var(--gw-green) 74%, black);
background: var(--gw-green);
color: var(--yellow);
}
.booking-shell--card-stepper .booking-step-divider {
display: none;
}
.booking-shell--card-stepper .booking-step-label {
color: var(--text-muted);
font-family: var(--font-head);
font-size: 19px;
font-weight: 600;
letter-spacing: -0.025em;
line-height: 1.2;
text-align: left;
text-wrap: balance;
}
.booking-shell--card-stepper .booking-step.active .booking-step-label {
color: var(--text-brand);
}
.booking-shell--card-stepper .booking-step:disabled .booking-step-number {
background: rgba(17, 20, 24, 0.04);
color: var(--text-subtle);
}
.booking-shell--card-stepper .booking-step:disabled .booking-step-number :global(.icon) {
color: var(--text-subtle);
}
.booking-shell--card-stepper .booking-step:disabled .booking-step-label {
color: var(--text-subtle);
}
.booking-shell--card-stepper .booking-panel {
max-width: none;
margin: 24px 0 0;
padding: 0;
background: transparent;
box-shadow: none;
}
.booking-shell--card-stepper .booking-panel-banner {
margin-bottom: 18px;
padding: 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
color: #4f555c;
font-size: 16px;
line-height: 1.65;
}
.booking-shell--card-stepper .booking-card-grid {
gap: 14px;
}
.booking-shell--card-stepper .booking-card-grid-with-banner .booking-field-card {
border-radius: 26px;
}
.booking-shell--card-stepper .booking-field-card {
border-radius: 0;
background: transparent;
box-shadow: none;
padding: 0;
}
.booking-shell--card-stepper .booking-field-card-group {
padding: 0;
}
.booking-shell--card-stepper .booking-mode-card {
padding: 0;
background: transparent;
box-shadow: none;
}
.booking-shell--card-stepper .booking-mode-option {
background: rgba(255, 255, 255, 0.86);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 8px 18px rgba(17, 20, 24, 0.05);
}
.booking-shell--card-stepper .booking-mode-option-active {
background: rgba(33, 48, 33, 0.08);
}
.booking-shell--card-stepper .booking-field-stack {
padding: 18px;
border-radius: 24px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(247, 248, 246, 0.94));
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 12px 24px rgba(17, 20, 24, 0.04);
transition:
transform 0.2s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.2s ease,
background 0.2s ease;
}
.booking-shell--card-stepper .booking-field-stack:focus-within {
transform: translateY(-1px);
background: linear-gradient(180deg, rgba(255, 255, 255, 1), rgba(248, 247, 242, 0.98));
box-shadow:
inset 0 0 0 1px rgba(255, 209, 0, 0.42),
0 16px 28px rgba(17, 20, 24, 0.06);
}
.booking-shell--card-stepper .booking-field-card label,
.booking-shell--card-stepper .booking-service-label {
color: #171b20;
font-size: 15px;
font-weight: 600;
letter-spacing: 0;
text-transform: none;
}
.booking-shell--card-stepper .booking-field-card input,
.booking-shell--card-stepper .booking-field-card textarea {
border: 1px solid rgba(17, 20, 24, 0.1);
background: #fff;
box-shadow: none;
}
.booking-shell--card-stepper .booking-field-card input:hover,
.booking-shell--card-stepper .booking-field-card textarea:hover {
border-color: rgba(17, 20, 24, 0.18);
background: #fff;
box-shadow: none;
}
.booking-shell--card-stepper .booking-field-card input:focus,
.booking-shell--card-stepper .booking-field-card textarea:focus {
border-color: rgba(33, 48, 33, 0.45);
background: #fff;
box-shadow: 0 0 0 4px rgba(255, 209, 0, 0.12);
}
.booking-shell--card-stepper .booking-selected-service-chip,
.booking-shell--card-stepper .booking-check-option {
background: transparent;
}
.booking-shell--card-stepper .booking-selected-service-chip {
display: inline-flex;
align-items: center;
min-height: 42px;
padding: 0 16px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
box-shadow:
inset 0 0 0 1px rgba(33, 48, 33, 0.1),
0 10px 18px rgba(17, 20, 24, 0.04);
color: var(--gw-green);
font-weight: 700;
}
.booking-shell--card-stepper .booking-actions {
max-width: none;
margin-left: 0;
margin-right: 0;
}
.booking-shell--card-stepper .booking-actions-next,
.booking-shell--card-stepper .booking-actions-final {
margin-top: 18px;
}
.booking-shell--card-stepper .booking-next-note {
color: #687076;
}
.booking-shell--card-stepper .booking-submit-button,
.booking-shell--card-stepper .booking-next-button {
box-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.08),
0 12px 24px rgba(17, 20, 24, 0.1);
}
.booking-shell--minimal-premium {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(248, 247, 242, 0.72));
}
.booking-shell--minimal-premium :global(.form-inner) {
max-width: 980px;
}
.booking-shell--minimal-premium .booking-header {
max-width: 760px;
margin: 0 auto 34px;
}
.booking-shell--minimal-premium .booking-eyebrow {
background: rgba(33, 48, 33, 0.06);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
color: var(--gw-green);
}
.booking-shell--minimal-premium .booking-title {
margin-bottom: 18px;
color: #11171b;
}
.booking-shell--minimal-premium .booking-intro {
max-width: 38rem;
margin: 0 auto;
color: #59606d;
font-size: 16px;
line-height: 1.65;
}
.booking-shell--minimal-premium .booking-trust-row {
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.booking-shell--minimal-premium .booking-trust-chip {
background: rgba(33, 48, 33, 0.05);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
color: var(--gw-green);
}
.booking-shell--minimal-premium .booking-stepper {
justify-content: center;
margin-top: 24px;
}
.booking-shell--minimal-premium .booking-step {
background: rgba(255, 255, 255, 0.86);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 8px 18px rgba(17, 20, 24, 0.05);
}
.booking-shell--minimal-premium .booking-step.active {
background: rgba(33, 48, 33, 0.08);
}
.booking-shell--minimal-premium .booking-panel {
max-width: 860px;
margin: 0 auto;
padding: 30px;
border-radius: 32px;
background: rgba(251, 251, 251, 0.92);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 22px 52px rgba(17, 20, 24, 0.08);
}
.booking-shell--minimal-premium .booking-panel-banner {
margin-bottom: 22px;
padding: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
color: #59606d;
font-size: 15px;
line-height: 1.65;
}
.booking-shell--minimal-premium .booking-card-grid {
gap: 18px;
}
.booking-shell--minimal-premium .booking-field-card,
.booking-shell--minimal-premium .booking-mode-card {
border-radius: 24px;
background: linear-gradient(180deg, rgba(251, 251, 251, 0.98), rgba(247, 248, 246, 1));
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 12px 26px rgba(17, 20, 24, 0.05);
}
.booking-shell--minimal-premium .booking-field-card {
padding: 24px;
}
.booking-shell--minimal-premium .booking-field-card-group {
padding: 24px;
}
.booking-shell--minimal-premium .booking-mode-card {
padding: 16px 18px;
}
.booking-shell--minimal-premium .booking-field-card label,
.booking-shell--minimal-premium .booking-service-label {
color: #171b20;
}
.booking-shell--minimal-premium .booking-field-card input,
.booking-shell--minimal-premium .booking-field-card textarea {
border: none;
background: rgba(255, 255, 255, 0.96);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.08),
0 1px 0 rgba(255, 255, 255, 0.3);
}
.booking-shell--minimal-premium .booking-field-card input:hover,
.booking-shell--minimal-premium .booking-field-card textarea:hover {
border: none;
background: #fff;
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.12),
0 4px 12px rgba(17, 20, 24, 0.04);
}
.booking-shell--minimal-premium .booking-field-card input:focus,
.booking-shell--minimal-premium .booking-field-card textarea:focus {
border: none;
background: #fff;
box-shadow:
inset 0 0 0 2px rgba(255, 209, 0, 0.6),
0 0 0 4px rgba(255, 209, 0, 0.14);
}
.booking-shell--minimal-premium .booking-selected-service-chip,
.booking-shell--minimal-premium .booking-check-option {
background: rgba(255, 255, 255, 0.9);
}
.booking-shell--minimal-premium .booking-actions {
max-width: 860px;
margin-left: auto;
margin-right: auto;
}
.booking-shell--minimal-premium .booking-actions-next {
margin-top: 18px;
}
.booking-shell--minimal-premium .booking-actions-final {
margin-top: 18px;
}
.booking-shell--minimal-premium .booking-next-note {
color: #687076;
}
.booking-shell--minimal-premium .booking-submit-button,
.booking-shell--minimal-premium .booking-next-button {
box-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.08),
0 12px 24px rgba(17, 20, 24, 0.1);
}
.booking-shell--contact-modern {
position: relative;
overflow: clip;
background:
radial-gradient(circle at top left, rgba(229, 214, 194, 0.24), transparent 34%),
radial-gradient(circle at top right, rgba(33, 48, 33, 0.06), transparent 30%),
linear-gradient(180deg, rgba(251, 251, 251, 0.96), rgba(244, 246, 242, 0.99));
}
.booking-shell--contact-modern::before {
content: '';
position: absolute;
inset: 22px auto auto 50%;
width: min(720px, calc(100% - 48px));
height: 1px;
transform: translateX(-50%);
background: linear-gradient(90deg, transparent, rgba(33, 48, 33, 0.16), transparent);
pointer-events: none;
}
.booking-shell--contact-modern::after {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.12), transparent 18%),
radial-gradient(circle at 50% 0, rgba(255, 209, 0, 0.05), transparent 22%);
pointer-events: none;
}
.booking-shell--contact-modern :global(.form-inner) {
max-width: 1080px;
}
.booking-shell--contact-modern .booking-header {
max-width: 760px;
margin: 0 auto 32px;
}
.booking-shell--contact-modern .booking-eyebrow {
padding: 7px 14px;
background: rgba(33, 48, 33, 0.05);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
letter-spacing: 0.04em;
text-transform: none;
}
.booking-shell--contact-modern .booking-title {
margin-bottom: 18px;
color: #11171b;
font-size: clamp(36px, 4.6vw, 56px);
line-height: 1.04;
}
.booking-shell--contact-modern .booking-title-highlight {
color: color-mix(in srgb, var(--gw-green) 88%, black);
}
.booking-shell--contact-modern .booking-intro {
max-width: 42rem;
margin: 0 auto;
color: var(--text-muted);
font-size: 16px;
line-height: 1.68;
text-wrap: balance;
}
.booking-shell--contact-modern .booking-form-shell {
position: relative;
max-width: 960px;
margin: 0 auto;
padding: 18px 22px 30px;
border-radius: 36px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(249, 249, 248, 0.98));
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.42),
0 26px 60px rgba(17, 20, 24, 0.07);
}
.booking-shell--contact-modern .booking-social-proof {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 18px;
align-items: center;
margin: 0 0 16px;
padding: 18px 20px 20px;
border-radius: 26px;
background:
linear-gradient(135deg, rgba(33, 48, 33, 0.07), rgba(229, 214, 194, 0.18));
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
0 14px 34px rgba(17, 20, 24, 0.06);
}
.booking-shell--contact-modern .booking-social-proof-copy {
display: grid;
gap: 4px;
justify-items: start;
}
.booking-shell--contact-modern .booking-social-proof p {
margin: 0;
color: var(--gw-green);
font-family: var(--font-head);
font-size: 18px;
font-weight: 600;
line-height: 1.3;
text-wrap: balance;
}
.booking-shell--contact-modern .booking-social-proof-note {
color: var(--text-subtle);
font-size: 14px;
line-height: 1.5;
text-wrap: balance;
}
.booking-shell--contact-modern .booking-avatar-group {
display: flex;
align-items: center;
flex: none;
padding-left: 16px;
}
.booking-shell--contact-modern .booking-avatar-bubble {
display: inline-flex;
width: 54px;
height: 54px;
margin-left: -16px;
overflow: hidden;
border: 3px solid rgba(255, 255, 255, 0.98);
border-radius: 50%;
box-shadow: 0 10px 24px rgba(17, 20, 24, 0.08);
}
.booking-shell--contact-modern .booking-avatar-bubble img {
width: 100%;
height: 100%;
object-fit: cover;
}
.booking-shell--contact-modern .booking-stepper {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.booking-shell--contact-modern .booking-step {
min-width: 0;
min-height: 0;
align-items: flex-start;
gap: 10px;
padding: 18px 20px;
border-radius: 22px;
background: linear-gradient(180deg, rgba(33, 48, 33, 0.94), rgba(45, 66, 48, 0.98));
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.06),
0 12px 26px rgba(17, 20, 24, 0.12);
transition:
transform 0.24s cubic-bezier(0.22, 1, 0.36, 1),
background 0.24s ease,
box-shadow 0.24s ease,
opacity 0.24s ease;
}
.booking-shell--contact-modern .booking-step.active {
transform: translateY(-2px);
background: linear-gradient(180deg, rgba(33, 48, 33, 1), rgba(52, 75, 56, 1));
box-shadow:
inset 0 0 0 1px rgba(255, 209, 0, 0.22),
0 16px 30px rgba(17, 20, 24, 0.18);
}
.booking-shell--contact-modern .booking-step:disabled {
opacity: 0.72;
cursor: not-allowed;
}
@media (hover: hover) {
.booking-shell--contact-modern .booking-step:not(:disabled):hover {
transform: translateY(-1px);
background: linear-gradient(180deg, rgba(38, 56, 38, 0.98), rgba(52, 75, 56, 0.98));
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
0 14px 28px rgba(17, 20, 24, 0.16);
}
}
.booking-shell--contact-modern .booking-step-number {
display: inline-flex;
align-items: center;
gap: 8px;
width: auto;
height: auto;
padding: 7px 12px;
border: 1px solid rgba(255, 209, 0, 0.2);
border-radius: 999px;
background: rgba(255, 209, 0, 0.12);
color: var(--yellow);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.14em;
line-height: 1;
text-transform: uppercase;
}
.booking-shell--contact-modern .booking-step-number :global(.icon) {
color: var(--yellow);
font-size: 12px;
}
.booking-shell--contact-modern .booking-step.active .booking-step-number {
border-color: rgba(255, 209, 0, 0.28);
background: rgba(255, 209, 0, 0.18);
color: var(--yellow);
}
.booking-shell--contact-modern .booking-step.active .booking-step-number :global(.icon) {
color: var(--yellow);
}
.booking-shell--contact-modern .booking-step-divider {
display: none;
}
.booking-shell--contact-modern .booking-step-label {
color: rgba(255, 255, 255, 0.98);
font-family: var(--font-head);
font-size: 19px;
font-weight: 600;
letter-spacing: -0.025em;
line-height: 1.2;
text-align: left;
text-wrap: balance;
}
.booking-shell--contact-modern .booking-step:disabled .booking-step-label {
color: rgba(255, 255, 255, 0.66);
}
.booking-shell--contact-modern .booking-panel {
max-width: none;
margin: 0;
padding: 0;
background: transparent;
box-shadow: none;
}
.booking-shell--contact-modern .booking-panel-banner {
margin-bottom: 18px;
padding: 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
color: var(--text-muted);
font-size: 15px;
line-height: 1.65;
}
.booking-shell--contact-modern .booking-card-grid {
gap: 16px;
}
.booking-shell--contact-modern .booking-field-card,
.booking-shell--contact-modern .booking-field-card-group,
.booking-shell--contact-modern .booking-mode-card {
padding: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.booking-shell--contact-modern .booking-mode-options {
gap: 14px;
}
.booking-shell--contact-modern .booking-mode-option {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.86), rgba(247, 248, 246, 0.9));
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 10px 24px rgba(17, 20, 24, 0.04);
}
.booking-shell--contact-modern .booking-mode-option-active {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(241, 245, 239, 0.98));
box-shadow:
inset 0 0 0 1px rgba(33, 48, 33, 0.18),
0 14px 28px rgba(17, 20, 24, 0.05);
}
.booking-shell--contact-modern .booking-field-stack {
padding: 20px;
border-radius: 24px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 248, 247, 0.98));
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.3),
0 10px 22px rgba(17, 20, 24, 0.04);
transition:
transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.22s ease,
background 0.22s ease;
}
.booking-shell--contact-modern .booking-field-stack:focus-within {
transform: translateY(-2px);
background: linear-gradient(180deg, rgba(255, 255, 255, 1), rgba(249, 248, 245, 0.98));
box-shadow:
inset 0 0 0 1px rgba(255, 209, 0, 0.42),
0 18px 34px rgba(17, 20, 24, 0.07);
}
.booking-shell--contact-modern .booking-field-card label,
.booking-shell--contact-modern .booking-service-label {
color: #171b20;
font-size: 15px;
font-weight: 600;
letter-spacing: 0;
text-transform: none;
}
.booking-shell--contact-modern .booking-field-card input,
.booking-shell--contact-modern .booking-field-card textarea {
border: 1px solid rgba(17, 20, 24, 0.1);
background: #fff;
box-shadow: none;
}
.booking-shell--contact-modern .booking-field-card input:hover,
.booking-shell--contact-modern .booking-field-card textarea:hover {
border-color: rgba(17, 20, 24, 0.18);
background: #fff;
box-shadow: none;
}
.booking-shell--contact-modern .booking-field-card input:focus,
.booking-shell--contact-modern .booking-field-card textarea:focus {
border-color: rgba(33, 48, 33, 0.45);
background: #fff;
box-shadow: 0 0 0 4px rgba(255, 209, 0, 0.12);
}
.booking-shell--contact-modern .booking-selected-service-chip,
.booking-shell--contact-modern .booking-check-option {
background: rgba(255, 255, 255, 0.92);
}
.booking-shell--contact-modern .booking-check-option {
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 8px 18px rgba(17, 20, 24, 0.03);
transition:
transform 0.2s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.2s ease,
background 0.2s ease;
}
@media (hover: hover) {
.booking-shell--contact-modern .booking-check-option:hover {
transform: translateY(-1px);
box-shadow:
inset 0 0 0 1px rgba(33, 48, 33, 0.12),
0 12px 24px rgba(17, 20, 24, 0.05);
}
}
.booking-shell--contact-modern .booking-selected-service-chip {
display: inline-flex;
align-items: center;
min-height: 42px;
padding: 0 16px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
box-shadow: inset 0 0 0 1px rgba(33, 48, 33, 0.1);
color: var(--gw-green);
font-weight: 700;
}
.booking-shell--contact-modern .booking-actions {
max-width: none;
margin-left: 0;
margin-right: 0;
}
.booking-shell--contact-modern .booking-actions-next,
.booking-shell--contact-modern .booking-actions-final {
margin-top: 20px;
}
.booking-shell--contact-modern .booking-next-note {
color: #687076;
}
.booking-shell--contact-modern .booking-submit-button,
.booking-shell--contact-modern .booking-next-button {
box-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.08),
0 16px 30px rgba(17, 20, 24, 0.12);
transition:
transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.22s ease,
filter 0.22s ease;
}
@media (hover: hover) {
.booking-shell--contact-modern .booking-submit-button:hover,
.booking-shell--contact-modern .booking-next-button:hover {
transform: translateY(-2px);
filter: brightness(1.02);
box-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.08),
0 20px 34px rgba(17, 20, 24, 0.14);
}
}
@media (max-width: 768px) {
.booking-mode-card {
padding: 20px 18px;
}
.booking-mode-options {
grid-template-columns: 1fr;
gap: 12px;
}
.booking-mode-option {
padding: 14px 16px;
border-radius: 18px;
}
.booking-shell--contact-modern .booking-header {
margin-bottom: 26px;
}
.booking-shell--contact-modern .booking-title {
font-size: 34px;
}
.booking-shell--contact-modern .booking-form-shell {
padding: 16px 16px 22px;
border-radius: 26px;
}
.booking-shell--contact-modern .booking-social-proof {
grid-template-columns: 1fr;
justify-items: center;
gap: 12px;
padding: 16px;
border-radius: 22px;
text-align: center;
}
.booking-shell--contact-modern .booking-social-proof-copy {
justify-items: center;
}
.booking-shell--contact-modern .booking-stepper {
grid-template-columns: 1fr;
gap: 10px;
}
.booking-shell--contact-modern .booking-step {
padding: 16px;
border-radius: 18px;
}
.booking-shell--contact-modern .booking-step-label {
font-size: 16px;
}
.booking-shell--contact-modern .booking-selected-service-row {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.booking-shell--contact-modern .booking-selected-service-row .booking-service-label {
margin-bottom: 0;
line-height: 1.35;
}
.booking-shell--contact-modern .booking-selected-service-chip {
width: 100%;
justify-content: center;
padding: 10px 14px;
line-height: 1.35;
text-align: center;
white-space: normal;
}
.booking-shell--contact-modern .booking-service-options {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.booking-shell--contact-modern .booking-check-option {
width: 100%;
align-items: flex-start;
gap: 12px;
padding: 12px 14px;
border-radius: 16px;
line-height: 1.35;
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
}
.booking-shell--contact-modern .booking-check-box {
margin-top: 1px;
}
.booking-shell--contact-modern .booking-field-card,
.booking-shell--contact-modern .booking-field-card-group {
padding: 0;
border-radius: 0;
}
.booking-shell--contact-modern .booking-field-stack {
padding: 16px;
border-radius: 20px;
}
.booking-shell--contact-modern .booking-field-card label,
.booking-shell--contact-modern .booking-service-label {
font-size: 14px;
line-height: 1.35;
align-items: flex-start;
}
.booking-shell--contact-modern .booking-actions-next,
.booking-shell--contact-modern .booking-actions-final {
width: 100%;
gap: 12px;
}
.booking-shell--contact-modern .booking-actions-next .btn,
.booking-shell--contact-modern .booking-actions-final .btn {
width: 100%;
justify-content: center;
}
.booking-shell--contact-modern .booking-next-note {
max-width: 24rem;
margin-left: auto;
margin-right: auto;
text-wrap: balance;
}
.booking-shell--card-stepper .booking-header {
margin-bottom: 26px;
}
.booking-shell--card-stepper .booking-title {
margin-bottom: 18px;
font-size: 34px;
}
.booking-shell--card-stepper .booking-form-shell {
padding: 0 18px 22px;
border-radius: 24px;
}
.booking-shell--card-stepper .booking-social-proof {
align-items: center;
flex-direction: column;
gap: 10px;
margin: 0 -18px;
padding: 14px 18px;
border-radius: 24px 24px 0 0;
}
.booking-shell--card-stepper .booking-social-proof-copy {
width: 100%;
max-width: 24rem;
}
.booking-shell--card-stepper .booking-social-proof p {
font-size: 16px;
}
.booking-shell--card-stepper .booking-social-proof-note {
font-size: 13px;
}
.booking-shell--card-stepper .booking-stepper {
gap: 10px;
padding: 0;
align-items: stretch;
}
.booking-shell--card-stepper .booking-step {
width: 100%;
gap: 6px;
padding: 0 0 12px;
}
.booking-shell--card-stepper .booking-step-number {
font-size: 10px;
}
.booking-shell--card-stepper .booking-step-label {
font-size: 15px;
line-height: 1.22;
text-wrap: balance;
}
.booking-shell--card-stepper .booking-panel {
margin-top: 20px;
}
.booking-shell--card-stepper .booking-panel-banner {
font-size: 15px;
}
.booking-shell--card-stepper .booking-selected-service-row {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.booking-shell--card-stepper .booking-selected-service-row .booking-service-label {
margin-bottom: 0;
line-height: 1.35;
}
.booking-shell--card-stepper .booking-inline-link {
font-size: 13px;
}
.booking-shell--card-stepper .booking-selected-service-chip {
width: 100%;
justify-content: center;
padding: 10px 14px;
text-align: center;
line-height: 1.35;
white-space: normal;
}
.booking-shell--card-stepper .booking-service-options {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
.booking-shell--card-stepper .booking-check-option {
width: 100%;
align-items: flex-start;
gap: 12px;
padding: 12px 14px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.92);
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
line-height: 1.35;
}
.booking-shell--card-stepper .booking-check-box {
margin-top: 1px;
}
.booking-shell--card-stepper .booking-field-card,
.booking-shell--card-stepper .booking-field-card-group {
padding: 0;
border-radius: 0;
}
.booking-shell--card-stepper .booking-field-stack {
padding: 16px;
border-radius: 20px;
}
.booking-shell--card-stepper .booking-field-card label,
.booking-shell--card-stepper .booking-service-label {
font-size: 14px;
line-height: 1.35;
align-items: flex-start;
}
.booking-shell--card-stepper .booking-actions-next,
.booking-shell--card-stepper .booking-actions-final {
width: 100%;
gap: 12px;
}
.booking-shell--card-stepper .booking-actions-next .btn,
.booking-shell--card-stepper .booking-actions-final .btn {
width: 100%;
justify-content: center;
}
.booking-shell--card-stepper .booking-next-note {
max-width: 24rem;
margin-left: auto;
margin-right: auto;
text-wrap: balance;
}
.booking-shell--minimal-premium .booking-header {
margin-bottom: 26px;
}
.booking-shell--minimal-premium .booking-intro {
font-size: 15px;
line-height: 1.58;
}
.booking-shell--minimal-premium .booking-stepper {
gap: 8px;
}
.booking-shell--minimal-premium .booking-panel {
padding: 18px;
border-radius: 24px;
}
.booking-shell--minimal-premium .booking-field-card,
.booking-shell--minimal-premium .booking-field-card-group {
padding: 20px;
border-radius: 22px;
}
.booking-shell--minimal-premium .booking-mode-card {
border-radius: 20px;
}
}
.booking-actions-final-single {
justify-content: center;
}
@media (max-width: 600px) {
.booking-submit-button,
.booking-next-button {
white-space: normal;
line-height: 1.25;
padding: 14px 18px;
text-align: center;
}
.booking-submit-button :global(.icon),
.booking-next-button :global(.icon) {
flex: none;
}
}
.booking-shell--card-stepper .booking-form-shell,
.booking-shell--contact-modern .booking-form-shell {
overflow-x: clip;
}
/* Booking section uses the shared --space-container-x-mobile gutter
(responsive.css). The bordered .booking-form-shell card therefore
aligns with every other bordered container at 24px inset on mobile. */
.booking-shell--card-stepper .booking-field-card input,
.booking-shell--card-stepper .booking-field-card textarea,
.booking-shell--contact-modern .booking-field-card input,
.booking-shell--contact-modern .booking-field-card textarea {
min-width: 0;
max-width: 100%;
}
.booking-shell--card-stepper .booking-field-stack,
.booking-shell--contact-modern .booking-field-stack {
min-width: 0;
}
.booking-shell--card-stepper .booking-card-grid-owner,
.booking-shell--contact-modern .booking-card-grid-owner {
margin-top: 14px;
}
@media (max-width: 600px) {
.booking-shell--card-stepper .booking-social-proof,
.booking-shell--contact-modern .booking-social-proof {
gap: 10px;
padding: 10px 14px;
}
.booking-shell--card-stepper .booking-avatar-bubble,
.booking-shell--contact-modern .booking-avatar-bubble {
width: 36px;
height: 36px;
margin-left: -10px;
border-width: 2px;
}
.booking-shell--card-stepper .booking-social-proof p,
.booking-shell--contact-modern .booking-social-proof p {
font-size: 14px;
line-height: 1.3;
}
.booking-shell--card-stepper .booking-social-proof-note,
.booking-shell--contact-modern .booking-social-proof-note {
font-size: 12px;
line-height: 1.35;
}
.booking-shell--card-stepper .booking-panel,
.booking-shell--contact-modern .booking-panel {
margin-top: 14px;
}
.booking-shell--card-stepper .booking-field-stack,
.booking-shell--contact-modern .booking-field-stack {
padding: 12px;
border-radius: 18px;
}
.booking-shell--card-stepper .booking-card-grid,
.booking-shell--contact-modern .booking-card-grid {
gap: 10px;
}
}
</style>