v4.0.0.2
This commit is contained in:
@@ -35,12 +35,12 @@
|
||||
{#each standardSections as section}
|
||||
{@const enhanced = getEnhancedImage(section.imageUrl)}
|
||||
<section
|
||||
use:reveal
|
||||
use:reveal={{ distance: 0 }}
|
||||
class="about-section reveal-block"
|
||||
class:about-section-gradient={section.accent === 'gradient'}
|
||||
>
|
||||
<div class="page-inner about-section-grid" class:about-section-reverse={section.reverse}>
|
||||
<div class="about-copy">
|
||||
<div class="about-copy fade-up">
|
||||
{#if section.eyebrow}
|
||||
<span class="eyebrow about-eyebrow">{section.eyebrow}</span>
|
||||
{/if}
|
||||
@@ -49,7 +49,7 @@
|
||||
<p>{paragraph}</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="about-media">
|
||||
<div class="about-media scale-soft">
|
||||
{#if enhanced}
|
||||
<enhanced:img src={enhanced} alt={section.imageAlt} loading="lazy" decoding="async" />
|
||||
{:else}
|
||||
@@ -63,9 +63,9 @@
|
||||
<!-- ── Founder section ── -->
|
||||
{#if founderSection}
|
||||
{@const founderEnhanced = getEnhancedImage(founderSection.imageUrl)}
|
||||
<section use:reveal={{ delay: 50 }} class="about-founder reveal-block">
|
||||
<section use:reveal={{ delay: 50, distance: 0 }} class="about-founder reveal-block">
|
||||
<div class="page-inner about-founder-grid">
|
||||
<div class="about-founder-media">
|
||||
<div class="about-founder-media scale-soft">
|
||||
{#if founderEnhanced}
|
||||
<enhanced:img
|
||||
src={founderEnhanced}
|
||||
@@ -82,7 +82,7 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="about-founder-copy">
|
||||
<div class="about-founder-copy fade-up">
|
||||
<article class="about-founder-note">
|
||||
{#if founderSection.eyebrow}
|
||||
<span class="eyebrow about-eyebrow about-founder-kicker">{founderSection.eyebrow}</span>
|
||||
@@ -122,7 +122,7 @@
|
||||
<span>If you are unsure about anything, feel free to email me anytime.</span>
|
||||
</a>
|
||||
|
||||
<a href="/contact-us" class="btn btn-green btn-mobile-center about-founder-cta">
|
||||
<a href="/contact-us" class="btn btn-green btn-mobile-center about-founder-cta cta-shimmer">
|
||||
Book a free Meet & Greet
|
||||
</a>
|
||||
</article>
|
||||
@@ -133,13 +133,13 @@
|
||||
|
||||
<!-- ── FAQs ── -->
|
||||
{#if pageContent.faqs && pageContent.faqs.length}
|
||||
<section use:reveal={{ delay: 30 }} class="about-faq reveal-block">
|
||||
<section use:reveal={{ delay: 30, distance: 0 }} class="about-faq reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="about-faq-header">
|
||||
<div class="about-faq-header fade-up">
|
||||
<span class="eyebrow about-eyebrow">FAQ</span>
|
||||
<h2>{pageContent.faqTitle ?? 'Common questions'}</h2>
|
||||
</div>
|
||||
<div use:accordion class="faq about-faq-list">
|
||||
<div use:accordion class="faq about-faq-list fade-up">
|
||||
{#each pageContent.faqs as item}
|
||||
<details>
|
||||
<summary>{item.question}</summary>
|
||||
@@ -571,4 +571,15 @@
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Per-section choreography — copy leads, then the media settles after a
|
||||
short beat. Repeated across each about-section instance. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:global(.reveal-visible) .about-copy { transition-delay: 40ms; }
|
||||
:global(.reveal-visible) .about-media { transition-delay: 200ms; }
|
||||
:global(.reveal-visible) .about-founder-media { transition-delay: 60ms; }
|
||||
:global(.reveal-visible) .about-founder-copy { transition-delay: 220ms; }
|
||||
:global(.reveal-visible) .about-faq-header { transition-delay: 40ms; }
|
||||
:global(.reveal-visible) .about-faq-list { transition-delay: 180ms; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,21 +10,20 @@ async function fillOwnerStep() {
|
||||
await fireEvent.input(screen.getByLabelText(/^Email/i), {
|
||||
target: { value: 'alex@example.com' }
|
||||
});
|
||||
await fireEvent.input(screen.getByLabelText(/Contact #/i), {
|
||||
await fireEvent.input(screen.getByLabelText(/Phone number/i), {
|
||||
target: { value: '021 123 4567' }
|
||||
});
|
||||
}
|
||||
|
||||
async function fillDogStep() {
|
||||
await fireEvent.click(screen.getByLabelText('Tiny Gang Pack Walks'));
|
||||
await fireEvent.click(screen.getByLabelText('Other Services'));
|
||||
await fireEvent.input(screen.getByLabelText(/Dog's Name/i), {
|
||||
target: { value: 'Maya' }
|
||||
});
|
||||
await fireEvent.input(screen.getByLabelText(/Location/i), {
|
||||
await fireEvent.input(screen.getByLabelText(/Your suburb/i), {
|
||||
target: { value: 'Kingsland' }
|
||||
});
|
||||
await fireEvent.input(screen.getByLabelText(/Tiny Gang Pack Walks fit/i), {
|
||||
await fireEvent.click(screen.getByLabelText('Tiny Gang Pack Walks'));
|
||||
await fireEvent.input(screen.getByLabelText(/A bit about your dog/i), {
|
||||
target: { value: 'Loves small group walks.' }
|
||||
});
|
||||
}
|
||||
@@ -103,7 +102,7 @@ describe('BookingSection', () => {
|
||||
petName: 'Maya',
|
||||
location: 'Kingsland',
|
||||
message: 'Loves small group walks.',
|
||||
services: ['Tiny Gang Pack Walks', 'Other Services'],
|
||||
services: ['Tiny Gang Pack Walks'],
|
||||
website: '',
|
||||
referrer: 'https://www.google.com/',
|
||||
stepChanges: 1,
|
||||
@@ -144,7 +143,7 @@ describe('BookingSection', () => {
|
||||
await fireEvent.click(container.querySelector('.booking-next-button')!);
|
||||
expect(screen.getByText('Please tell us how we can help')).toBeInTheDocument();
|
||||
|
||||
await fireEvent.input(screen.getByLabelText(/Your Message/i), {
|
||||
await fireEvent.input(screen.getByLabelText(/How can we help/i), {
|
||||
target: { value: 'I would like to discuss a business partnership.' }
|
||||
});
|
||||
|
||||
|
||||
@@ -22,20 +22,20 @@
|
||||
$: founderStoryEnhanced = getEnhancedImage(founderStory.imageUrl);
|
||||
</script>
|
||||
|
||||
<section id="promise" use:reveal={{ delay: 20 }} class="reveal-block">
|
||||
<section id="promise" use:reveal={{ delay: 20, distance: 0 }} class="reveal-block">
|
||||
<div class="founder-inner">
|
||||
<article class="founder-note">
|
||||
<div class="founder-intro">
|
||||
<div class="founder-intro fade-up">
|
||||
<span class="eyebrow founder-kicker">A note from Aless</span>
|
||||
<span class="founder-greeting">Hi, Aless from Goodwalk <span class="founder-greeting-wave" aria-hidden="true">👋</span></span>
|
||||
</div>
|
||||
|
||||
<h2 class="founder-heading">
|
||||
<h2 class="founder-heading fade-up">
|
||||
<span class="founder-heading-main">{founderStory.title}</span>
|
||||
<span class="founder-heading-sub">Goodwalk is built around trust.</span>
|
||||
</h2>
|
||||
|
||||
<div class="founder-trust-strip" aria-label="What owners can expect from Goodwalk">
|
||||
<div class="founder-trust-strip fade-up" aria-label="What owners can expect from Goodwalk">
|
||||
<span class="founder-trust-label">What owners notice first</span>
|
||||
<ul class="founder-trust-list">
|
||||
{#each founderTrustNotes as note}
|
||||
@@ -44,29 +44,29 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="founder-body">
|
||||
<div class="founder-body fade-up">
|
||||
{#each founderStoryParagraphs as paragraph}
|
||||
<p>{paragraph}</p>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p class="founder-closing">
|
||||
<p class="founder-closing fade-up">
|
||||
Ready to <strong>{founderStory.emphasis}</strong>
|
||||
</p>
|
||||
|
||||
<div class="founder-actions">
|
||||
<div class="founder-actions fade-up">
|
||||
<a class="founder-contact-note" href="mailto:info@goodwalk.co.nz" aria-label="Email Aless at Goodwalk">
|
||||
<span class="founder-contact-wave" aria-hidden="true">👋</span>
|
||||
<span>If you are unsure about anything, feel free to email, call, or send me an Instagram DM anytime.</span>
|
||||
</a>
|
||||
|
||||
<a href={founderStory.cta.href} class="btn btn-green btn-with-arrow btn-hide-arrow-mobile founder-cta">
|
||||
<a href={founderStory.cta.href} class="btn btn-green btn-with-arrow btn-hide-arrow-mobile founder-cta cta-shimmer">
|
||||
{founderStory.cta.label}
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="founder-signoff">
|
||||
<div class="founder-signoff fade-up">
|
||||
<div class="founder-signoff-copy">
|
||||
<p class="founder-signoff-name">Aless, founder of Goodwalk</p>
|
||||
<p class="founder-signoff-line">The same calm face at the door, the same trusted routine for your dog.</p>
|
||||
@@ -102,6 +102,19 @@
|
||||
contain-intrinsic-size: 980px;
|
||||
}
|
||||
|
||||
/* Narrative cascade — the article unfolds top-to-bottom in a steady,
|
||||
unhurried rhythm. Intentionally slow steps (~90ms) so the eye reads
|
||||
each block as it lands, not as a rush. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:global(.reveal-visible) .founder-intro { transition-delay: 40ms; }
|
||||
:global(.reveal-visible) .founder-heading { transition-delay: 130ms; }
|
||||
:global(.reveal-visible) .founder-trust-strip { transition-delay: 220ms; }
|
||||
:global(.reveal-visible) .founder-body { transition-delay: 310ms; }
|
||||
:global(.reveal-visible) .founder-closing { transition-delay: 400ms; }
|
||||
:global(.reveal-visible) .founder-actions { transition-delay: 490ms; }
|
||||
:global(.reveal-visible) .founder-signoff { transition-delay: 580ms; }
|
||||
}
|
||||
|
||||
.founder-inner {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
<section id="how-it-works" use:reveal={{ delay: 30 }} class="reveal-block">
|
||||
<section id="how-it-works" use:reveal={{ delay: 30, distance: 0 }} class="reveal-block">
|
||||
<div class="hiw-inner">
|
||||
|
||||
<div class="section-header hiw-header">
|
||||
<div class="section-header hiw-header fade-up">
|
||||
<span class="eyebrow hiw-eyebrow">Getting started</span>
|
||||
<h2 class="section-heading">{content.title}</h2>
|
||||
{#if content.intro}
|
||||
@@ -23,18 +23,18 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="hiw-journey-bar" aria-label="How getting started works">
|
||||
<div class="hiw-journey-bar stagger-children" aria-label="How getting started works">
|
||||
{#each journeyChips as chip}
|
||||
<span class="hiw-journey-pill">
|
||||
<span class="hiw-journey-pill fade-up">
|
||||
<Icon name={chip.icon} className="hiw-journey-icon" />
|
||||
{chip.label}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="hiw-steps">
|
||||
<div class="hiw-steps stagger-children">
|
||||
{#each content.steps as step, index}
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-step fade-up">
|
||||
<div class="hiw-step-meta">
|
||||
<span class="hiw-phase">{step.phase}</span>
|
||||
<span class="hiw-num">0{index + 1}</span>
|
||||
@@ -54,8 +54,8 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="hiw-cta">
|
||||
<a href="#newlead" class="btn btn-green btn-mobile-center btn-with-arrow">
|
||||
<div class="hiw-cta fade-up">
|
||||
<a href="#newlead" class="btn btn-green btn-mobile-center btn-with-arrow cta-shimmer">
|
||||
Book your free Meet & Greet
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</a>
|
||||
@@ -364,4 +364,28 @@
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
/* Choreography: when the section reveal fires, the inner tiers come in
|
||||
as a hierarchy — header settles first, journey pills follow, then
|
||||
step cards cascade, then the CTA tail. Each tier's own stagger still
|
||||
applies on top of these base offsets, so within a tier the children
|
||||
ripple as expected. Kept inside @media so reduced-motion users skip
|
||||
the cascade and see everything at rest immediately. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:global(.reveal-visible) .hiw-header {
|
||||
transition-delay: 40ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible) .hiw-journey-bar > :nth-child(1) { transition-delay: 160ms; }
|
||||
:global(.reveal-visible) .hiw-journey-bar > :nth-child(2) { transition-delay: 220ms; }
|
||||
:global(.reveal-visible) .hiw-journey-bar > :nth-child(3) { transition-delay: 280ms; }
|
||||
|
||||
:global(.reveal-visible) .hiw-steps > :nth-child(1) { transition-delay: 340ms; }
|
||||
:global(.reveal-visible) .hiw-steps > :nth-child(2) { transition-delay: 420ms; }
|
||||
:global(.reveal-visible) .hiw-steps > :nth-child(3) { transition-delay: 500ms; }
|
||||
|
||||
:global(.reveal-visible) .hiw-cta {
|
||||
transition-delay: 600ms;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,24 +4,40 @@
|
||||
|
||||
export let instagram: HomePageContent['instagram'];
|
||||
|
||||
// Editorial layout — full-bleed photo with a dark scrim on the left.
|
||||
// Photo source is the existing tiny-gang pack walk image already used
|
||||
// elsewhere on the site, so no new assets are required.
|
||||
const editorialPhoto = '/images/goodwalk-tiny-gang-pack-walk-small-dogs-auckland.webp';
|
||||
const editorialPhotoAlt = 'Goodwalk Tiny Gang on a pack walk in Auckland';
|
||||
</script>
|
||||
|
||||
<aside id="instagram" aria-label="Follow Goodwalk on Instagram">
|
||||
<div class="instagram-stage">
|
||||
<div class="instagram-panel">
|
||||
<div class="instagram-copy">
|
||||
<span class="eyebrow instagram-kicker">Daily walks, happy dogs</span>
|
||||
<h2>{instagram.title}</h2>
|
||||
<p class="instagram-blurb">See our dogs in action — walks, play, and happy pups</p>
|
||||
<a href={instagram.href} target="_blank" rel="noopener" class="btn btn-green instagram-button">
|
||||
<Icon name="fab fa-instagram" />
|
||||
{instagram.label}
|
||||
</a>
|
||||
</div>
|
||||
<div class="ig-editorial">
|
||||
<div class="ig-editorial-photo">
|
||||
<img src={editorialPhoto} alt={editorialPhotoAlt} loading="lazy" decoding="async" />
|
||||
</div>
|
||||
|
||||
<div class="instagram-dog-wrap" aria-hidden="true">
|
||||
<img src="/images/goodwalk-instagram-dog-cutout.webp" alt="" class="instagram-dog" loading="lazy" decoding="async" />
|
||||
<div class="ig-editorial-scrim">
|
||||
<div class="ig-editorial-copy">
|
||||
<span class="ig-editorial-source">
|
||||
<Icon name="fab fa-instagram" />
|
||||
<span>{instagram.label}</span>
|
||||
</span>
|
||||
|
||||
<h2>{instagram.title}</h2>
|
||||
<p>Daily walks, real dogs, no filter. Park runs, pack moments, and the kind of updates you'd actually want to scroll.</p>
|
||||
|
||||
<a
|
||||
href={instagram.href}
|
||||
target={instagram.external ? '_blank' : undefined}
|
||||
rel={instagram.external ? 'noopener' : undefined}
|
||||
class="ig-editorial-cta"
|
||||
>
|
||||
<Icon name="fab fa-instagram" />
|
||||
<span>Follow {instagram.label}</span>
|
||||
<Icon name="fas fa-arrow-right" className="ig-editorial-arrow" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -30,133 +46,186 @@
|
||||
#instagram {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 360px;
|
||||
padding: 32px 24px 56px;
|
||||
}
|
||||
|
||||
.ig-editorial {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.instagram-stage {
|
||||
position: relative;
|
||||
max-width: 1040px;
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.instagram-panel {
|
||||
position: relative;
|
||||
min-height: 150px;
|
||||
padding: 24px 320px 24px 44px;
|
||||
border-radius: 28px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.52), transparent 42%),
|
||||
linear-gradient(135deg, rgba(255, 252, 242, 0.98), rgba(255, 243, 198, 0.96));
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
|
||||
0 26px 50px rgba(106, 80, 16, 0.14);
|
||||
overflow: visible;
|
||||
aspect-ratio: 2.6 / 1;
|
||||
min-height: 280px;
|
||||
box-shadow: 0 30px 70px rgba(17, 20, 24, 0.16);
|
||||
}
|
||||
|
||||
.instagram-copy {
|
||||
max-width: 580px;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.instagram-kicker {
|
||||
/* All visual styling comes from the shared .eyebrow utility. */
|
||||
display: inline-block;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.instagram-copy :global(h2) {
|
||||
max-width: none;
|
||||
margin: 0 0 6px;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.instagram-blurb {
|
||||
max-width: 520px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.instagram-button {
|
||||
margin-top: 14px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.instagram-dog-wrap {
|
||||
.ig-editorial-photo {
|
||||
position: absolute;
|
||||
right: -56px;
|
||||
bottom: -14px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
width: 380px;
|
||||
pointer-events: none;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.instagram-dog {
|
||||
display: block;
|
||||
.ig-editorial-photo img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 60% 40%;
|
||||
transform: scale(1.02);
|
||||
transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@media (min-width: 1800px) {
|
||||
.instagram-stage {
|
||||
max-width: 1144px;
|
||||
}
|
||||
.ig-editorial:hover .ig-editorial-photo img {
|
||||
transform: scale(1.06);
|
||||
}
|
||||
|
||||
.ig-editorial-scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(
|
||||
100deg,
|
||||
rgba(15, 24, 15, 0.86) 0%,
|
||||
rgba(15, 24, 15, 0.74) 36%,
|
||||
rgba(15, 24, 15, 0.18) 70%,
|
||||
rgba(15, 24, 15, 0) 100%
|
||||
);
|
||||
padding: 48px clamp(28px, 4vw, 56px);
|
||||
}
|
||||
|
||||
.ig-editorial-copy {
|
||||
max-width: 480px;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ig-editorial-source {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px 6px 10px;
|
||||
margin-bottom: 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
color: #fff;
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.ig-editorial-source :global(.icon) {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Specificity bump: #instagram h2 (id+element) lives in the global
|
||||
typography.css and would otherwise win. Prefixing #instagram here
|
||||
matches that specificity so the editorial sizing applies. */
|
||||
#instagram .ig-editorial-copy h2 {
|
||||
margin: 0 0 12px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(26px, 3vw, 36px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.08;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ig-editorial-copy p {
|
||||
margin: 0 0 26px;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
max-width: 36ch;
|
||||
}
|
||||
|
||||
.ig-editorial-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 13px 22px;
|
||||
border-radius: 999px;
|
||||
background: var(--yellow);
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 12px 24px rgba(255, 209, 0, 0.32);
|
||||
transition: transform 0.22s ease, box-shadow 0.22s ease;
|
||||
}
|
||||
|
||||
.ig-editorial-cta:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 18px 32px rgba(255, 209, 0, 0.38);
|
||||
}
|
||||
|
||||
.ig-editorial-cta :global(.ig-editorial-arrow) {
|
||||
transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.ig-editorial-cta:hover :global(.ig-editorial-arrow) {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* Mobile — stack the photo above the copy so neither gets crushed.
|
||||
Copy centers inside the green panel for a calmer vertical rhythm. */
|
||||
@media (max-width: 768px) {
|
||||
#instagram {
|
||||
padding-bottom: 120px;
|
||||
padding: 24px 16px 40px;
|
||||
}
|
||||
|
||||
.instagram-panel {
|
||||
padding: 30px 24px 180px;
|
||||
border-radius: 28px;
|
||||
.ig-editorial {
|
||||
aspect-ratio: auto;
|
||||
min-height: 0;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.instagram-copy {
|
||||
.ig-editorial-photo {
|
||||
position: relative;
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
.ig-editorial-scrim {
|
||||
position: relative;
|
||||
inset: auto;
|
||||
background: var(--gw-green);
|
||||
padding: 28px 22px 32px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ig-editorial-copy {
|
||||
max-width: none;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.instagram-copy :global(h2) {
|
||||
max-width: none;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
#instagram .ig-editorial-copy h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.instagram-blurb {
|
||||
max-width: none;
|
||||
font-size: inherit;
|
||||
.ig-editorial-copy p {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.instagram-button {
|
||||
margin-top: 24px;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ig-editorial-photo img,
|
||||
.ig-editorial-cta {
|
||||
transition: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.instagram-dog-wrap {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
bottom: -96px;
|
||||
width: min(300px, calc(100% - 32px));
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.instagram-dog {
|
||||
width: 100%;
|
||||
.ig-editorial-cta:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -155,8 +155,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
function fieldValue(value: string): string {
|
||||
return value.trim() || '—';
|
||||
function toStr(value: unknown): string {
|
||||
if (typeof value === 'string') return value;
|
||||
if (value == null) return '';
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function fieldValue(value: unknown): string {
|
||||
return toStr(value).trim() || '—';
|
||||
}
|
||||
|
||||
function resetIdle() {
|
||||
@@ -528,33 +534,7 @@
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function clearOptionalHealthFields() {
|
||||
noteInteraction();
|
||||
accessInstructions = '';
|
||||
hasFoodAllergies = 'no';
|
||||
foodAllergiesDetail = '';
|
||||
hasEnvAllergies = 'no';
|
||||
envAllergiesDetail = '';
|
||||
onSpecialDiet = 'no';
|
||||
specialDietDetail = '';
|
||||
onMedication = 'no';
|
||||
medicationDetail = '';
|
||||
|
||||
errors = {
|
||||
...errors,
|
||||
accessInstructions: '',
|
||||
hasFoodAllergies: '',
|
||||
foodAllergiesDetail: '',
|
||||
hasEnvAllergies: '',
|
||||
envAllergiesDetail: '',
|
||||
onSpecialDiet: '',
|
||||
specialDietDetail: '',
|
||||
onMedication: '',
|
||||
medicationDetail: '',
|
||||
};
|
||||
}
|
||||
|
||||
function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
|
||||
function buildSubmissionSnapshot(submittedAt: string): SubmissionSummary {
|
||||
return {
|
||||
submittedAt,
|
||||
sections: [
|
||||
@@ -651,22 +631,23 @@
|
||||
}
|
||||
|
||||
submitting = true;
|
||||
signedOnDate = new Date().toISOString();
|
||||
const submissionSnapshot = buildSubmissionSnapshot(signedOnDate);
|
||||
|
||||
const medicalNotes = [
|
||||
temperament.trim(),
|
||||
hasFoodAllergies === 'yes' ? `Food allergies: ${foodAllergiesDetail.trim()}` : '',
|
||||
hasEnvAllergies === 'yes' ? `Environmental allergies: ${envAllergiesDetail.trim()}` : '',
|
||||
onSpecialDiet === 'yes' ? `Special diet: ${specialDietDetail.trim()}` : '',
|
||||
onMedication === 'yes' ? `Medication: ${medicationDetail.trim()}` : '',
|
||||
additionalNotes.trim(),
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = window.setTimeout(() => controller.abort(), 25000);
|
||||
|
||||
try {
|
||||
signedOnDate = new Date().toISOString();
|
||||
const submissionSnapshot = buildSubmissionSnapshot(signedOnDate);
|
||||
|
||||
const medicalNotes = [
|
||||
toStr(temperament).trim(),
|
||||
hasFoodAllergies === 'yes' ? `Food allergies: ${toStr(foodAllergiesDetail).trim()}` : '',
|
||||
hasEnvAllergies === 'yes' ? `Environmental allergies: ${toStr(envAllergiesDetail).trim()}` : '',
|
||||
onSpecialDiet === 'yes' ? `Special diet: ${toStr(specialDietDetail).trim()}` : '',
|
||||
onMedication === 'yes' ? `Medication: ${toStr(medicationDetail).trim()}` : '',
|
||||
toStr(additionalNotes).trim(),
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
const response = await fetch('/api/onboarding-submit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -1148,13 +1129,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-subsection-actions">
|
||||
<button class="subsection-quick-action" type="button" on:click={clearOptionalHealthFields}>
|
||||
<Icon name="fas fa-check" />
|
||||
Clear optional health details
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span>Home access instructions</span>
|
||||
<textarea
|
||||
@@ -2060,12 +2034,6 @@
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.panel-subsection-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin: -2px 0 4px;
|
||||
}
|
||||
|
||||
.panel-subsection-head h3 {
|
||||
margin: 0;
|
||||
color: #171b20;
|
||||
@@ -2097,40 +2065,7 @@
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.subsection-quick-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(33, 48, 33, 0.12);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: rgba(33, 48, 33, 0.84);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background 0.18s ease,
|
||||
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.subsection-quick-action:hover {
|
||||
border-color: rgba(33, 48, 33, 0.22);
|
||||
background: #fff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 18px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.subsection-quick-action:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px rgba(255, 209, 0, 0.14);
|
||||
}
|
||||
|
||||
.onboarding-step-icon {
|
||||
.onboarding-step-icon {
|
||||
flex: none;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
@@ -2803,8 +2738,7 @@
|
||||
.confirm-row,
|
||||
.yn-btn,
|
||||
.recall-btn,
|
||||
.chip-option,
|
||||
.subsection-quick-action {
|
||||
.chip-option {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
@@ -2965,10 +2899,6 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.panel-subsection-actions {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.panel-subsection-head h3 {
|
||||
font-size: 17px;
|
||||
}
|
||||
@@ -2985,12 +2915,7 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.subsection-quick-action {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.signature-header {
|
||||
.signature-header {
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -54,36 +54,36 @@ describe('OnboardingPage', () => {
|
||||
await waitFor(() => expect(screen.getByPlaceholderText('First name')).toBeInTheDocument());
|
||||
|
||||
await fireEvent.input(screen.getByPlaceholderText('First name'), { target: { value: 'Alex' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Surname'), { target: { value: 'Walker' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Last name'), { target: { value: 'Walker' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('you@example.com'), { target: { value: 'alex@example.com' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('021 234 5678'), { target: { value: '0212345678' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Street address'), { target: { value: '1 Test Street' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Street, suburb, city'), { target: { value: '1 Test Street' } });
|
||||
await fireEvent.click(screen.getByRole('button', { name: /save and continue/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByPlaceholderText('Dog name')).toBeInTheDocument());
|
||||
await fireEvent.input(screen.getByPlaceholderText('Dog name'), { target: { value: 'Milo' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Breed'), { target: { value: 'Spoodle' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('e.g. Labrador, Cavoodle, mixed'), { target: { value: 'Spoodle' } });
|
||||
await fireEvent.input(screen.getByLabelText(/date of birth/i), { target: { value: '2020-01-01' } });
|
||||
await clickService('Tiny Gang Pack Walks');
|
||||
await fireEvent.click(screen.getByRole('button', { name: /save and continue/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByPlaceholderText('Vet clinic or vet name')).toBeInTheDocument());
|
||||
await fireEvent.input(screen.getByPlaceholderText('Vet clinic or vet name'), { target: { value: 'Grey Lynn Vets' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Vet phone number'), { target: { value: '099999999' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Vet address'), { target: { value: '2 Vet Street' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Emergency contact name'), { target: { value: 'Jamie Walker' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Emergency contact number'), { target: { value: '0211111111' } });
|
||||
await chooseOption(/is your dog vaccinated/i, 'Yes');
|
||||
await chooseOption(/does your dog have any food allergies/i, 'No');
|
||||
await chooseOption(/does your dog have any environmental allergies/i, 'No');
|
||||
await chooseOption(/is your dog on a special diet/i, 'No');
|
||||
await chooseOption(/is your dog taking any medication/i, 'No');
|
||||
await waitFor(() => expect(screen.getByPlaceholderText('Clinic name or your usual vet')).toBeInTheDocument());
|
||||
await fireEvent.input(screen.getByPlaceholderText('Clinic name or your usual vet'), { target: { value: 'Grey Lynn Vets' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Clinic phone number'), { target: { value: '099999999' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Clinic address'), { target: { value: '2 Vet Street' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Full name'), { target: { value: 'Jamie Walker' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Phone number'), { target: { value: '0211111111' } });
|
||||
await chooseOption(/vaccinations up to date/i, 'Yes');
|
||||
await chooseOption(/^Any food allergies\?$/i, 'No');
|
||||
await chooseOption(/^Any environmental allergies\?$/i, 'No');
|
||||
await chooseOption(/^On a special diet\?$/i, 'No');
|
||||
await chooseOption(/medication that could affect walks/i, 'No');
|
||||
await fireEvent.click(screen.getByRole('button', { name: /save and continue/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText(/registered with council/i)).toBeInTheDocument());
|
||||
await chooseOption(/registered with council/i, 'Yes');
|
||||
await fireEvent.click(screen.getByRole('button', { name: /save and continue/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText(/anything else you'd like us to know/i)).toBeInTheDocument());
|
||||
await waitFor(() => expect(screen.getByText(/anything else we should know/i)).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,18 +41,18 @@
|
||||
/>
|
||||
|
||||
{#if pageContent.comparison}
|
||||
<section use:reveal id="pricing-chooser" class="pricing-guide reveal-block">
|
||||
<section use:reveal={{ distance: 0 }} id="pricing-chooser" class="pricing-guide reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="pricing-guide-heading">
|
||||
<div class="pricing-guide-heading fade-up">
|
||||
<span class="eyebrow">{pageContent.comparison.title}</span>
|
||||
{#if pageContent.comparison.intro}
|
||||
<h2>{pageContent.comparison.intro}</h2>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="pricing-chooser-grid">
|
||||
<div class="pricing-chooser-grid stagger-children">
|
||||
{#each pageContent.sections as section, sectionIndex}
|
||||
<article class="pricing-chooser-card">
|
||||
<article class="pricing-chooser-card fade-up">
|
||||
{#if section.imageUrl}
|
||||
<div class="pricing-chooser-image-wrap">
|
||||
<img class="pricing-chooser-image" src={section.imageUrl} alt={section.imageAlt ?? section.title} loading="lazy" decoding="async" />
|
||||
@@ -103,19 +103,19 @@
|
||||
|
||||
{#each pageContent.sections as section, sectionIndex}
|
||||
<section
|
||||
use:reveal
|
||||
use:reveal={{ distance: 0 }}
|
||||
id={`pricing-section-${sectionIndex}`}
|
||||
class:pricing-detail--alt={sectionIndex % 2 === 1}
|
||||
class="pricing-detail reveal-block"
|
||||
>
|
||||
<div class="page-inner pricing-detail-inner">
|
||||
<div class:pricing-detail-media--right={sectionIndex % 2 === 1} class="pricing-detail-media">
|
||||
<div class:pricing-detail-media--right={sectionIndex % 2 === 1} class="pricing-detail-media scale-soft">
|
||||
{#if section.imageUrl}
|
||||
<img src={section.imageUrl} alt={section.imageAlt ?? section.title} loading="lazy" decoding="async" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="pricing-detail-copy">
|
||||
<div class="pricing-detail-copy fade-up">
|
||||
{#if section.eyebrow}
|
||||
<p class="eyebrow pricing-detail-eyebrow">{section.eyebrow}</p>
|
||||
{/if}
|
||||
@@ -158,14 +158,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-inner pricing-plan-shell">
|
||||
<div class="page-inner pricing-plan-shell fade-up">
|
||||
<div class:pricing-plan-grid-three={section.plans.length === 3} class="pricing-plan-grid">
|
||||
{#each decoratePlans(section.plans) as plan}
|
||||
<PricingPlanCard {plan} variant="pricing" hideCtaOnMobile={true} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<a class="btn btn-yellow pricing-section-mobile-cta" href="#newlead">
|
||||
<a class="btn btn-yellow pricing-section-mobile-cta cta-shimmer" href="#newlead">
|
||||
Book a Meet & Greet
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</a>
|
||||
@@ -173,8 +173,8 @@
|
||||
</section>
|
||||
{/each}
|
||||
|
||||
<section use:reveal class="pricing-worth reveal-block" aria-labelledby="pricing-worth-heading">
|
||||
<div class="page-inner pricing-worth-inner">
|
||||
<section use:reveal={{ distance: 0 }} class="pricing-worth reveal-block" aria-labelledby="pricing-worth-heading">
|
||||
<div class="page-inner pricing-worth-inner fade-up">
|
||||
<span class="eyebrow eyebrow--accent pricing-worth-kicker">On the price</span>
|
||||
<h2 id="pricing-worth-heading">A better walk costs more.</h2>
|
||||
|
||||
@@ -188,16 +188,16 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section use:reveal class="pricing-included reveal-block">
|
||||
<section use:reveal={{ distance: 0 }} class="pricing-included reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="pricing-included-heading">
|
||||
<div class="pricing-included-heading fade-up">
|
||||
<span class="eyebrow">Every booking</span>
|
||||
<h2>Easy to start. Easy to trust.</h2>
|
||||
</div>
|
||||
|
||||
<ul class="pricing-included-list" aria-label="What every Goodwalk booking includes">
|
||||
<ul class="pricing-included-list stagger-children" aria-label="What every Goodwalk booking includes">
|
||||
{#each includedPromises as promise}
|
||||
<li>
|
||||
<li class="fade-up">
|
||||
<span class="pricing-included-icon" aria-hidden="true">
|
||||
<Icon name={promise.icon} />
|
||||
</span>
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
/>
|
||||
|
||||
{#if pageContent.highlight}
|
||||
<section use:reveal class="service-highlight reveal-block">
|
||||
<section use:reveal={{ distance: 0 }} class="service-highlight reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class:service-highlight-layout-points={pageContent.highlight.points?.length} class="service-highlight-layout">
|
||||
<div class="service-highlight-copy">
|
||||
@@ -187,9 +187,9 @@
|
||||
<h2>{pageContent.highlight.title}</h2>
|
||||
|
||||
{#if pageContent.highlight.points?.length}
|
||||
<div class="service-highlight-points">
|
||||
<div class="service-highlight-points stagger-children">
|
||||
{#each pageContent.highlight.points as point}
|
||||
<article class="service-highlight-point">
|
||||
<article class="service-highlight-point fade-up">
|
||||
<h3>{point.title}</h3>
|
||||
<p>{point.body}</p>
|
||||
</article>
|
||||
@@ -234,7 +234,7 @@
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section use:reveal class="service-benefits reveal-block">
|
||||
<section use:reveal={{ distance: 0 }} class="service-benefits reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="service-section-heading">
|
||||
<h2>{pageContent.benefits.title}</h2>
|
||||
@@ -244,12 +244,12 @@
|
||||
</div>
|
||||
|
||||
<div class="service-benefit-shell">
|
||||
<div bind:this={benefitScroller} class:service-benefit-grid-simple-swipe={useSimpleBenefitSwipe} class="service-benefit-grid">
|
||||
<div bind:this={benefitScroller} class:service-benefit-grid-simple-swipe={useSimpleBenefitSwipe} class="service-benefit-grid stagger-children">
|
||||
{#each benefitCards as benefit, index}
|
||||
<article
|
||||
class:active={index === activeBenefitIndex}
|
||||
class:service-benefit-card-featured={benefit.featured}
|
||||
class={`service-benefit-card ${benefit.tintClass}`}
|
||||
class={`service-benefit-card fade-up ${benefit.tintClass}`}
|
||||
>
|
||||
<div class="service-benefit-icon" aria-hidden="true">
|
||||
<Icon name={benefit.icon ?? 'fas fa-paw'} />
|
||||
@@ -344,7 +344,7 @@
|
||||
{/if}
|
||||
|
||||
{#if pageContent.decision}
|
||||
<section class:service-decision-pack={isPackWalks} class:service-decision-dog={isDogWalking} use:reveal class="service-decision reveal-block" aria-labelledby="service-decision-heading">
|
||||
<section class:service-decision-pack={isPackWalks} class:service-decision-dog={isDogWalking} use:reveal={{ distance: 0 }} class="service-decision reveal-block" aria-labelledby="service-decision-heading">
|
||||
<div class="page-inner">
|
||||
<div class="service-decision-card">
|
||||
<div class="service-decision-header">
|
||||
@@ -356,8 +356,8 @@
|
||||
{decisionIntro}
|
||||
</p>
|
||||
</div>
|
||||
<div class="service-decision-grid">
|
||||
<div class="service-decision-col service-decision-col-fit">
|
||||
<div class="service-decision-grid stagger-children">
|
||||
<div class="service-decision-col service-decision-col-fit fade-up">
|
||||
<span class="service-decision-col-kicker service-decision-col-kicker-fit">{decisionFitKicker}</span>
|
||||
<h3>{pageContent.decision.fitTitle ?? 'A good fit if your dog:'}</h3>
|
||||
<ul>
|
||||
@@ -371,7 +371,7 @@
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="service-decision-col service-decision-col-not">
|
||||
<div class="service-decision-col service-decision-col-not fade-up">
|
||||
<span class="service-decision-col-kicker service-decision-col-kicker-not">{decisionNotKicker}</span>
|
||||
<h3>{pageContent.decision.notFitTitle ?? 'Probably not a fit if:'}</h3>
|
||||
<ul>
|
||||
@@ -395,7 +395,7 @@
|
||||
{/if}
|
||||
|
||||
<section
|
||||
use:reveal
|
||||
use:reveal={{ distance: 0 }}
|
||||
class:service-pricing-immediate-mobile={useSimpleBenefitSwipe}
|
||||
class:service-pricing-pack={isPackWalks}
|
||||
class="service-pricing reveal-block"
|
||||
@@ -410,7 +410,7 @@
|
||||
|
||||
<div class:service-plan-grid-three={pageContent.pricing.plans.length === 3} class="service-plan-grid">
|
||||
{#each pricingPlans as plan}
|
||||
<PricingPlanCard {plan} variant="service" />
|
||||
<PricingPlanCard {plan} variant="service" hideCtaOnMobile={true} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -426,7 +426,7 @@
|
||||
Every booking starts with a free, no-obligation Meet & Greet.
|
||||
</p>
|
||||
|
||||
<a class="btn btn-yellow btn-mobile-center service-plan-mobile-cta" href="#newlead">Book a Meet & Greet</a>
|
||||
<a class="btn btn-yellow btn-mobile-center service-plan-mobile-cta cta-shimmer" href="#newlead">Book a Meet & Greet</a>
|
||||
|
||||
{#if pageContent.pricing.extras?.length}
|
||||
<div class="service-extras" aria-labelledby="service-extras-heading">
|
||||
@@ -437,9 +437,9 @@
|
||||
</span>
|
||||
<h3 id="service-extras-heading" class="service-extras-title">Optional extras</h3>
|
||||
</div>
|
||||
<div class="service-extras-grid">
|
||||
<div class="service-extras-grid stagger-children">
|
||||
{#each pageContent.pricing.extras as extra}
|
||||
<article class="service-extra-card">
|
||||
<article class="service-extra-card fade-up">
|
||||
<div class="service-extra-card-body">
|
||||
<span class="service-extra-card-label">{extra.label}</span>
|
||||
{#if extra.note}
|
||||
@@ -515,15 +515,15 @@
|
||||
{/if}
|
||||
|
||||
{#if showRelatedServices}
|
||||
<section use:reveal class="service-related reveal-block" aria-label="Other services">
|
||||
<section use:reveal={{ distance: 0 }} class="service-related reveal-block" aria-label="Other services">
|
||||
<div class="page-inner">
|
||||
<div class="service-section-heading">
|
||||
<h2>Explore our other services</h2>
|
||||
</div>
|
||||
|
||||
<div class="service-related-grid">
|
||||
<div class="service-related-grid stagger-children">
|
||||
{#each relatedCards as card, i}
|
||||
<a class="service-related-card service-related-tint-{i % 4}" href={card.href}>
|
||||
<a class="service-related-card fade-up service-related-tint-{i % 4}" href={card.href}>
|
||||
<div class="service-related-icon" aria-hidden="true">
|
||||
<Icon name={card.icon} />
|
||||
</div>
|
||||
|
||||
@@ -67,22 +67,22 @@
|
||||
.map(({ service }) => service);
|
||||
</script>
|
||||
|
||||
<section id="services" use:reveal={{ delay: 20 }} class="reveal-block">
|
||||
<section id="services" use:reveal={{ delay: 20, distance: 0 }} class="reveal-block">
|
||||
<div class="services-inner">
|
||||
<div class="section-header">
|
||||
<div class="section-header fade-up">
|
||||
<h2 class="section-heading">{heading}</h2>
|
||||
<p class="section-intro services-intro">{intro}</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="services-grid">
|
||||
|
||||
<div class="services-grid stagger-children">
|
||||
{#each orderedServices as service}
|
||||
{@const meta = serviceMeta[service.title]}
|
||||
<a
|
||||
href={service.href}
|
||||
class:service-card-featured={meta?.featured}
|
||||
class="service-card"
|
||||
class="service-card fade-up"
|
||||
aria-label={`${service.title} — view service page`}
|
||||
>
|
||||
<div class="service-card-media">
|
||||
@@ -452,4 +452,15 @@
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
/* Tier choreography — header settles, then the three cards cascade. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:global(.reveal-visible) .section-header {
|
||||
transition-delay: 40ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible) .services-grid > :nth-child(1) { transition-delay: 180ms; }
|
||||
:global(.reveal-visible) .services-grid > :nth-child(2) { transition-delay: 260ms; }
|
||||
:global(.reveal-visible) .services-grid > :nth-child(3) { transition-delay: 340ms; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
cta={{ label: 'Book a Meet & Greet', href: '#newlead', variant: 'yellow' }}
|
||||
/>
|
||||
|
||||
<section use:reveal class="testimonials-page-grid-section reveal-block">
|
||||
<section use:reveal={{ threshold: 0.02 }} class="testimonials-page-grid-section reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="testimonials-page-grid">
|
||||
{#each testimonialCards as testimonial}
|
||||
@@ -399,43 +399,41 @@
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
opacity 0.56s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.56s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.28s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
border-color 0.24s ease;
|
||||
opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(1) {
|
||||
transition-delay: 60ms;
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(2) {
|
||||
transition-delay: 140ms;
|
||||
transition-delay: 50ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(3) {
|
||||
transition-delay: 220ms;
|
||||
transition-delay: 100ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(4) {
|
||||
transition-delay: 300ms;
|
||||
transition-delay: 150ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(5) {
|
||||
transition-delay: 380ms;
|
||||
transition-delay: 200ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(6) {
|
||||
transition-delay: 460ms;
|
||||
transition-delay: 250ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-google-cta-wrap {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
opacity 0.56s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.56s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
transition-delay: 240ms;
|
||||
opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
transition-delay: 140ms;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
@@ -482,7 +480,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
@media (min-width: 1180px) {
|
||||
.testimonials-page-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
max-width: 1500px;
|
||||
|
||||
@@ -181,10 +181,10 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<section id="testimonials" use:reveal={{ delay: 40 }} class="reveal-block">
|
||||
<section id="testimonials" use:reveal={{ delay: 40, distance: 0 }} class="reveal-block">
|
||||
<div class="testimonials-inner">
|
||||
<div class="testimonials-header">
|
||||
<div class="testimonials-header-main">
|
||||
<div class="testimonials-header-main fade-up">
|
||||
<span class="eyebrow testimonials-eyebrow">
|
||||
<Icon name="fas fa-star" className="testimonials-eyebrow-star" />
|
||||
{eyebrow}
|
||||
@@ -192,13 +192,13 @@
|
||||
<h2 class="section-heading">{heading}</h2>
|
||||
</div>
|
||||
|
||||
<div class="testimonials-header-side">
|
||||
<div class="testimonials-header-side fade-up">
|
||||
<div class="testimonials-intro">
|
||||
<p>{blurb}</p>
|
||||
</div>
|
||||
|
||||
<div class="testimonials-cta-row">
|
||||
<a href={testimonialsHref} class="btn btn-yellow testimonials-cta testimonials-cta-primary">
|
||||
<a href={testimonialsHref} class="btn btn-yellow testimonials-cta testimonials-cta-primary cta-shimmer">
|
||||
<Icon name="fas fa-comment-dots" />
|
||||
<span class="testimonials-cta-label-desktop">All testimonials</span>
|
||||
<span class="testimonials-cta-label-mobile">Testimonials</span>
|
||||
@@ -215,7 +215,7 @@
|
||||
{#if slides.length}
|
||||
<div
|
||||
bind:this={carouselEl}
|
||||
class="testimonials-carousel"
|
||||
class="testimonials-carousel scale-soft"
|
||||
role="region"
|
||||
aria-label="Customer testimonials"
|
||||
on:mouseenter={() => (paused = true)}
|
||||
@@ -527,6 +527,12 @@
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:global(.reveal-visible) .testimonials-header-main { transition-delay: 40ms; }
|
||||
:global(.reveal-visible) .testimonials-header-side { transition-delay: 140ms; }
|
||||
:global(.reveal-visible) .testimonials-carousel { transition-delay: 260ms; }
|
||||
}
|
||||
|
||||
.testimonial-stage {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -84,15 +84,15 @@
|
||||
.map(({ value }) => value);
|
||||
</script>
|
||||
|
||||
<section id="values" use:reveal={{ delay: 30 }} class="reveal-block">
|
||||
<section id="values" use:reveal={{ delay: 30, distance: 0 }} class="reveal-block">
|
||||
<div class="values-inner">
|
||||
<div class="section-header">
|
||||
<div class="section-header fade-up">
|
||||
<h2 class="section-heading">Calmer dogs. Calmer evenings.</h2>
|
||||
</div>
|
||||
|
||||
<div class="values-photo-grid" aria-label="Goodwalk client dogs">
|
||||
<div class="values-photo-grid stagger-children stagger-tight" aria-label="Goodwalk client dogs">
|
||||
{#each clientPhotoCards as photo, index}
|
||||
<figure class:values-photo-card-featured={index === 0} class="values-photo-card">
|
||||
<figure class:values-photo-card-featured={index === 0} class="values-photo-card fade-up">
|
||||
{#if photo.enhanced}
|
||||
<enhanced:img
|
||||
class="values-photo-image"
|
||||
@@ -120,9 +120,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="values-bento values-contrast">
|
||||
<div class="values-bento values-contrast stagger-children">
|
||||
{#each stakes as stake, index}
|
||||
<article class:values-contrast-cell-good={index === 1} class="values-contrast-cell">
|
||||
<article class:values-contrast-cell-good={index === 1} class="values-contrast-cell fade-up">
|
||||
<div class="values-contrast-head">
|
||||
<span class:values-contrast-label-good={index === 1} class="values-contrast-label">
|
||||
{stake.label}
|
||||
@@ -149,13 +149,13 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="values-points-header">
|
||||
<div class="values-points-header fade-up">
|
||||
<h3 class="values-points-title">Things we don't treat as extras</h3>
|
||||
</div>
|
||||
|
||||
<div class="values-bento values-points">
|
||||
<div class="values-bento values-points stagger-children">
|
||||
{#each orderedValues as value}
|
||||
<div class="values-point">
|
||||
<div class="values-point fade-up">
|
||||
<div class="values-point-icon">
|
||||
<Icon name={value.icon} className="values-point-glyph" />
|
||||
</div>
|
||||
@@ -627,4 +627,25 @@
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
/* Tier choreography — header, then photo gallery cascades tight,
|
||||
contrast cells settle, then the bottom-row "extras" cascade. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:global(.reveal-visible) .values-inner > .section-header { transition-delay: 40ms; }
|
||||
|
||||
:global(.reveal-visible) .values-photo-grid > :nth-child(1) { transition-delay: 160ms; }
|
||||
:global(.reveal-visible) .values-photo-grid > :nth-child(2) { transition-delay: 220ms; }
|
||||
:global(.reveal-visible) .values-photo-grid > :nth-child(3) { transition-delay: 280ms; }
|
||||
:global(.reveal-visible) .values-photo-grid > :nth-child(4) { transition-delay: 340ms; }
|
||||
:global(.reveal-visible) .values-photo-grid > :nth-child(5) { transition-delay: 400ms; }
|
||||
|
||||
:global(.reveal-visible) .values-contrast > :nth-child(1) { transition-delay: 460ms; }
|
||||
:global(.reveal-visible) .values-contrast > :nth-child(2) { transition-delay: 540ms; }
|
||||
|
||||
:global(.reveal-visible) .values-points-header { transition-delay: 620ms; }
|
||||
:global(.reveal-visible) .values-points > :nth-child(1) { transition-delay: 680ms; }
|
||||
:global(.reveal-visible) .values-points > :nth-child(2) { transition-delay: 740ms; }
|
||||
:global(.reveal-visible) .values-points > :nth-child(3) { transition-delay: 800ms; }
|
||||
:global(.reveal-visible) .values-points > :nth-child(n + 4) { transition-delay: 860ms; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
};
|
||||
};
|
||||
|
||||
type LifecycleStatus = 'active' | 'paused' | 'cancelled' | 'archived';
|
||||
|
||||
type Lifecycle = {
|
||||
status: LifecycleStatus;
|
||||
reason: string;
|
||||
changedAt: string;
|
||||
changedBy: string;
|
||||
};
|
||||
|
||||
type AllClient = {
|
||||
email: string;
|
||||
fullName: string;
|
||||
@@ -33,10 +42,22 @@
|
||||
dogName: string;
|
||||
dogBreed: string;
|
||||
status: 'pending' | 'completed';
|
||||
lifecycle: Lifecycle;
|
||||
lastActivityAt: string;
|
||||
welcomePackSentAt: string;
|
||||
};
|
||||
|
||||
const lifecycleOptions: { value: LifecycleStatus; label: string; description: string }[] = [
|
||||
{ value: 'active', label: 'Active', description: 'Currently a client. Included in outreach.' },
|
||||
{ value: 'paused', label: 'Paused', description: 'Temporarily not walking. Still included in outreach.' },
|
||||
{ value: 'cancelled', label: 'Cancelled', description: 'No longer a client. Excluded from outreach but kept on file.' },
|
||||
{ value: 'archived', label: 'Archived', description: 'Long-term inactive. Hidden from outreach but kept for records.' },
|
||||
];
|
||||
|
||||
function lifecycleLabel(status: LifecycleStatus): string {
|
||||
return lifecycleOptions.find((o) => o.value === status)?.label ?? status;
|
||||
}
|
||||
|
||||
type BirthdayClient = {
|
||||
email: string;
|
||||
fullName: string;
|
||||
@@ -126,6 +147,118 @@
|
||||
let confirmPreview = false;
|
||||
let confirmBusy = false;
|
||||
let confirmError = '';
|
||||
let confirmPreviewHtml = '';
|
||||
let confirmPreviewBusy = false;
|
||||
let confirmPreviewToken = 0;
|
||||
|
||||
async function fetchConfirmPreview(ctx: ConfirmContext) {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
const stamp = ++confirmPreviewToken;
|
||||
confirmPreviewBusy = true;
|
||||
try {
|
||||
let url = '';
|
||||
let body: Record<string, unknown> = {};
|
||||
if (ctx.kind === 'welcome') {
|
||||
url = '/api/owner/render-welcome-pack';
|
||||
body = {
|
||||
email: ctx.client.email,
|
||||
serviceType: ctx.serviceType,
|
||||
priceDetails: ctx.priceDetails,
|
||||
startDate: ctx.startDate,
|
||||
};
|
||||
} else if (ctx.kind === 'birthday') {
|
||||
url = '/api/owner/render-birthday-email';
|
||||
body = { email: ctx.client.email };
|
||||
} else {
|
||||
// 'message' uses the live preview already rendered in the messaging tab.
|
||||
if (stamp === confirmPreviewToken) confirmPreviewHtml = msgPreviewHtml || '';
|
||||
return;
|
||||
}
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (stamp !== confirmPreviewToken) return;
|
||||
if (res.ok && typeof data?.html === 'string') {
|
||||
confirmPreviewHtml = data.html;
|
||||
} else {
|
||||
confirmPreviewHtml = '';
|
||||
}
|
||||
} catch {
|
||||
if (stamp === confirmPreviewToken) confirmPreviewHtml = '';
|
||||
} finally {
|
||||
if (stamp === confirmPreviewToken) confirmPreviewBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openConfirmPreviewInNewTab() {
|
||||
if (!confirmPreviewHtml) return;
|
||||
const blob = new Blob([confirmPreviewHtml], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
||||
}
|
||||
|
||||
$: if (confirmCtx) {
|
||||
void fetchConfirmPreview(confirmCtx);
|
||||
} else {
|
||||
confirmPreviewHtml = '';
|
||||
confirmPreviewBusy = false;
|
||||
}
|
||||
|
||||
let statusCtx: AllClient | null = null;
|
||||
let statusDraft: LifecycleStatus = 'active';
|
||||
let statusReason = '';
|
||||
let statusBusy = false;
|
||||
let statusError = '';
|
||||
|
||||
function openStatusEditor(client: AllClient) {
|
||||
statusCtx = client;
|
||||
statusDraft = client.lifecycle?.status ?? 'active';
|
||||
statusReason = client.lifecycle?.reason ?? '';
|
||||
statusError = '';
|
||||
}
|
||||
|
||||
function closeStatusEditor() {
|
||||
if (statusBusy) return;
|
||||
statusCtx = null;
|
||||
statusError = '';
|
||||
}
|
||||
|
||||
async function saveStatus() {
|
||||
if (!statusCtx) return;
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
statusError = 'Your session has expired. Please sign in again.';
|
||||
return;
|
||||
}
|
||||
statusBusy = true;
|
||||
statusError = '';
|
||||
try {
|
||||
const res = await fetch('/api/owner/client-status', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ email: statusCtx.email, status: statusDraft, reason: statusReason.trim() }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) throw new Error(data?.detail?.message ?? data?.detail ?? 'Could not update status.');
|
||||
|
||||
const updated = data?.lifecycle as Lifecycle | undefined;
|
||||
if (updated) {
|
||||
allClients = allClients.map((c) =>
|
||||
c.email === statusCtx!.email ? { ...c, lifecycle: updated } : c,
|
||||
);
|
||||
}
|
||||
statusCtx = null;
|
||||
} catch (error) {
|
||||
statusError = error instanceof Error ? error.message : 'Could not update status.';
|
||||
} finally {
|
||||
statusBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return window.localStorage.getItem('gw_onboarding_session') ?? '';
|
||||
@@ -825,6 +958,13 @@
|
||||
<svelte:head>
|
||||
<title>{header.title} | Goodwalk Admin</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<main class="owner-page">
|
||||
@@ -1067,9 +1207,17 @@
|
||||
|
||||
<div class="owner-cell">
|
||||
<span class="owner-mobile-label">Status</span>
|
||||
<span class:owner-status-complete={client.status === 'completed'} class="owner-status-pill">
|
||||
{client.status === 'completed' ? 'Completed' : 'Pending'}
|
||||
</span>
|
||||
<div class="owner-status-stack">
|
||||
<span class:owner-status-complete={client.status === 'completed'} class="owner-status-pill">
|
||||
{client.status === 'completed' ? 'Completed' : 'Pending'}
|
||||
</span>
|
||||
<span class="owner-lifecycle-pill" data-lifecycle={client.lifecycle?.status ?? 'active'}>
|
||||
{lifecycleLabel(client.lifecycle?.status ?? 'active')}
|
||||
</span>
|
||||
{#if client.lifecycle?.reason}
|
||||
<small class="owner-lifecycle-reason" title={client.lifecycle.reason}>{client.lifecycle.reason}</small>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="owner-cell">
|
||||
@@ -1079,12 +1227,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="owner-cell owner-action-cell">
|
||||
<div class="owner-cell owner-action-cell owner-action-stack">
|
||||
<span class="owner-mobile-label">Action</span>
|
||||
<button class="owner-secondary-btn" type="button" on:click={() => openEnquiry(client.email)}>
|
||||
<Icon name="fas fa-file-lines" />
|
||||
View enquiry
|
||||
</button>
|
||||
<button class="owner-secondary-btn" type="button" on:click={() => openStatusEditor(client)}>
|
||||
<Icon name="fas fa-circle-half-stroke" />
|
||||
Change status
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
@@ -1473,8 +1625,8 @@
|
||||
</section>
|
||||
|
||||
{#if confirmCtx}
|
||||
<div class="owner-modal-backdrop" role="presentation" on:click={closeConfirm}>
|
||||
<div class="owner-modal" role="dialog" aria-modal="true" aria-label="Confirm send" on:click|stopPropagation>
|
||||
<div class="owner-modal-backdrop" role="presentation" on:click|self={closeConfirm}>
|
||||
<div class="owner-modal" role="dialog" aria-modal="true" aria-label="Confirm send" tabindex="-1">
|
||||
<div class="owner-modal-head">
|
||||
<h2>
|
||||
{#if confirmCtx.kind === 'welcome'}
|
||||
@@ -1506,9 +1658,34 @@
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<div class="owner-modal-preview-shell">
|
||||
{#if confirmPreviewHtml}
|
||||
<iframe
|
||||
class="owner-modal-preview-iframe"
|
||||
title="Email preview"
|
||||
sandbox=""
|
||||
srcdoc={confirmPreviewHtml}
|
||||
></iframe>
|
||||
<div class="owner-modal-preview-actions">
|
||||
<small>This is exactly how the email will look.</small>
|
||||
<button type="button" class="owner-modal-preview-link" on:click={openConfirmPreviewInNewTab}>
|
||||
<Icon name="fas fa-up-right-from-square" />
|
||||
Open full preview
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="owner-modal-preview-empty">
|
||||
{confirmPreviewBusy ? 'Rendering preview…' : 'Preview unavailable.'}
|
||||
</div>
|
||||
{/if}
|
||||
{#if confirmPreviewBusy && confirmPreviewHtml}
|
||||
<span class="owner-modal-preview-busy">Refreshing…</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<label class="owner-preview-toggle">
|
||||
<input type="checkbox" bind:checked={confirmPreview} disabled={confirmBusy} />
|
||||
<span>Send a preview to me ({ownerEmail}) instead</span>
|
||||
<span>Also send a test copy to me ({ownerEmail}) instead of the client</span>
|
||||
</label>
|
||||
|
||||
{#if confirmError}
|
||||
@@ -1531,9 +1708,62 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if statusCtx}
|
||||
<div class="owner-modal-backdrop" role="presentation" on:click|self={closeStatusEditor}>
|
||||
<div class="owner-modal" role="dialog" aria-modal="true" aria-label="Change client status" tabindex="-1">
|
||||
<div class="owner-modal-head">
|
||||
<h2>Change status for {statusCtx.fullName || statusCtx.email}</h2>
|
||||
<button type="button" class="owner-modal-close" on:click={closeStatusEditor} aria-label="Close" disabled={statusBusy}>
|
||||
<Icon name="fas fa-xmark" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="owner-modal-status">
|
||||
Soft-change only. The client stays on file in case we want to reach out later
|
||||
(e.g. a newsletter). Cancelled and archived clients are excluded from outreach.
|
||||
</p>
|
||||
|
||||
<div class="status-options">
|
||||
{#each lifecycleOptions as opt}
|
||||
<label class:status-option-active={statusDraft === opt.value} class="status-option">
|
||||
<input type="radio" name="lifecycle-status" value={opt.value} bind:group={statusDraft} disabled={statusBusy} />
|
||||
<div>
|
||||
<strong>{opt.label}</strong>
|
||||
<small>{opt.description}</small>
|
||||
</div>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<label class="status-reason-field">
|
||||
<span>Reason (optional)</span>
|
||||
<textarea bind:value={statusReason} rows="3" maxlength="500" disabled={statusBusy}
|
||||
placeholder="e.g. moved overseas, switched walkers, on hold until spring…"></textarea>
|
||||
</label>
|
||||
|
||||
{#if statusCtx.lifecycle?.changedAt}
|
||||
<p class="owner-modal-status">
|
||||
Last changed {formatDateLabel(statusCtx.lifecycle.changedAt)}{statusCtx.lifecycle.changedBy ? ` by ${statusCtx.lifecycle.changedBy}` : ''}.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if statusError}
|
||||
<div class="owner-inline-error">{statusError}</div>
|
||||
{/if}
|
||||
|
||||
<div class="owner-compose-actions">
|
||||
<button class="owner-cancel-btn" type="button" on:click={closeStatusEditor} disabled={statusBusy}>Cancel</button>
|
||||
<button class="owner-send-btn" type="button" on:click={saveStatus} disabled={statusBusy}>
|
||||
{statusBusy ? 'Saving…' : 'Save status'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if enquiryOpen}
|
||||
<div class="owner-modal-backdrop" role="presentation" on:click={closeEnquiry}>
|
||||
<div class="owner-modal" role="dialog" aria-modal="true" aria-label="Client enquiry" on:click|stopPropagation>
|
||||
<div class="owner-modal-backdrop" role="presentation" on:click|self={closeEnquiry}>
|
||||
<div class="owner-modal" role="dialog" aria-modal="true" aria-label="Client enquiry" tabindex="-1">
|
||||
<div class="owner-modal-head">
|
||||
<h2>Website enquiry</h2>
|
||||
<button type="button" class="owner-modal-close" on:click={closeEnquiry} aria-label="Close">
|
||||
@@ -1585,6 +1815,18 @@
|
||||
|
||||
<style>
|
||||
.owner-page {
|
||||
/* Scope a clean Inter type system to the admin only — leaves the public
|
||||
site's Unbounded / Readex Pro stack untouched. */
|
||||
--admin-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
--font-head: var(--admin-font);
|
||||
--font-body: var(--admin-font);
|
||||
font-family: var(--admin-font);
|
||||
font-feature-settings: 'cv11', 'ss01', 'ss03';
|
||||
letter-spacing: -0.005em;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
|
||||
min-height: 100vh;
|
||||
background: #f7f6f1;
|
||||
color: var(--gw-green);
|
||||
@@ -1592,6 +1834,13 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.owner-page :global(input),
|
||||
.owner-page :global(textarea),
|
||||
.owner-page :global(select),
|
||||
.owner-page :global(button) {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.owner-shell {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
@@ -1719,29 +1968,8 @@
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.owner-hero {
|
||||
padding: 16px 0 10px;
|
||||
}
|
||||
|
||||
.owner-hero h1 {
|
||||
margin: 0 0 6px;
|
||||
color: rgba(17, 23, 27, 0.9);
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(22px, 3.4vw, 30px);
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.owner-hero p {
|
||||
margin: 0;
|
||||
max-width: 62ch;
|
||||
color: rgba(33, 48, 33, 0.66);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.owner-content {
|
||||
padding: 16px 0 112px;
|
||||
padding: 16px 0 calc(108px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.owner-summary-grid {
|
||||
@@ -1801,7 +2029,6 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.owner-section-head h2,
|
||||
.owner-message-card h2 {
|
||||
margin: 0;
|
||||
color: #171b20;
|
||||
@@ -1811,7 +2038,6 @@
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.owner-section-head p,
|
||||
.owner-message-card p {
|
||||
margin: 6px 0 0;
|
||||
color: rgba(33, 48, 33, 0.64);
|
||||
@@ -1932,24 +2158,6 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.owner-chip-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.owner-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
color: #213021;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.owner-status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1971,6 +2179,122 @@
|
||||
color: #213021;
|
||||
}
|
||||
|
||||
.owner-status-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.owner-lifecycle-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
color: rgba(33, 48, 33, 0.72);
|
||||
}
|
||||
|
||||
.owner-lifecycle-pill[data-lifecycle='active'] {
|
||||
background: rgba(60, 140, 75, 0.16);
|
||||
color: #1f5a2c;
|
||||
}
|
||||
|
||||
.owner-lifecycle-pill[data-lifecycle='paused'] {
|
||||
background: rgba(255, 209, 0, 0.22);
|
||||
color: #6a4a00;
|
||||
}
|
||||
|
||||
.owner-lifecycle-pill[data-lifecycle='cancelled'] {
|
||||
background: rgba(190, 60, 60, 0.14);
|
||||
color: #8a2a2a;
|
||||
}
|
||||
|
||||
.owner-lifecycle-pill[data-lifecycle='archived'] {
|
||||
background: rgba(33, 48, 33, 0.12);
|
||||
color: rgba(33, 48, 33, 0.62);
|
||||
}
|
||||
|
||||
.owner-lifecycle-reason {
|
||||
color: rgba(33, 48, 33, 0.6);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-options {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 6px 0 10px;
|
||||
}
|
||||
|
||||
.status-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(33, 48, 33, 0.12);
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.status-option input {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.status-option strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: #171b20;
|
||||
}
|
||||
|
||||
.status-option small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: rgba(33, 48, 33, 0.62);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.status-option-active {
|
||||
border-color: rgba(33, 48, 33, 0.36);
|
||||
background: rgba(255, 209, 0, 0.12);
|
||||
}
|
||||
|
||||
.status-reason-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status-reason-field span {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: rgba(33, 48, 33, 0.72);
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-reason-field textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(33, 48, 33, 0.16);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
min-height: 70px;
|
||||
}
|
||||
|
||||
.owner-open-btn,
|
||||
.owner-send-btn,
|
||||
.owner-cancel-btn,
|
||||
@@ -2175,43 +2499,51 @@
|
||||
|
||||
.owner-bottom-nav {
|
||||
position: fixed;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
bottom: calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
z-index: 25;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 2px;
|
||||
padding: 6px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 253, 250, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(17, 20, 24, 0.08);
|
||||
box-shadow: 0 6px 18px rgba(17, 20, 24, 0.08);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: saturate(180%) blur(18px);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(18px);
|
||||
border: 1px solid rgba(17, 20, 24, 0.06);
|
||||
box-shadow: 0 10px 28px rgba(17, 20, 24, 0.12), 0 1px 0 rgba(255, 255, 255, 0.6) inset;
|
||||
}
|
||||
|
||||
.owner-bottom-tab {
|
||||
min-height: 48px;
|
||||
min-height: 54px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
background: transparent;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
align-content: center;
|
||||
gap: 3px;
|
||||
color: rgba(33, 48, 33, 0.58);
|
||||
gap: 4px;
|
||||
color: rgba(33, 48, 33, 0.6);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease, color 0.18s ease, transform 0.18s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.owner-bottom-tab:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.owner-bottom-tab span {
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.005em;
|
||||
}
|
||||
|
||||
.owner-bottom-tab :global(i) {
|
||||
font-size: 13px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.owner-bottom-tab-active {
|
||||
@@ -2219,8 +2551,11 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.owner-bottom-tab-active span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.owner-tab:hover,
|
||||
.owner-open-btn:hover,
|
||||
.owner-send-btn:hover,
|
||||
.owner-cancel-btn:hover,
|
||||
@@ -2398,10 +2733,10 @@
|
||||
color: rgba(33, 48, 33, 0.4);
|
||||
}
|
||||
|
||||
.rte-editor p { margin: 0 0 12px; }
|
||||
.rte-editor ul,
|
||||
.rte-editor ol { margin: 0 0 12px; padding-left: 22px; }
|
||||
.rte-editor a { color: #c0392b; text-decoration: underline; }
|
||||
.rte-editor :global(p) { margin: 0 0 12px; }
|
||||
.rte-editor :global(ul),
|
||||
.rte-editor :global(ol) { margin: 0 0 12px; padding-left: 22px; }
|
||||
.rte-editor :global(a) { color: #c0392b; text-decoration: underline; }
|
||||
|
||||
.msg-form {
|
||||
display: grid;
|
||||
@@ -2563,25 +2898,6 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.owner-field textarea {
|
||||
width: 100%;
|
||||
min-height: 110px;
|
||||
padding: 12px 15px;
|
||||
border: 1px solid rgba(17, 20, 24, 0.1);
|
||||
border-radius: 14px;
|
||||
background: #fffdfa;
|
||||
font: inherit;
|
||||
color: #213021;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.owner-field textarea:focus {
|
||||
border-color: rgba(33, 48, 33, 0.42);
|
||||
box-shadow: 0 0 0 4px rgba(255, 209, 0, 0.12);
|
||||
}
|
||||
|
||||
.msg-recipient-modes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -2662,35 +2978,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.owner-footer-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.owner-footer-desktop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.owner-footer-desktop :global(.ob-footer-inner) {
|
||||
height: 38px;
|
||||
padding: 0 28px;
|
||||
}
|
||||
|
||||
.owner-footer-desktop :global(.ob-footer-back),
|
||||
.owner-footer-desktop :global(.ob-footer-email),
|
||||
.owner-footer-desktop :global(.ob-footer-logout) {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.owner-footer-desktop :global(.ob-footer-logout) {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.owner-content {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
@@ -2703,24 +2991,97 @@
|
||||
.owner-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(17, 20, 24, 0.45);
|
||||
backdrop-filter: blur(2px);
|
||||
background: rgba(17, 20, 24, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
padding: 14px;
|
||||
padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
|
||||
padding-top: calc(14px + env(safe-area-inset-top, 0px));
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
.owner-modal {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
max-width: 560px;
|
||||
max-height: min(92vh, 800px);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 18px;
|
||||
box-shadow: 0 24px 60px rgba(17, 20, 24, 0.25);
|
||||
border-radius: 18px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 24px 60px rgba(17, 20, 24, 0.28);
|
||||
}
|
||||
|
||||
.owner-modal-preview-shell {
|
||||
margin: 6px 0 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(17, 20, 24, 0.08);
|
||||
background: #f7f6f1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.owner-modal-preview-iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.owner-modal-preview-busy {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: rgba(33, 48, 33, 0.6);
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.owner-modal-preview-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 120px;
|
||||
color: rgba(33, 48, 33, 0.5);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.owner-modal-preview-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
border-top: 1px solid rgba(17, 20, 24, 0.06);
|
||||
}
|
||||
|
||||
.owner-modal-preview-actions small {
|
||||
color: rgba(33, 48, 33, 0.55);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.owner-modal-preview-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(33, 48, 33, 0.16);
|
||||
background: #fff;
|
||||
color: #213021;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.owner-modal-head {
|
||||
@@ -2832,21 +3193,56 @@
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.owner-page {
|
||||
/* Prevent any element from breaking horizontal scroll on a phone. */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.owner-shell {
|
||||
padding-left: max(16px, env(safe-area-inset-left, 0px));
|
||||
padding-right: max(16px, env(safe-area-inset-right, 0px));
|
||||
}
|
||||
|
||||
.owner-topbar-inner {
|
||||
min-height: 56px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.owner-logo img {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.owner-pill {
|
||||
font-size: 10px;
|
||||
padding: 4px 9px;
|
||||
}
|
||||
|
||||
.owner-topbar-logout {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.owner-summary-grid {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.owner-summary-pill {
|
||||
padding: 8px 10px;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
gap: 10px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.owner-summary-pill strong {
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.owner-summary-pill span {
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.owner-section-head {
|
||||
@@ -2859,6 +3255,16 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.owner-table-row {
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.owner-cell h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.owner-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -2867,19 +3273,45 @@
|
||||
}
|
||||
|
||||
.owner-pagination .owner-page-btn {
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
min-height: 40px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.owner-pagination .owner-page-indicator {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.owner-enquiry-list > div {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.owner-modal {
|
||||
max-width: 100%;
|
||||
max-height: calc(100dvh - 90px - env(safe-area-inset-bottom, 0px));
|
||||
border-radius: 20px 20px 16px 16px;
|
||||
padding: 18px 16px;
|
||||
}
|
||||
|
||||
.owner-modal-head h2 {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.owner-modal-close {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.owner-modal-preview-iframe {
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.owner-send-btn,
|
||||
.owner-cancel-btn {
|
||||
min-height: 46px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Activity tab ── */
|
||||
|
||||
Reference in New Issue
Block a user