Design language

This commit is contained in:
2026-05-13 09:39:52 +12:00
parent 6c943b14bd
commit de8b60b9c3
11 changed files with 329 additions and 236 deletions
+17 -43
View File
@@ -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 &amp; Greet. Start here and well 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">
+23 -3
View File
@@ -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
+26 -2
View File
@@ -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);
}); });
}); });
+3 -17
View File
@@ -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;
+68 -6
View File
@@ -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 {
+125 -11
View File
@@ -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>
+3 -3
View File
@@ -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 },
+9 -8
View File
@@ -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;
} }
+38 -36
View File
@@ -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
View File
@@ -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 {
+6
View File
@@ -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