Design Language tweaks

This commit is contained in:
2026-05-15 01:28:10 +12:00
parent baafafabdb
commit 580d600c47
52 changed files with 3465 additions and 1548 deletions
+496 -110
View File
@@ -4,69 +4,524 @@
import type { IconCard } from '$lib/types';
export let services: IconCard[];
export let heading = 'What we do';
export let heading = 'Choose the walk style that suits your dog best.';
export let intro =
'Choose the walk style that fits your dog best, then book a free Meet & Greet when you are ready.';
'Dogs are social creatures. The Tiny Gang gives them their own little friendship group — older dogs guide the younger ones, playful dogs burn energy together, and everyone comes home happy, tired, and fulfilled. All the fun of doggy daycare, without the huge groups or price tag.';
const requestedServiceStorageKey = 'goodwalk_requested_service';
const sharedPromises = [
'Familiar walkers',
'Small-scale care',
'Reliable pickup & drop-off',
'Updates you will actually want'
];
function bookingHref() {
return '#newlead';
}
function primeBookingService(serviceTitle: string) {
try {
window.sessionStorage.setItem(requestedServiceStorageKey, serviceTitle);
} catch {
// Ignore storage failures and continue with the link target.
// Lightweight presentation metadata — the card only needs to say *what*
// each service is before the visitor opens the full service page.
const serviceMeta: Record<
string,
{
eyebrow: string;
featured?: boolean;
featuredLabel?: string;
imageUrl: string;
imageAlt: string;
lead: string;
cues: string[];
}
> = {
'Tiny Gang Pack Walks': {
eyebrow: 'Good Walk Signature',
featured: true,
featuredLabel: 'Most loved',
imageUrl: '/images/auckland-pack-walk-small-dogs-group.jpg',
imageAlt: 'Small dogs together on a Tiny Gang pack walk',
lead: 'The Tiny Gang is built for dogs who love company, big adventures, and coming home happily worn out!',
cues: ['4-8 dogs', 'Pickup & drop-off', 'Tiny Gang matching']
},
'1:1 Walks': {
eyebrow: 'Tailored support',
imageUrl: '/images/one-on-one-dog-portrait-1.jpg',
imageAlt: 'Dog enjoying a one-on-one walk',
lead: 'For nervous dogs, senior dogs, and little personalities who do better with extra attention.',
cues: ['Solo focus', 'Custom pace', 'Confidence building']
},
'Puppy Visits': {
eyebrow: 'Building Blocks For The Tiny Gang',
imageUrl: '/images/auckland-puppy-home-visit.jpg',
imageAlt: 'Puppy during a calm home visit',
lead: 'Early puppy visits designed to build confidence, routine, and good habits before Tiny Gang adventures begin!',
cues: ['Home visits', 'Routine support', 'Play & company']
}
};
window.dispatchEvent(
new CustomEvent('goodwalk:service-selected', {
detail: { service: serviceTitle }
})
);
}
$: orderedServices = services
.map((service, index) => ({ service, index }))
.sort((a, b) => {
const aFeatured = serviceMeta[a.service.title]?.featured ? 0 : 1;
const bFeatured = serviceMeta[b.service.title]?.featured ? 0 : 1;
if (aFeatured !== bFeatured) {
return aFeatured - bFeatured;
}
return a.index - b.index;
})
.map(({ service }) => service);
</script>
<section id="services" use:reveal={{ delay: 20 }} class="reveal-block">
<div class="services-inner">
<h2 class="section-heading">{heading}</h2>
<p class="services-intro">{intro}</p>
<div class="section-header">
<h2 class="section-heading">{heading}</h2>
<p class="section-intro services-intro">{intro}</p>
</div>
<div class="services-grid">
{#each services as service}
<div class="service-card">
<div class="service-icon-bubble">
<Icon name={service.icon} className="service-card-icon" />
{#each orderedServices as service}
{@const meta = serviceMeta[service.title]}
<a
href={service.href}
class:service-card-featured={meta?.featured}
class="service-card"
aria-label={`${service.title} — view service page`}
>
<div class="service-card-media">
{#if meta}
<img src={meta.imageUrl} alt={meta.imageAlt} loading="lazy" decoding="async" />
{/if}
{#if meta?.featuredLabel}
<span class="service-card-badge">{meta.featuredLabel}</span>
{/if}
</div>
<h3>{service.title}</h3>
<p>{service.body}</p>
{#if service.priceFrom}
<p class="service-card-price">{service.priceFrom}</p>
{/if}
<div class="service-card-body">
<span class="service-card-emblem">
<Icon name={service.icon} className="service-card-emblem-glyph" />
</span>
{#if service.href}
<div class="service-card-actions">
<a href={bookingHref()} class="btn btn-green" on:click={() => primeBookingService(service.title)}>
<span>Book {service.title}</span>
<Icon name="fas fa-arrow-right" />
</a>
{#if meta?.eyebrow}
<span class="service-card-eyebrow">{meta.eyebrow}</span>
{/if}
<h3>{service.title}</h3>
<p>{meta?.lead ?? service.body}</p>
<a href={service.href} class="service-card-link">
View details &amp; pricing
</a>
</div>
{/if}
</div>
{#if meta?.cues?.length}
<div class="service-card-cues">
{#each meta.cues as cue}
<span class="service-card-cue">{cue}</span>
{/each}
</div>
{/if}
<span class="service-card-cta">
View service page
<Icon name="fas fa-arrow-right" className="service-card-cta-arrow" />
</span>
</div>
</a>
{/each}
</div>
</div>
</section>
<style>
/* ── Section intro ── */
.services-intro {
max-width: 700px;
}
/* ── Overview band ── */
.services-overview {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(300px, 0.8fr);
gap: 18px;
align-items: stretch;
margin-top: 28px;
}
.services-overview-copy,
.services-overview-panel {
border-radius: 28px;
background: linear-gradient(180deg, #fff 0%, #fbf8f2 100%);
box-shadow:
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
0 12px 28px rgba(17, 20, 24, 0.05);
}
.services-overview-copy {
padding: 28px 30px;
}
.services-overview-kicker,
.services-overview-panel-label {
display: inline-flex;
align-items: center;
min-height: 30px;
width: fit-content;
padding: 0 11px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.08);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.services-overview-copy h3 {
margin: 14px 0 10px;
color: #0d1a0d;
font-size: clamp(26px, 2.6vw, 34px);
line-height: 1.02;
letter-spacing: -0.04em;
}
.services-overview-copy p,
.services-overview-panel {
color: #4c5056;
font-size: 15px;
line-height: 1.65;
}
.services-overview-copy p {
max-width: 44ch;
margin: 0;
}
.services-overview-panel {
display: grid;
align-content: center;
gap: 16px;
padding: 24px 24px 22px;
background:
radial-gradient(circle at top right, rgba(255, 209, 0, 0.22), transparent 36%),
linear-gradient(180deg, #fff 0%, #f8f3e7 100%);
}
.services-overview-pills {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.services-overview-pill {
display: inline-flex;
align-items: center;
min-height: 36px;
padding: 0 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
box-shadow: inset 0 0 0 1px rgba(33, 48, 33, 0.08);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 12px;
font-weight: 700;
line-height: 1.2;
}
/* ── Service cards ── */
.services-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 22px;
margin-top: 30px;
}
.service-card {
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
text-align: left;
border-radius: 24px;
background: #fff;
border: 1px solid rgba(17, 20, 24, 0.07);
box-shadow: 0 6px 20px rgba(17, 20, 24, 0.05);
text-decoration: none;
color: inherit;
transition:
box-shadow 0.28s ease,
transform 0.24s cubic-bezier(0.22, 1, 0.36, 1),
border-color 0.28s ease;
}
.service-card-featured {
border-color: rgba(242, 191, 47, 0.45);
box-shadow:
inset 0 0 0 1px rgba(255, 209, 0, 0.22),
0 10px 26px rgba(17, 20, 24, 0.07);
}
@media (hover: hover) {
.service-card:hover {
transform: translateY(-6px);
border-color: rgba(33, 48, 33, 0.16);
box-shadow: 0 22px 46px rgba(17, 20, 24, 0.13);
filter: none;
}
.service-card:hover .service-card-media img {
transform: scale(1.06);
}
.service-card:hover .service-card-cta-arrow {
transform: translateX(4px);
}
.service-card:hover .service-card-emblem::after,
.service-card:focus-visible .service-card-emblem::after,
.service-card:active .service-card-emblem::after {
animation: serviceEmblemShine 0.9s cubic-bezier(0.22, 1, 0.36, 1) 1;
}
}
.service-card:active {
transform: translateY(-3px);
}
.service-card-media {
position: relative;
aspect-ratio: 4 / 3;
overflow: hidden;
background: #ede4d2;
}
.service-card-media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.service-card-badge {
position: absolute;
top: 14px;
right: 14px;
padding: 6px 11px;
border-radius: 999px;
background: var(--gw-green);
color: #fff6cf;
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.service-card-body {
position: relative;
display: flex;
flex: 1;
flex-direction: column;
padding: 42px 26px 26px;
}
/* Brand emblem straddles the photo / body seam */
.service-card-emblem {
position: absolute;
top: 0;
left: 24px;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 16px;
background: var(--gw-green);
box-shadow: 0 10px 22px rgba(33, 48, 33, 0.26);
overflow: hidden;
}
.service-card-emblem :global(.service-card-emblem-glyph) {
font-size: 22px;
color: var(--yellow);
}
.service-card-emblem::after {
content: '';
position: absolute;
top: -20%;
left: -85%;
width: 60%;
height: 140%;
background: linear-gradient(
120deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.18) 35%,
rgba(255, 255, 255, 0.65) 50%,
rgba(255, 255, 255, 0.18) 65%,
rgba(255, 255, 255, 0) 100%
);
transform: rotate(14deg);
pointer-events: none;
opacity: 0;
}
.service-card-eyebrow {
margin-bottom: 8px;
color: var(--gw-green);
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.service-card-body h3 {
margin: 0 0 8px;
font-family: var(--font-head);
font-size: 21px;
font-weight: 700;
line-height: 1.2;
letter-spacing: -0.02em;
color: #0d1a0d;
}
.service-card-body p {
margin: 0;
color: #4c5056;
font-size: 15px;
line-height: 1.6;
}
.service-card-cues {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin-top: 16px;
}
.service-card-cue {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 4px 11px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.06);
box-shadow: inset 0 0 0 1px rgba(33, 48, 33, 0.07);
color: var(--gw-green);
font-size: 11px;
font-weight: 700;
line-height: 1.2;
}
.service-card-cta {
display: flex;
align-items: center;
gap: 8px;
margin-top: auto;
padding-top: 20px;
border-top: 1px solid rgba(17, 20, 24, 0.08);
color: var(--gw-green);
font-family: var(--font-head);
font-size: 13px;
font-weight: 700;
}
.service-card-cta-arrow {
font-size: 11px;
transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
/* ── Mobile ── */
@media (max-width: 768px) {
.services-intro {
max-width: 34ch;
}
.services-overview {
grid-template-columns: 1fr;
gap: 12px;
margin-top: 22px;
}
.services-overview-copy,
.services-overview-panel {
border-radius: 24px;
}
.services-overview-copy {
padding: 22px 18px;
}
.services-overview-copy h3 {
font-size: clamp(24px, 8vw, 31px);
line-height: 1.06;
}
.services-overview-copy p,
.services-overview-panel {
font-size: 14px;
line-height: 1.55;
}
.services-overview-panel {
padding: 18px;
gap: 12px;
}
.services-overview-pills {
gap: 8px;
}
.services-overview-pill {
min-height: 32px;
padding: 0 12px;
font-size: 11px;
}
.services-grid {
grid-template-columns: 1fr;
gap: 16px;
margin-top: 24px;
}
.service-card-body {
padding: 40px 22px 24px;
}
.service-card-emblem {
width: 48px;
height: 48px;
border-radius: 15px;
}
.service-card-emblem :global(.service-card-emblem-glyph) {
font-size: 20px;
}
.service-card-body h3 {
font-size: 20px;
}
}
@keyframes serviceEmblemShine {
0% {
left: -85%;
opacity: 0;
}
18% {
opacity: 1;
}
82% {
left: 130%;
opacity: 0;
}
100% {
left: 130%;
opacity: 0;
}
}
/* ── Reveal ── */
:global(.reveal-ready.reveal-block) {
opacity: 0;
transform: translate3d(0, var(--reveal-distance, 24px), 0);
@@ -80,73 +535,4 @@
opacity: 1;
transform: translate3d(0, 0, 0);
}
@media (hover: hover) and (min-width: 769px) {
:global(.reveal-visible.reveal-block) .service-card {
animation: service-card-settle 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
}
:global(.reveal-visible.reveal-block) .service-card:nth-child(1) {
animation-delay: 0.02s;
}
:global(.reveal-visible.reveal-block) .service-card:nth-child(2) {
animation-delay: 0.06s;
}
:global(.reveal-visible.reveal-block) .service-card:nth-child(3) {
animation-delay: 0.1s;
}
}
@keyframes service-card-settle {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.services-intro {
max-width: 700px;
margin: 18px auto 0;
text-align: center;
color: #4c5056;
font-size: 17px;
line-height: 1.65;
}
.service-card-actions {
display: grid;
gap: 12px;
margin-top: 18px;
}
.service-card-actions :global(.btn) {
width: 100%;
justify-content: center;
}
.service-card-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 28px;
color: var(--gw-green);
font-size: 14px;
font-weight: 700;
text-decoration: none;
}
@media (hover: hover) {
.service-card-link:hover {
text-decoration: underline;
text-underline-offset: 0.18em;
}
}
</style>