Design language
This commit is contained in:
@@ -53,6 +53,23 @@
|
|||||||
<Icon name="fab fa-google" />
|
<Icon name="fab fa-google" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if footer.email || footer.phone}
|
||||||
|
<div class="footer-contact">
|
||||||
|
{#if footer.email}
|
||||||
|
<a href="mailto:{footer.email}" class="footer-contact-link">
|
||||||
|
<Icon name="fas fa-envelope" />
|
||||||
|
{footer.email}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{#if footer.phone}
|
||||||
|
<a href="tel:{footer.phone.replace(/[^0-9+]/g, '')}" class="footer-contact-link">
|
||||||
|
<Icon name="fas fa-phone" />
|
||||||
|
{footer.phone}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-explore">
|
<div class="footer-explore">
|
||||||
@@ -80,49 +97,6 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-action footer-panel footer-panel-accent">
|
|
||||||
<p class="footer-col-label">Get Started</p>
|
|
||||||
<h3 class="footer-action-title">Ready when you are</h3>
|
|
||||||
<p class="footer-action-copy">Questions, pricing, or your first Meet & Greet. Start here and we’ll reply within 24 hours.</p>
|
|
||||||
<a href="/contact-us" class="footer-book-btn">
|
|
||||||
Contact Us
|
|
||||||
<Icon name="fas fa-arrow-right" />
|
|
||||||
</a>
|
|
||||||
<p class="footer-book-note">Friendly, no-pressure first step</p>
|
|
||||||
<a
|
|
||||||
href="https://g.page/r/CUsvrWPhkYrAEB0/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
class="footer-reviews"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="footer-google-logo"
|
|
||||||
src="/images/google-g-logo.svg"
|
|
||||||
alt=""
|
|
||||||
width="16"
|
|
||||||
height="17"
|
|
||||||
/>
|
|
||||||
<span>30+ five-star Google reviews</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{#if footer.email || footer.phone}
|
|
||||||
<div class="footer-contact">
|
|
||||||
{#if footer.email}
|
|
||||||
<a href="mailto:{footer.email}" class="footer-contact-link">
|
|
||||||
<Icon name="fas fa-envelope" />
|
|
||||||
{footer.email}
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
{#if footer.phone}
|
|
||||||
<a href="tel:{footer.phone.replace(/[^0-9+]/g, '')}" class="footer-contact-link">
|
|
||||||
<Icon name="fas fa-phone" />
|
|
||||||
{footer.phone}
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
export let navigation: NavigationContent;
|
export let navigation: NavigationContent;
|
||||||
|
|
||||||
let mobileMenuOpen = false;
|
let mobileMenuOpen = false;
|
||||||
|
let headerElement: HTMLElement;
|
||||||
|
let mobileMenuTop = 0;
|
||||||
const mobilePhoneDisplay = '(022) 642 1011';
|
const mobilePhoneDisplay = '(022) 642 1011';
|
||||||
const mobilePhoneHref = '+64226421011';
|
const mobilePhoneHref = '+64226421011';
|
||||||
|
|
||||||
@@ -32,6 +34,11 @@
|
|||||||
mobileMenuOpen = !mobileMenuOpen;
|
mobileMenuOpen = !mobileMenuOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateMobileMenuTop() {
|
||||||
|
if (!headerElement) return;
|
||||||
|
mobileMenuTop = Math.max(headerElement.getBoundingClientRect().bottom, 0);
|
||||||
|
}
|
||||||
|
|
||||||
function mobileLinkIcon(href: string) {
|
function mobileLinkIcon(href: string) {
|
||||||
if (href === '/') return 'fas fa-house';
|
if (href === '/') return 'fas fa-house';
|
||||||
if (href === '/pack-walks') return 'fas fa-paw';
|
if (href === '/pack-walks') return 'fas fa-paw';
|
||||||
@@ -91,11 +98,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleViewportChange() {
|
function handleViewportChange() {
|
||||||
|
updateMobileMenuTop();
|
||||||
|
|
||||||
if (window.innerWidth > 768) {
|
if (window.innerWidth > 768) {
|
||||||
mobileMenuOpen = false;
|
mobileMenuOpen = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (mobileMenuOpen && typeof window !== 'undefined') {
|
||||||
|
updateMobileMenuTop();
|
||||||
|
}
|
||||||
|
|
||||||
$: if (typeof document !== 'undefined') {
|
$: if (typeof document !== 'undefined') {
|
||||||
document.body.classList.toggle('mobile-menu-open', mobileMenuOpen);
|
document.body.classList.toggle('mobile-menu-open', mobileMenuOpen);
|
||||||
}
|
}
|
||||||
@@ -103,17 +116,19 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
handleViewportChange();
|
handleViewportChange();
|
||||||
window.addEventListener('resize', handleViewportChange);
|
window.addEventListener('resize', handleViewportChange);
|
||||||
|
window.addEventListener('scroll', updateMobileMenuTop, { passive: true });
|
||||||
window.addEventListener('keydown', handleKeydown);
|
window.addEventListener('keydown', handleKeydown);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleViewportChange);
|
window.removeEventListener('resize', handleViewportChange);
|
||||||
|
window.removeEventListener('scroll', updateMobileMenuTop);
|
||||||
window.removeEventListener('keydown', handleKeydown);
|
window.removeEventListener('keydown', handleKeydown);
|
||||||
document.body.classList.remove('mobile-menu-open');
|
document.body.classList.remove('mobile-menu-open');
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header bind:this={headerElement}>
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="nav-links">
|
<ul class="nav-links">
|
||||||
{#each navigation.desktopLinks as link, i}
|
{#each navigation.desktopLinks as link, i}
|
||||||
@@ -231,8 +246,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class:open={mobileMenuOpen} class="mobile-menu-shell">
|
<div
|
||||||
<div class="mobile-menu" id="mobile-menu">
|
class:open={mobileMenuOpen}
|
||||||
|
class="mobile-menu-shell"
|
||||||
|
style={`--mobile-menu-top: ${mobileMenuTop}px;`}
|
||||||
|
on:click={closeMenu}
|
||||||
|
>
|
||||||
|
<div class="mobile-menu" id="mobile-menu" on:click|stopPropagation>
|
||||||
<div class="mobile-menu-links">
|
<div class="mobile-menu-links">
|
||||||
{#each navigation.mobileLinks as link}
|
{#each navigation.mobileLinks as link}
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -28,16 +28,40 @@ describe('Header', () => {
|
|||||||
|
|
||||||
const menuToggle = container.querySelector('.hamburger') as HTMLButtonElement;
|
const menuToggle = container.querySelector('.hamburger') as HTMLButtonElement;
|
||||||
const mobileMenu = container.querySelector('.mobile-menu') as HTMLDivElement;
|
const mobileMenu = container.querySelector('.mobile-menu') as HTMLDivElement;
|
||||||
|
const mobileMenuShell = container.querySelector('.mobile-menu-shell') as HTMLDivElement;
|
||||||
const firstMobileLink = mobileMenu.querySelector('a') as HTMLAnchorElement;
|
const firstMobileLink = mobileMenu.querySelector('a') as HTMLAnchorElement;
|
||||||
|
|
||||||
expect(menuToggle).toHaveAttribute('aria-expanded', 'false');
|
expect(menuToggle).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
await fireEvent.click(menuToggle);
|
await fireEvent.click(menuToggle);
|
||||||
expect(menuToggle).toHaveAttribute('aria-expanded', 'true');
|
expect(menuToggle).toHaveAttribute('aria-expanded', 'true');
|
||||||
expect(mobileMenu.classList.contains('open')).toBe(true);
|
expect(mobileMenuShell.classList.contains('open')).toBe(true);
|
||||||
|
|
||||||
await fireEvent.click(firstMobileLink);
|
await fireEvent.click(firstMobileLink);
|
||||||
expect(menuToggle).toHaveAttribute('aria-expanded', 'false');
|
expect(menuToggle).toHaveAttribute('aria-expanded', 'false');
|
||||||
expect(mobileMenu.classList.contains('open')).toBe(false);
|
expect(mobileMenuShell.classList.contains('open')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the mobile menu when tapping outside the menu panel', async () => {
|
||||||
|
Object.defineProperty(window, 'innerWidth', {
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
value: 390
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = render(Header, {
|
||||||
|
navigation: homepageContent.navigation
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuToggle = container.querySelector('.hamburger') as HTMLButtonElement;
|
||||||
|
const mobileMenuShell = container.querySelector('.mobile-menu-shell') as HTMLDivElement;
|
||||||
|
|
||||||
|
await fireEvent.click(menuToggle);
|
||||||
|
expect(menuToggle).toHaveAttribute('aria-expanded', 'true');
|
||||||
|
expect(mobileMenuShell.classList.contains('open')).toBe(true);
|
||||||
|
|
||||||
|
await fireEvent.click(mobileMenuShell);
|
||||||
|
expect(menuToggle).toHaveAttribute('aria-expanded', 'false');
|
||||||
|
expect(mobileMenuShell.classList.contains('open')).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -152,8 +152,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="service-benefit-mobile-hint">Swipe through the reasons Tiny Gang works so well.</p>
|
|
||||||
|
|
||||||
<div class="service-benefit-shell">
|
<div class="service-benefit-shell">
|
||||||
<div bind:this={benefitScroller} class="service-benefit-grid">
|
<div bind:this={benefitScroller} class="service-benefit-grid">
|
||||||
{#each benefitCards as benefit}
|
{#each benefitCards as benefit}
|
||||||
@@ -566,13 +564,9 @@
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-benefit-mobile-hint {
|
.service-benefit-mobile-controls {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-benefit-mobile-controls {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-section-heading {
|
.service-section-heading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -1133,14 +1127,6 @@
|
|||||||
line-height: 1.68;
|
line-height: 1.68;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-benefit-mobile-hint {
|
|
||||||
display: block;
|
|
||||||
margin: -10px 0 16px;
|
|
||||||
color: var(--gray);
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-benefit-mobile-controls {
|
.service-benefit-mobile-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount, tick } from 'svelte';
|
||||||
import { reveal } from '$lib/actions/reveal';
|
import { reveal } from '$lib/actions/reveal';
|
||||||
import Icon from '$lib/components/Icon.svelte';
|
import Icon from '$lib/components/Icon.svelte';
|
||||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
let inView = false;
|
let inView = false;
|
||||||
let prefersReducedMotion = false;
|
let prefersReducedMotion = false;
|
||||||
let carouselEl: HTMLDivElement | undefined;
|
let carouselEl: HTMLDivElement | undefined;
|
||||||
|
let stageEl: HTMLDivElement | undefined;
|
||||||
let slideSignature = '';
|
let slideSignature = '';
|
||||||
|
|
||||||
$: slides = testimonials
|
$: slides = testimonials
|
||||||
@@ -89,6 +90,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeIndex = (activeIndex - 1 + slides.length) % slides.length;
|
activeIndex = (activeIndex - 1 + slides.length) % slides.length;
|
||||||
|
syncMobileStage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNext() {
|
function showNext() {
|
||||||
@@ -97,6 +99,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
activeIndex = (activeIndex + 1) % slides.length;
|
activeIndex = (activeIndex + 1) % slides.length;
|
||||||
|
syncMobileStage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMobileViewport() {
|
||||||
|
return typeof window !== 'undefined' && window.innerWidth <= 767;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncMobileStage(behavior: ScrollBehavior = 'smooth') {
|
||||||
|
if (!stageEl || !isMobileViewport()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
stageEl.scrollTo({
|
||||||
|
left: stageEl.clientWidth * activeIndex,
|
||||||
|
behavior
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStageScroll() {
|
||||||
|
if (!stageEl || !isMobileViewport()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIndex = Math.round(stageEl.scrollLeft / Math.max(stageEl.clientWidth, 1));
|
||||||
|
if (nextIndex !== activeIndex) {
|
||||||
|
activeIndex = nextIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -120,6 +150,13 @@
|
|||||||
observer.observe(carouselEl);
|
observer.observe(carouselEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
syncMobileStage('auto');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
syncMobileStage('auto');
|
||||||
|
|
||||||
const interval = window.setInterval(() => {
|
const interval = window.setInterval(() => {
|
||||||
if (!paused && !prefersReducedMotion && inView && slides.length > 1) {
|
if (!paused && !prefersReducedMotion && inView && slides.length > 1) {
|
||||||
showNext();
|
showNext();
|
||||||
@@ -128,6 +165,7 @@
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(interval);
|
window.clearInterval(interval);
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
motionQuery.removeEventListener('change', onMotionChange);
|
motionQuery.removeEventListener('change', onMotionChange);
|
||||||
observer?.disconnect();
|
observer?.disconnect();
|
||||||
};
|
};
|
||||||
@@ -162,7 +200,7 @@
|
|||||||
<Icon name="fas fa-chevron-left" />
|
<Icon name="fas fa-chevron-left" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="testimonial-stage">
|
<div bind:this={stageEl} class="testimonial-stage" on:scroll={handleStageScroll}>
|
||||||
<div class="testimonial-woof" aria-hidden="true">
|
<div class="testimonial-woof" aria-hidden="true">
|
||||||
<span class="testimonial-woof-text">WOOF</span>
|
<span class="testimonial-woof-text">WOOF</span>
|
||||||
<span class="testimonial-ray testimonial-ray-1"></span>
|
<span class="testimonial-ray testimonial-ray-1"></span>
|
||||||
@@ -640,14 +678,28 @@
|
|||||||
|
|
||||||
.testimonial-stage {
|
.testimonial-stage {
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
|
display: flex;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
scroll-snap-type: x proximity;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-stage::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.testimonial-slide {
|
.testimonial-slide {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: none;
|
display: grid;
|
||||||
|
flex: 0 0 100%;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
transform: none;
|
transform: none;
|
||||||
|
scroll-snap-align: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.testimonial-slide-active {
|
.testimonial-slide-active {
|
||||||
@@ -689,17 +741,27 @@
|
|||||||
.testimonial-mobile-controls {
|
.testimonial-mobile-controls {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.testimonial-arrow-inline {
|
.testimonial-arrow-inline {
|
||||||
position: static;
|
position: static;
|
||||||
width: 48px;
|
width: 46px;
|
||||||
height: 48px;
|
height: 46px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(33, 48, 33, 0.08);
|
||||||
|
color: var(--gw-green);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
transform: none;
|
transform: none;
|
||||||
box-shadow: 0 10px 22px rgba(20, 24, 20, 0.08);
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-arrow-inline:active {
|
||||||
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.testimonial-google {
|
.testimonial-google {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
import Icon from '$lib/components/Icon.svelte';
|
import Icon from '$lib/components/Icon.svelte';
|
||||||
import type { IconCard } from '$lib/types';
|
import type { IconCard } from '$lib/types';
|
||||||
|
|
||||||
export let values: IconCard[];
|
export let values: IconCard[];
|
||||||
|
let valuesScroller: HTMLDivElement | undefined;
|
||||||
|
|
||||||
$: orderedValues = values
|
$: orderedValues = values
|
||||||
.map((value, index) => ({ value, index }))
|
.map((value, index) => ({ value, index }))
|
||||||
@@ -17,6 +19,36 @@
|
|||||||
return a.index - b.index;
|
return a.index - b.index;
|
||||||
})
|
})
|
||||||
.map(({ value }) => value);
|
.map(({ value }) => value);
|
||||||
|
|
||||||
|
function isMobileViewport() {
|
||||||
|
return typeof window !== 'undefined' && window.innerWidth <= 768;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scrollValues(direction: 1 | -1) {
|
||||||
|
if (!valuesScroller || !isMobileViewport()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstCard = valuesScroller.querySelector<HTMLElement>('.value-card');
|
||||||
|
if (!firstCard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardStyle = window.getComputedStyle(valuesScroller);
|
||||||
|
const gap = Number.parseFloat(cardStyle.columnGap || cardStyle.gap || '0') || 0;
|
||||||
|
const step = firstCard.offsetWidth + gap;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
valuesScroller.scrollBy({ left: direction * step, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!valuesScroller || !isMobileViewport()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
valuesScroller.scrollTo({ left: 0, behavior: 'auto' });
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section id="values">
|
<section id="values">
|
||||||
@@ -27,18 +59,29 @@
|
|||||||
Everything is designed to make life easier for busy Auckland dog owners and safer, happier for the dogs in our care.
|
Everything is designed to make life easier for busy Auckland dog owners and safer, happier for the dogs in our care.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="values-grid">
|
<div class="values-shell">
|
||||||
{#each orderedValues as value}
|
<div bind:this={valuesScroller} class="values-grid">
|
||||||
<div class="value-card">
|
{#each orderedValues as value}
|
||||||
<div class="value-icon-wrap">
|
<div class="value-card">
|
||||||
<Icon name={value.icon} className="value-card-icon" />
|
<div class="value-icon-wrap">
|
||||||
|
<Icon name={value.icon} className="value-card-icon" />
|
||||||
|
</div>
|
||||||
|
<div class="value-text">
|
||||||
|
<h3>{value.title}</h3>
|
||||||
|
<p>{value.body}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="value-text">
|
{/each}
|
||||||
<h3>{value.title}</h3>
|
</div>
|
||||||
<p>{value.body}</p>
|
|
||||||
</div>
|
<div class="values-mobile-controls" aria-label="Value cards navigation">
|
||||||
</div>
|
<button type="button" class="values-mobile-button" aria-label="Previous value" on:click={() => scrollValues(-1)}>
|
||||||
{/each}
|
<Icon name="fas fa-chevron-left" />
|
||||||
|
</button>
|
||||||
|
<button type="button" class="values-mobile-button" aria-label="Next value" on:click={() => scrollValues(1)}>
|
||||||
|
<Icon name="fas fa-chevron-right" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -68,7 +111,68 @@
|
|||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.values-mobile-controls {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.values-shell {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.values-grid {
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: minmax(272px, 84vw);
|
||||||
|
grid-template-columns: none;
|
||||||
|
gap: 14px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
scroll-snap-type: x proximity;
|
||||||
|
scroll-padding-left: 24px;
|
||||||
|
padding: 0 24px 8px 0;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.values-grid::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.values-mobile-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.values-mobile-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-card {
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 24px 22px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 24px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-card:nth-child(odd) {
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.values-eyebrow {
|
.values-eyebrow {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
@@ -81,4 +185,14 @@
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.values-mobile-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.values-mobile-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const homepageContent: HomePageContent = {
|
|||||||
desktopLinks: [
|
desktopLinks: [
|
||||||
{ label: 'Our Services', href: '#services' },
|
{ label: 'Our Services', href: '#services' },
|
||||||
{ label: 'Our Pricing', href: '/our-pricing' },
|
{ label: 'Our Pricing', href: '/our-pricing' },
|
||||||
{ label: 'About', href: '/about' }
|
{ label: 'About Us', href: '/about' }
|
||||||
],
|
],
|
||||||
mobileLinks: [
|
mobileLinks: [
|
||||||
{ label: 'Home', href: '/' },
|
{ label: 'Home', href: '/' },
|
||||||
@@ -18,8 +18,8 @@ export const homepageContent: HomePageContent = {
|
|||||||
{ label: '1:1 Walks', href: '/dog-walking' },
|
{ label: '1:1 Walks', href: '/dog-walking' },
|
||||||
{ label: 'Puppy Visits', href: '/puppy-visits' },
|
{ label: 'Puppy Visits', href: '/puppy-visits' },
|
||||||
{ label: 'Our Pricing', href: '/our-pricing' },
|
{ label: 'Our Pricing', href: '/our-pricing' },
|
||||||
{ label: 'About', href: '/about' },
|
{ label: 'About Us', href: '/about' },
|
||||||
{ label: 'Contact', href: '/contact-us' }
|
{ label: 'Contact Us', href: '/contact-us' }
|
||||||
],
|
],
|
||||||
cta: { label: 'Contact Us', href: '/contact-us', variant: 'yellow' },
|
cta: { label: 'Contact Us', href: '/contact-us', variant: 'yellow' },
|
||||||
instagram: { href: 'https://www.instagram.com/goodwalk.nz/', external: true },
|
instagram: { href: 'https://www.instagram.com/goodwalk.nz/', external: true },
|
||||||
|
|||||||
@@ -46,20 +46,22 @@ header {
|
|||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.nav-ribbon {
|
.nav-ribbon {
|
||||||
padding: 10px 16px;
|
padding: 14px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-ribbon-item {
|
.nav-ribbon-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
gap: 6px;
|
gap: 7px;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
letter-spacing: 0.04em;
|
line-height: 1.25;
|
||||||
|
letter-spacing: 0.045em;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-ribbon-item .icon {
|
.nav-ribbon-item .icon {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide the third ribbon item and its preceding divider on mobile */
|
/* Hide the third ribbon item and its preceding divider on mobile */
|
||||||
@@ -69,8 +71,7 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-ribbon-divider {
|
.nav-ribbon-divider {
|
||||||
height: 12px;
|
display: none;
|
||||||
flex: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,8 +438,8 @@ nav {
|
|||||||
|
|
||||||
.footer-inner {
|
.footer-inner {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1.1fr 0.8fr 0.8fr 1fr;
|
grid-template-columns: 1.1fr 0.8fr 1fr;
|
||||||
gap: 24px;
|
gap: 32px;
|
||||||
margin-bottom: 48px;
|
margin-bottom: 48px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,16 +135,18 @@
|
|||||||
|
|
||||||
.mobile-menu {
|
.mobile-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
padding: 10px;
|
padding: 10px 12px 14px;
|
||||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.985), rgba(248, 248, 245, 0.98));
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.985), rgba(248, 248, 245, 0.98));
|
||||||
border: 1px solid rgba(33, 48, 33, 0.08);
|
border: 1px solid rgba(33, 48, 33, 0.08);
|
||||||
border-radius: 22px;
|
border-top: 0;
|
||||||
|
border-radius: 0 0 24px 24px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 16px 32px rgba(17, 20, 24, 0.12),
|
0 18px 32px rgba(17, 20, 24, 0.12),
|
||||||
0 2px 10px rgba(17, 20, 24, 0.05);
|
0 6px 14px rgba(17, 20, 24, 0.05);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-10px) scale(0.992);
|
transform: translateY(-10px) scale(0.992);
|
||||||
transition:
|
transition:
|
||||||
@@ -153,13 +155,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-menu-shell {
|
.mobile-menu-shell {
|
||||||
display: block;
|
display: flex;
|
||||||
position: absolute;
|
align-items: flex-start;
|
||||||
top: calc(100% + 8px);
|
position: fixed;
|
||||||
left: 0;
|
inset: var(--mobile-menu-top, 0px) 0 0;
|
||||||
right: 0;
|
|
||||||
z-index: 120;
|
z-index: 120;
|
||||||
padding: 0 16px;
|
padding: 0 0 max(20px, env(safe-area-inset-bottom));
|
||||||
|
background: rgba(17, 20, 24, 0.04);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
@@ -306,7 +308,7 @@
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: center 48%;
|
object-position: 43% center;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,41 +708,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
padding: 48px 24px 28px;
|
padding: 40px 24px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-inner {
|
.footer-inner {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 16px;
|
gap: 22px;
|
||||||
}
|
|
||||||
|
|
||||||
.footer-action {
|
|
||||||
order: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-panel {
|
|
||||||
padding: 22px 18px;
|
|
||||||
border-radius: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-action-title {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-action-copy {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-book-note {
|
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-nav a {
|
.footer-nav a {
|
||||||
padding: 11px 0;
|
padding: 9px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-brand p {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-col-label {
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-contact {
|
.footer-contact {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-bottom {
|
.footer-bottom {
|
||||||
@@ -756,7 +752,13 @@
|
|||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.footer-nav {
|
.footer-nav {
|
||||||
gap: 2px 16px;
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-locations .footer-nav {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-phone {
|
.mobile-phone {
|
||||||
|
|||||||
+11
-107
@@ -737,33 +737,13 @@ footer {
|
|||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-panel {
|
|
||||||
min-height: 100%;
|
|
||||||
padding: 28px 26px;
|
|
||||||
border-radius: 28px;
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
|
|
||||||
0 18px 34px rgba(0, 0, 0, 0.08);
|
|
||||||
backdrop-filter: blur(6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-panel-accent {
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top right, rgba(255, 209, 71, 0.22), transparent 38%),
|
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.11) 0%, rgba(255, 255, 255, 0.06) 100%);
|
|
||||||
box-shadow:
|
|
||||||
inset 0 0 0 1px rgba(255, 255, 255, 0.1),
|
|
||||||
0 20px 40px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-brand p {
|
.footer-brand p {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
opacity: 0.76;
|
opacity: 0.72;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 18px;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
max-width: 34ch;
|
max-width: 30ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.social-links a {
|
.social-links a {
|
||||||
@@ -789,13 +769,13 @@ footer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-col-label {
|
.footer-col-label {
|
||||||
margin: 0 0 16px;
|
margin: 0 0 14px;
|
||||||
font-family: var(--font-head);
|
font-family: var(--font-head);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.12em;
|
letter-spacing: 0.12em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
opacity: 0.45;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-nav {
|
.footer-nav {
|
||||||
@@ -816,10 +796,10 @@ footer {
|
|||||||
|
|
||||||
.footer-nav a {
|
.footer-nav a {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 9px 0;
|
padding: 8px 0;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
opacity: 0.75;
|
opacity: 0.72;
|
||||||
transition: opacity 0.18s;
|
transition: opacity 0.18s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,89 +807,13 @@ footer {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-action-title {
|
|
||||||
margin: 0 0 10px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 28px;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: -0.04em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-action-copy {
|
|
||||||
margin: 0 0 22px;
|
|
||||||
max-width: 34ch;
|
|
||||||
color: rgba(255, 255, 255, 0.78);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-book-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 16px 22px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--yellow);
|
|
||||||
color: #000;
|
|
||||||
font-family: var(--font-head);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1.2;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
transition: background 0.2s, transform 0.15s;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-book-btn:hover {
|
|
||||||
background: #ffe033;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-book-note {
|
|
||||||
margin: 0 0 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.68;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-reviews {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 9px 16px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, 0.07);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
font-family: var(--font-head);
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: background 0.2s, opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-google-logo {
|
|
||||||
width: 16px;
|
|
||||||
height: 17px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-reviews:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.13);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-contact {
|
.footer-contact {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 16px;
|
margin-top: 18px;
|
||||||
padding-top: 16px;
|
padding-top: 18px;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
max-width: 24rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-contact-link {
|
.footer-contact-link {
|
||||||
|
|||||||
@@ -37,6 +37,10 @@
|
|||||||
|
|
||||||
let revealObserver: IntersectionObserver | null = null;
|
let revealObserver: IntersectionObserver | null = null;
|
||||||
|
|
||||||
|
function shouldAnimateAnchorBounce() {
|
||||||
|
return typeof window !== 'undefined' && window.innerWidth > 768;
|
||||||
|
}
|
||||||
|
|
||||||
function initReveal() {
|
function initReveal() {
|
||||||
revealObserver?.disconnect();
|
revealObserver?.disconnect();
|
||||||
|
|
||||||
@@ -64,6 +68,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bounceSection(hash: string) {
|
function bounceSection(hash: string) {
|
||||||
|
if (!shouldAnimateAnchorBounce()) return;
|
||||||
|
|
||||||
const id = hash.startsWith('#') ? hash.slice(1) : hash;
|
const id = hash.startsWith('#') ? hash.slice(1) : hash;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
// Wait for smooth scroll to finish before playing the bounce
|
// Wait for smooth scroll to finish before playing the bounce
|
||||||
|
|||||||
Reference in New Issue
Block a user