v4
This commit is contained in:
@@ -0,0 +1,960 @@
|
||||
<!--
|
||||
/meet-greet-v2 — A/B test variant of the Meet & Greet booking flow.
|
||||
|
||||
Standalone "sticker stack" multi-step wizard. NOT linked from the main
|
||||
nav. Submits to the same /api/submit backend as the live contact form
|
||||
(BookingSection.svelte) using an identical payload shape, so the
|
||||
downstream handler treats it the same. Marked noindex so the variant
|
||||
does not appear in search results during the test.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { homepageContent } from '$lib/content/homepage';
|
||||
|
||||
type SuccessModalComponentType = typeof import('$lib/components/SuccessModal.svelte').default;
|
||||
type ErrorModalComponentType = typeof import('$lib/components/ErrorModal.svelte').default;
|
||||
|
||||
const navigation = homepageContent.navigation;
|
||||
const footerContent = homepageContent.footer;
|
||||
|
||||
const visitStartedStorageKey = 'goodwalk_visit_started_at';
|
||||
const journeyStorageKey = 'goodwalk_journey';
|
||||
const maxJourneyEntries = 8;
|
||||
|
||||
const serviceOptions = ['Tiny Gang Pack Walks', '1:1 Walks', 'Puppy Visits', 'Other Services'];
|
||||
const dogShortcuts = ['puppy', 'senior', 'rescue', 'shy'];
|
||||
|
||||
let step = 1;
|
||||
const totalSteps = 4;
|
||||
|
||||
let dogDetails = '';
|
||||
let selectedServices: string[] = [];
|
||||
let message = '';
|
||||
let fullName = '';
|
||||
let email = '';
|
||||
let phone = '';
|
||||
let location = '';
|
||||
|
||||
let website = ''; // honeypot
|
||||
let formStartedAt = 0;
|
||||
let visitStartedAt = 0;
|
||||
let pageEnteredAt = 0;
|
||||
let firstInteractionAt = 0;
|
||||
let sendClickedAt = 0;
|
||||
let stepChanges = 0;
|
||||
let journey: string[] = [];
|
||||
|
||||
let errors: Record<string, string> = {};
|
||||
let submitting = false;
|
||||
let submitted = false;
|
||||
let showErrorModal = false;
|
||||
let submitErrorDetail = '';
|
||||
|
||||
let SuccessModalComponent: SuccessModalComponentType | null = null;
|
||||
let ErrorModalComponent: ErrorModalComponentType | null = null;
|
||||
|
||||
$: dogFirstWord = dogDetails.trim().split(/[,\s]/)[0] || 'your dog';
|
||||
$: dogNameDisplay = dogFirstWord
|
||||
? dogFirstWord.charAt(0).toLocaleUpperCase() + dogFirstWord.slice(1)
|
||||
: 'your dog';
|
||||
|
||||
$: stepCopy = [
|
||||
{
|
||||
eyebrow: 'Question one',
|
||||
heading: "Who's the star?",
|
||||
helper: 'Tell us your dog in one line — name, age, breed. We will use this to point you toward the right walk.'
|
||||
},
|
||||
{
|
||||
eyebrow: 'Question two',
|
||||
heading: `What's ${dogNameDisplay} after?`,
|
||||
helper: 'Pick everything you are open to. We will recommend the best fit when we reply.'
|
||||
},
|
||||
{
|
||||
eyebrow: 'Question three',
|
||||
heading: 'Anything we should know?',
|
||||
helper: 'Health quirks, anxiety triggers, weekly schedule, dream outcome — anything that helps us prepare.'
|
||||
},
|
||||
{
|
||||
eyebrow: 'Question four',
|
||||
heading: 'How do we reach you?',
|
||||
helper: 'A real person replies within 24 hours, usually sooner.'
|
||||
}
|
||||
];
|
||||
|
||||
$: successPetName = dogNameDisplay;
|
||||
|
||||
$: 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);
|
||||
});
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
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 noteInteraction() {
|
||||
if (!firstInteractionAt) firstInteractionAt = Date.now();
|
||||
}
|
||||
|
||||
function clearError(field: string) {
|
||||
if (errors[field]) errors = { ...errors, [field]: '' };
|
||||
}
|
||||
|
||||
function appendShortcut(token: string) {
|
||||
noteInteraction();
|
||||
const current = dogDetails.trim();
|
||||
if (current.toLowerCase().includes(token.toLowerCase())) return;
|
||||
dogDetails = current ? `${current.replace(/[,\s]+$/, '')}, ${token}` : token;
|
||||
clearError('dogDetails');
|
||||
}
|
||||
|
||||
function toggleService(service: string) {
|
||||
noteInteraction();
|
||||
if (selectedServices.includes(service)) {
|
||||
selectedServices = selectedServices.filter((s) => s !== service);
|
||||
} else {
|
||||
selectedServices = [...selectedServices, service];
|
||||
}
|
||||
clearError('services');
|
||||
}
|
||||
|
||||
function validateEmail(raw: string): string {
|
||||
const value = raw.trim();
|
||||
if (!value) return 'Please enter your email';
|
||||
if (!value.includes('@')) return 'Email is missing the @ sign';
|
||||
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 doesn't look quite right";
|
||||
return '';
|
||||
}
|
||||
|
||||
function validateStep(target: number): boolean {
|
||||
const next: Record<string, string> = {};
|
||||
if (target === 1 && !dogDetails.trim()) {
|
||||
next.dogDetails = "Tell us a little about your dog so we can match the right walk.";
|
||||
}
|
||||
if (target === 2 && selectedServices.length === 0) {
|
||||
next.services = 'Pick at least one service to continue.';
|
||||
}
|
||||
if (target === 4) {
|
||||
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';
|
||||
if (!location.trim()) next.location = 'Please enter your suburb';
|
||||
}
|
||||
errors = next;
|
||||
return Object.keys(next).length === 0;
|
||||
}
|
||||
|
||||
function goNext() {
|
||||
noteInteraction();
|
||||
if (!validateStep(step)) return;
|
||||
if (step < totalSteps) {
|
||||
step += 1;
|
||||
stepChanges += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
noteInteraction();
|
||||
if (step > 1) {
|
||||
step -= 1;
|
||||
stepChanges += 1;
|
||||
errors = {};
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
noteInteraction();
|
||||
if (!validateStep(4)) return;
|
||||
|
||||
submitting = true;
|
||||
sendClickedAt = Date.now();
|
||||
submitErrorDetail = '';
|
||||
showErrorModal = false;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/submit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
enquiryType: 'booking',
|
||||
fullName,
|
||||
email,
|
||||
phone,
|
||||
petName: dogDetails,
|
||||
location,
|
||||
message,
|
||||
services: selectedServices,
|
||||
website,
|
||||
formStartedAt,
|
||||
visitStartedAt,
|
||||
pageEnteredAt,
|
||||
firstInteractionAt,
|
||||
sendClickedAt,
|
||||
stepChanges,
|
||||
journey,
|
||||
referrer: typeof document !== 'undefined' ? document.referrer : '',
|
||||
page: typeof window !== 'undefined' ? window.location.href : '',
|
||||
variant: 'meet-greet-v2'
|
||||
})
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<svelte:head>
|
||||
<title>Book your free Meet & Greet | Goodwalk</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="description" content="Book a free Goodwalk Meet & Greet — short, guided application for Auckland dog owners." />
|
||||
</svelte:head>
|
||||
|
||||
<Header {navigation} />
|
||||
|
||||
<main class="mgv2-page">
|
||||
{#if submitted && SuccessModalComponent}
|
||||
<svelte:component
|
||||
this={SuccessModalComponent}
|
||||
firstName={fullName.split(' ')[0]}
|
||||
petName={successPetName}
|
||||
{email}
|
||||
enquiryType="booking"
|
||||
onClose={() => (submitted = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if showErrorModal && ErrorModalComponent}
|
||||
<svelte:component
|
||||
this={ErrorModalComponent}
|
||||
detail={submitErrorDetail}
|
||||
enquiryType="booking"
|
||||
onClose={() => (showErrorModal = false)}
|
||||
onRetry={() => (showErrorModal = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<section class="mgv2-shell">
|
||||
<header class="mgv2-intro">
|
||||
<span class="mgv2-kicker">Free Meet & Greet</span>
|
||||
<h1>Start with the right fit for your dog.</h1>
|
||||
<p>Four short questions. A real reply within 24 hours.</p>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="mgv2-progress"
|
||||
role="progressbar"
|
||||
aria-valuemin="1"
|
||||
aria-valuemax={totalSteps}
|
||||
aria-valuenow={step}
|
||||
>
|
||||
{#each Array.from({ length: totalSteps }) as _, index}
|
||||
<span class="mgv2-progress-dot" class:active={index + 1 === step}></span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mgv2-stack">
|
||||
<span class="mgv2-decoy mgv2-decoy-left" aria-hidden="true"></span>
|
||||
<span class="mgv2-decoy mgv2-decoy-right" aria-hidden="true"></span>
|
||||
|
||||
<article class="mgv2-card">
|
||||
<span class="mgv2-badge" aria-hidden="true">
|
||||
<Icon name="fas fa-paw" />
|
||||
</span>
|
||||
|
||||
<input
|
||||
bind:value={website}
|
||||
type="text"
|
||||
name="website"
|
||||
class="mgv2-honeypot"
|
||||
tabindex="-1"
|
||||
autocomplete="new-password"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{#key step}
|
||||
<div class="mgv2-step" in:fade={{ duration: 200 }} out:fade={{ duration: 120 }}>
|
||||
<span class="mgv2-eyebrow">{stepCopy[step - 1].eyebrow}</span>
|
||||
<h2 class="mgv2-heading">{stepCopy[step - 1].heading}</h2>
|
||||
<p class="mgv2-helper">{stepCopy[step - 1].helper}</p>
|
||||
|
||||
{#if step === 1}
|
||||
<label class="mgv2-field" for="mgv2-dog">
|
||||
<span class="mgv2-label">Your dog, in a line</span>
|
||||
<input
|
||||
bind:value={dogDetails}
|
||||
on:input={() => clearError('dogDetails')}
|
||||
id="mgv2-dog"
|
||||
type="text"
|
||||
placeholder="Teddy, 3, schnoodle"
|
||||
class:mgv2-input-invalid={errors.dogDetails}
|
||||
autocomplete="off"
|
||||
/>
|
||||
{#if errors.dogDetails}
|
||||
<span class="mgv2-error">{errors.dogDetails}</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<div class="mgv2-chips" aria-label="Quick add">
|
||||
{#each dogShortcuts as token}
|
||||
<button
|
||||
type="button"
|
||||
class="mgv2-chip"
|
||||
on:click={() => appendShortcut(token)}
|
||||
>+ {token}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if step === 2}
|
||||
<div
|
||||
class="mgv2-service-grid"
|
||||
class:mgv2-service-grid-invalid={errors.services}
|
||||
role="group"
|
||||
aria-label="Service interest"
|
||||
>
|
||||
{#each serviceOptions as service}
|
||||
{@const checked = selectedServices.includes(service)}
|
||||
<button
|
||||
type="button"
|
||||
class="mgv2-service"
|
||||
class:active={checked}
|
||||
aria-pressed={checked}
|
||||
on:click={() => toggleService(service)}
|
||||
>
|
||||
<span class="mgv2-service-check" aria-hidden="true">
|
||||
{#if checked}<Icon name="fas fa-check" />{/if}
|
||||
</span>
|
||||
<span class="mgv2-service-label">{service}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if errors.services}
|
||||
<span class="mgv2-error">{errors.services}</span>
|
||||
{/if}
|
||||
{:else if step === 3}
|
||||
<label class="mgv2-field" for="mgv2-message">
|
||||
<span class="mgv2-label">Notes for us</span>
|
||||
<textarea
|
||||
bind:value={message}
|
||||
id="mgv2-message"
|
||||
rows="5"
|
||||
placeholder="For example: nervous around bigger dogs, prefers shorter walks, recently rescued."
|
||||
></textarea>
|
||||
</label>
|
||||
{:else if step === 4}
|
||||
<div class="mgv2-grid-two">
|
||||
<label class="mgv2-field" for="mgv2-name">
|
||||
<span class="mgv2-label">Full name</span>
|
||||
<input
|
||||
bind:value={fullName}
|
||||
on:input={() => clearError('fullName')}
|
||||
id="mgv2-name"
|
||||
type="text"
|
||||
placeholder="Your full name"
|
||||
class:mgv2-input-invalid={errors.fullName}
|
||||
autocomplete="name"
|
||||
/>
|
||||
{#if errors.fullName}<span class="mgv2-error">{errors.fullName}</span>{/if}
|
||||
</label>
|
||||
|
||||
<label class="mgv2-field" for="mgv2-email">
|
||||
<span class="mgv2-label">Email</span>
|
||||
<input
|
||||
bind:value={email}
|
||||
on:input={() => clearError('email')}
|
||||
id="mgv2-email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
class:mgv2-input-invalid={errors.email}
|
||||
autocomplete="email"
|
||||
/>
|
||||
{#if errors.email}<span class="mgv2-error">{errors.email}</span>{/if}
|
||||
</label>
|
||||
|
||||
<label class="mgv2-field" for="mgv2-phone">
|
||||
<span class="mgv2-label">Phone</span>
|
||||
<input
|
||||
bind:value={phone}
|
||||
on:input={() => clearError('phone')}
|
||||
id="mgv2-phone"
|
||||
type="tel"
|
||||
placeholder="021 123 4567"
|
||||
class:mgv2-input-invalid={errors.phone}
|
||||
autocomplete="tel"
|
||||
/>
|
||||
{#if errors.phone}<span class="mgv2-error">{errors.phone}</span>{/if}
|
||||
</label>
|
||||
|
||||
<label class="mgv2-field" for="mgv2-location">
|
||||
<span class="mgv2-label">Suburb</span>
|
||||
<input
|
||||
bind:value={location}
|
||||
on:input={() => clearError('location')}
|
||||
id="mgv2-location"
|
||||
type="text"
|
||||
placeholder="For example, Grey Lynn"
|
||||
class:mgv2-input-invalid={errors.location}
|
||||
autocomplete="address-level2"
|
||||
/>
|
||||
{#if errors.location}<span class="mgv2-error">{errors.location}</span>{/if}
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mgv2-actions">
|
||||
{#if step > 1}
|
||||
<button type="button" class="mgv2-back" on:click={goBack}>
|
||||
<Icon name="fas fa-arrow-left" />
|
||||
Back
|
||||
</button>
|
||||
{:else}
|
||||
<span class="mgv2-back-spacer" aria-hidden="true"></span>
|
||||
{/if}
|
||||
|
||||
{#if step < totalSteps}
|
||||
<button type="button" class="mgv2-next" on:click={goNext}>
|
||||
Next
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="mgv2-next"
|
||||
on:click={handleSubmit}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? 'Sending…' : 'Send Meet & Greet request'}
|
||||
{#if !submitting}<Icon name="fas fa-arrow-right" />{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p class="mgv2-footnote">
|
||||
No payment, no pressure. Reply from a real person within 24 hours.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer footer={footerContent} />
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--mgv2-yellow: #FFC700;
|
||||
--mgv2-cream: #FAF8F1;
|
||||
--mgv2-ink: #0b0b0b;
|
||||
--mgv2-line: rgba(11, 11, 11, 0.08);
|
||||
--mgv2-muted: rgba(11, 11, 11, 0.55);
|
||||
}
|
||||
|
||||
.mgv2-page {
|
||||
background: var(--mgv2-cream);
|
||||
min-height: 100vh;
|
||||
padding: 56px 24px 88px;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.mgv2-shell {
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mgv2-intro {
|
||||
margin: 0 auto 28px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.mgv2-kicker {
|
||||
display: inline-block;
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(11, 11, 11, 0.05);
|
||||
color: var(--mgv2-ink);
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mgv2-intro h1 {
|
||||
margin: 0 0 8px;
|
||||
color: var(--mgv2-ink);
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(28px, 4vw, 38px);
|
||||
font-weight: 800;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.mgv2-intro p {
|
||||
margin: 0;
|
||||
color: var(--mgv2-muted);
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.mgv2-progress {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 28px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(11, 11, 11, 0.04);
|
||||
}
|
||||
|
||||
.mgv2-progress-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(11, 11, 11, 0.18);
|
||||
transition:
|
||||
width 0.22s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
background 0.18s ease;
|
||||
}
|
||||
|
||||
.mgv2-progress-dot.active {
|
||||
width: 32px;
|
||||
background: var(--mgv2-yellow);
|
||||
}
|
||||
|
||||
.mgv2-stack {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mgv2-decoy {
|
||||
position: absolute;
|
||||
inset: 14px -8px auto -8px;
|
||||
height: 100%;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mgv2-line);
|
||||
box-shadow: 0 18px 36px rgba(11, 11, 11, 0.08);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mgv2-decoy-left {
|
||||
transform: rotate(-3deg) translateY(-6px);
|
||||
}
|
||||
|
||||
.mgv2-decoy-right {
|
||||
transform: rotate(2deg) translateY(2px);
|
||||
}
|
||||
|
||||
.mgv2-card {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding: 36px 28px 28px;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mgv2-line);
|
||||
box-shadow:
|
||||
0 24px 60px rgba(11, 11, 11, 0.12),
|
||||
0 4px 14px rgba(11, 11, 11, 0.04);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mgv2-badge {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
right: 18px;
|
||||
z-index: 3;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 999px;
|
||||
background: var(--mgv2-yellow);
|
||||
color: var(--mgv2-ink);
|
||||
box-shadow:
|
||||
0 12px 24px rgba(255, 199, 0, 0.36),
|
||||
inset 0 0 0 2px rgba(11, 11, 11, 0.06);
|
||||
transform: rotate(8deg);
|
||||
}
|
||||
|
||||
.mgv2-badge :global(.icon) {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.mgv2-honeypot {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
top: -10000px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mgv2-step {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.mgv2-eyebrow {
|
||||
color: var(--mgv2-muted);
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mgv2-heading {
|
||||
margin: -4px 0 0;
|
||||
color: var(--mgv2-ink);
|
||||
font-family: var(--font-head);
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
line-height: 1.16;
|
||||
letter-spacing: -0.02em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.mgv2-helper {
|
||||
margin: -4px 0 6px;
|
||||
color: var(--mgv2-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.mgv2-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mgv2-label {
|
||||
color: var(--mgv2-ink);
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mgv2-field input,
|
||||
.mgv2-field textarea {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
border: 1px solid var(--mgv2-line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
color: var(--mgv2-ink);
|
||||
font-family: var(--font-body);
|
||||
font-size: 16px;
|
||||
line-height: 1.35;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.mgv2-field textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.mgv2-field input:focus,
|
||||
.mgv2-field textarea:focus {
|
||||
outline: none;
|
||||
border-color: rgba(11, 11, 11, 0.45);
|
||||
box-shadow: 0 0 0 4px rgba(255, 199, 0, 0.22);
|
||||
}
|
||||
|
||||
.mgv2-input-invalid,
|
||||
.mgv2-input-invalid:focus {
|
||||
border-color: rgba(192, 32, 38, 0.6);
|
||||
box-shadow: 0 0 0 4px rgba(192, 32, 38, 0.08);
|
||||
}
|
||||
|
||||
.mgv2-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mgv2-chip {
|
||||
appearance: none;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--mgv2-line);
|
||||
background: #fff;
|
||||
color: var(--mgv2-ink);
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
transform 0.16s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.mgv2-chip:hover {
|
||||
background: rgba(255, 199, 0, 0.16);
|
||||
border-color: rgba(255, 199, 0, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.mgv2-service-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mgv2-service {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--mgv2-line);
|
||||
background: #fff;
|
||||
color: var(--mgv2-ink);
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s ease, background 0.18s ease;
|
||||
}
|
||||
|
||||
.mgv2-service-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 7px;
|
||||
border: 1.5px solid var(--mgv2-line);
|
||||
background: #fff;
|
||||
color: var(--mgv2-ink);
|
||||
font-size: 11px;
|
||||
transition: background 0.18s ease, border-color 0.18s ease;
|
||||
}
|
||||
|
||||
.mgv2-service.active {
|
||||
border-color: var(--mgv2-ink);
|
||||
background: rgba(255, 199, 0, 0.14);
|
||||
}
|
||||
|
||||
.mgv2-service.active .mgv2-service-check {
|
||||
background: var(--mgv2-yellow);
|
||||
border-color: var(--mgv2-yellow);
|
||||
}
|
||||
|
||||
.mgv2-service:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px rgba(255, 199, 0, 0.22);
|
||||
}
|
||||
|
||||
.mgv2-grid-two {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mgv2-grid-two .mgv2-field:nth-child(3),
|
||||
.mgv2-grid-two .mgv2-field:nth-child(4) {
|
||||
grid-column: span 1;
|
||||
}
|
||||
|
||||
.mgv2-error {
|
||||
color: #b1262d;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.mgv2-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mgv2-back,
|
||||
.mgv2-back-spacer {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 44px;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--mgv2-ink);
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
border-radius: 999px;
|
||||
transition: background 0.18s ease;
|
||||
}
|
||||
|
||||
.mgv2-back-spacer {
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mgv2-back:hover {
|
||||
background: rgba(11, 11, 11, 0.06);
|
||||
}
|
||||
|
||||
.mgv2-next {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 48px;
|
||||
padding: 10px 22px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: var(--mgv2-yellow);
|
||||
color: var(--mgv2-ink);
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.01em;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
inset 0 -2px 0 rgba(11, 11, 11, 0.08),
|
||||
0 12px 24px rgba(255, 199, 0, 0.28);
|
||||
transition:
|
||||
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.18s ease,
|
||||
filter 0.18s ease;
|
||||
}
|
||||
|
||||
.mgv2-next:hover {
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(1.02);
|
||||
}
|
||||
|
||||
.mgv2-next:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.mgv2-footnote {
|
||||
margin: 32px auto 0;
|
||||
max-width: 360px;
|
||||
color: var(--mgv2-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.mgv2-page {
|
||||
padding: 36px 16px 64px;
|
||||
}
|
||||
|
||||
.mgv2-stack {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mgv2-decoy {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mgv2-card {
|
||||
padding: 30px 20px 22px;
|
||||
}
|
||||
|
||||
.mgv2-badge {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
top: -16px;
|
||||
right: 14px;
|
||||
}
|
||||
|
||||
.mgv2-badge :global(.icon) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.mgv2-heading {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.mgv2-service-grid,
|
||||
.mgv2-grid-two {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mgv2-next {
|
||||
padding: 10px 18px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user