2026-05-02 08:26:18 +12:00
< script lang = "ts" >
2026-05-02 11:24:11 +12:00
import { reveal } from '$lib/actions/reveal' ;
2026-05-02 08:26:18 +12:00
import Icon from '$lib/components/Icon.svelte' ;
import type { IconCard } from '$lib/types' ;
export let services : IconCard [];
2026-05-19 23:36:58 +12:00
export let heading = 'Find the walk that fits your dog.' ;
2026-05-07 21:47:42 +12:00
export let intro =
2026-05-19 23:36:58 +12:00
"The Tiny Gang is your dog's friendship group. Older dogs guide the youngsters; playful pairs burn energy together. The fun of doggy daycare, without the crowd or the price tag." ;
2026-05-07 21:47:42 +12:00
2026-05-15 01:28:10 +12:00
const sharedPromises = [
'Familiar walkers' ,
'Small-scale care' ,
'Reliable pickup & drop-off' ,
'Updates you will actually want'
];
2026-05-07 21:47:42 +12:00
2026-05-15 01:28:10 +12:00
// 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 ;
imageUrl : string ;
imageAlt : string ;
lead : string ;
cues : string [];
}
> = {
'Tiny Gang Pack Walks' : {
eyebrow : 'Good Walk Signature' ,
featured : true ,
2026-05-18 09:43:29 +12:00
imageUrl : '/images/goodwalk-tiny-gang-pack-walk-small-dogs-auckland.webp' ,
2026-05-15 01:28:10 +12:00
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' ]
},
2026-05-18 22:25:43 +12:00
'Solo Walks' : {
2026-05-15 01:28:10 +12:00
eyebrow : 'Tailored support' ,
2026-05-18 09:43:29 +12:00
imageUrl : '/images/goodwalk-brown-curly-dog-one-on-one-walk-auckland.webp' ,
2026-05-15 01:28:10 +12:00
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' ,
2026-05-18 09:43:29 +12:00
imageUrl : '/images/goodwalk-puppy-visit-cavalier-king-charles-spaniel-auckland.webp' ,
2026-05-15 01:28:10 +12:00
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' ]
2026-05-07 21:47:42 +12:00
}
2026-05-15 01:28:10 +12:00
};
2026-05-07 21:47:42 +12:00
2026-05-15 01:28:10 +12:00
$ : 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 ;
2026-05-02 08:26:18 +12:00
2026-05-15 01:28:10 +12:00
if ( aFeatured !== bFeatured ) {
return aFeatured - bFeatured ;
}
return a . index - b . index ;
})
. map (({ service }) => service );
2026-05-02 08:26:18 +12:00
</ script >
2026-05-26 08:30:08 +12:00
< section id = "services" use:reveal = {{ delay : 20 , distance : 0 }} class="reveal-block" >
2026-05-02 08:26:18 +12:00
< div class = "services-inner" >
2026-05-26 08:30:08 +12:00
< div class = "section-header fade-up" >
2026-05-15 01:28:10 +12:00
< h2 class = "section-heading" > { heading } </ h2 >
< p class = "section-intro services-intro" > { intro } </ p >
</ div >
2026-05-02 08:26:18 +12:00
2026-05-26 08:30:08 +12:00
< div class = "services-grid stagger-children" >
2026-05-15 01:28:10 +12:00
{ #each orderedServices as service }
{ @const meta = serviceMeta [ service . title ]}
< a
href = { service . href }
class:service-card-featured= { meta ? . featured }
2026-05-26 08:30:08 +12:00
class = "service-card fade-up"
2026-05-15 01:28:10 +12:00
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 }
</ div >
< div class = "service-card-body" >
< span class = "service-card-emblem" >
< Icon name = { service . icon } className="service-card-emblem-glyph" />
</ span >
{ #if meta ? . eyebrow }
< span class = "service-card-eyebrow" > { meta . eyebrow } </ span >
{ /if }
< h3 > { service . title } </ h3 >
< p > { meta ? . lead ?? service . body } </ p >
{ #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" >
2026-05-18 09:43:29 +12:00
More info
2026-05-15 01:28:10 +12:00
< Icon name = "fas fa-arrow-right" className = "service-card-cta-arrow" />
</ span >
2026-05-02 08:26:18 +12:00
</ div >
2026-05-15 01:28:10 +12:00
</ a >
2026-05-02 08:26:18 +12:00
{ /each }
</ div >
</ div >
</ section >
2026-05-02 11:24:11 +12:00
< style >
2026-05-18 09:43:29 +12:00
. services-inner {
max-width : min ( 1180 px , calc ( var ( -- max - w ) - 40 px ));
margin : 0 auto ;
2026-05-15 01:28:10 +12:00
}
2026-05-18 09:43:29 +12:00
. section-header {
grid-template-columns : minmax ( 0 , 1 fr ) minmax ( 20 rem , 0.85 fr );
align-items : start ;
column-gap : clamp ( 32 px , 5 vw , 72 px );
row-gap : 14 px ;
text-align : left ;
2026-05-15 01:28:10 +12:00
}
2026-05-18 09:43:29 +12:00
. section-header . section-heading {
max-width : 22 ch ;
text-align : left ;
2026-05-15 01:28:10 +12:00
}
2026-05-18 09:43:29 +12:00
. section-header . section-intro {
2026-05-15 01:28:10 +12:00
margin : 0 ;
2026-05-18 09:43:29 +12:00
padding-top : 14 px ;
max-width : 44 ch ;
justify-self : end ;
text-align : left ;
line-height : 1.72 ;
color : var ( -- text - heading - soft , var ( -- text - muted ));
2026-05-15 01:28:10 +12:00
}
2026-05-18 09:43:29 +12:00
/* ── Section intro ── */
. services-intro {
max-width : 34 ch ;
2026-05-15 01:28:10 +12:00
}
/* ── Service cards ── */
. services-grid {
display : grid ;
grid-template-columns : repeat ( 3 , minmax ( 0 , 1 fr ));
2026-05-18 09:43:29 +12:00
align-items : stretch ;
gap : 24 px ;
margin-top : 40 px ;
2026-05-15 01:28:10 +12:00
}
. service-card {
display : flex ;
flex-direction : column ;
2026-05-18 09:43:29 +12:00
height : 100 % ;
2026-05-15 01:28:10 +12:00
overflow : hidden ;
padding : 0 ;
text-align : left ;
border-radius : 24 px ;
background : #fff ;
border : 1 px solid rgba ( 17 , 20 , 24 , 0.07 );
box-shadow : 0 6 px 20 px rgba ( 17 , 20 , 24 , 0.05 );
text-decoration : none ;
color : inherit ;
2026-05-02 11:24:11 +12:00
transition :
2026-05-15 01:28:10 +12:00
box-shadow 0.28 s ease ,
transform 0.24 s cubic-bezier ( 0.22 , 1 , 0.36 , 1 ),
border-color 0.28 s ease ;
2026-05-02 11:24:11 +12:00
}
2026-05-18 09:43:29 +12:00
/* Featured emphasis lives in the chrome (border + glow + emblem shine),
not the column span — keeps all three cards visually balanced. */
2026-05-15 01:28:10 +12:00
. service-card-featured {
border-color : rgba ( 242 , 191 , 47 , 0.45 );
box-shadow :
inset 0 0 0 1 px rgba ( 255 , 209 , 0 , 0.22 ),
0 10 px 26 px rgba ( 17 , 20 , 24 , 0.07 );
2026-05-02 11:24:11 +12:00
}
2026-05-15 01:28:10 +12:00
@ media ( hover : hover ) {
. service-card : hover {
transform : translateY ( -6 px );
border-color : rgba ( 33 , 48 , 33 , 0.16 );
box-shadow : 0 22 px 46 px rgba ( 17 , 20 , 24 , 0.13 );
filter : none ;
2026-05-02 11:24:11 +12:00
}
2026-05-15 01:28:10 +12:00
. service-card : hover . service-card-media img {
transform : scale ( 1.06 );
2026-05-02 11:24:11 +12:00
}
2026-05-18 09:43:29 +12:00
. service-card : hover : global ( . service-card-cta-arrow ) {
2026-05-15 01:28:10 +12:00
transform : translateX ( 4 px );
2026-05-02 11:24:11 +12:00
}
2026-05-15 01:28:10 +12:00
. 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.9 s cubic-bezier ( 0.22 , 1 , 0.36 , 1 ) 1 ;
2026-05-02 11:24:11 +12:00
}
}
2026-05-15 01:28:10 +12:00
. service-card : active {
transform : translateY ( -3 px );
}
2026-05-02 11:24:11 +12:00
2026-05-15 01:28:10 +12:00
. service-card-media {
position : relative ;
aspect-ratio : 4 / 3 ;
overflow : hidden ;
background : #ede4d2 ;
2026-05-02 11:24:11 +12:00
}
2026-05-07 21:47:42 +12:00
2026-05-15 01:28:10 +12:00
. service-card-media img {
display : block ;
width : 100 % ;
height : 100 % ;
object-fit : cover ;
transition : transform 0.6 s cubic-bezier ( 0.22 , 1 , 0.36 , 1 );
2026-05-07 21:47:42 +12:00
}
2026-05-15 01:28:10 +12:00
. service-card-body {
position : relative ;
display : flex ;
flex : 1 ;
flex-direction : column ;
padding : 42 px 26 px 26 px ;
}
/* Brand emblem straddles the photo / body seam */
. service-card-emblem {
position : absolute ;
top : 0 ;
left : 24 px ;
transform : translateY ( -50 % );
display : flex ;
align-items : center ;
2026-05-07 21:47:42 +12:00
justify-content : center ;
2026-05-15 01:28:10 +12:00
width : 52 px ;
height : 52 px ;
border-radius : 16 px ;
background : var ( -- gw - green );
box-shadow : 0 10 px 22 px rgba ( 33 , 48 , 33 , 0.26 );
overflow : hidden ;
2026-05-07 21:47:42 +12:00
}
2026-05-15 01:28:10 +12:00
. service-card-emblem : global ( . service-card-emblem-glyph ) {
font-size : 22 px ;
color : var ( -- yellow );
}
. service-card-emblem :: after {
content : '' ;
position : absolute ;
top : -20 % ;
left : -85 % ;
width : 60 % ;
height : 140 % ;
background : linear-gradient (
120 deg ,
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 ( 14 deg );
pointer-events : none ;
opacity : 0 ;
}
. service-card-eyebrow {
margin-bottom : 8 px ;
color : var ( -- gw - green );
font-family : var ( -- font - head );
font-size : 11 px ;
font-weight : 700 ;
letter-spacing : 0.08 em ;
text-transform : uppercase ;
}
. service-card-body h3 {
margin : 0 0 8 px ;
font-family : var ( -- font - head );
font-size : 21 px ;
font-weight : 700 ;
line-height : 1.2 ;
letter-spacing : -0.02 em ;
color : #0d1a0d ;
}
. service-card-body p {
margin : 0 ;
color : #4c5056 ;
font-size : 15 px ;
line-height : 1.6 ;
}
2026-05-18 09:43:29 +12:00
/* Push cues+CTA cluster to the bottom so it lines up across all cards
regardless of how long each card's lead paragraph runs. */
2026-05-15 01:28:10 +12:00
. service-card-cues {
display : flex ;
flex-wrap : wrap ;
gap : 7 px ;
2026-05-18 09:43:29 +12:00
margin-top : auto ;
padding-top : 18 px ;
padding-bottom : 18 px ;
2026-05-15 01:28:10 +12:00
}
. service-card-cue {
2026-05-07 21:47:42 +12:00
display : inline-flex ;
align-items : center ;
min-height : 28 px ;
2026-05-15 01:28:10 +12:00
padding : 4 px 11 px ;
border-radius : 999 px ;
background : rgba ( 33 , 48 , 33 , 0.06 );
box-shadow : inset 0 0 0 1 px rgba ( 33 , 48 , 33 , 0.07 );
2026-05-07 21:47:42 +12:00
color : var ( -- gw - green );
2026-05-15 01:28:10 +12:00
font-size : 11 px ;
2026-05-07 21:47:42 +12:00
font-weight : 700 ;
2026-05-15 01:28:10 +12:00
line-height : 1.2 ;
2026-05-07 21:47:42 +12:00
}
2026-05-15 01:28:10 +12:00
. service-card-cta {
display : flex ;
align-items : center ;
gap : 8 px ;
padding-top : 20 px ;
border-top : 1 px solid rgba ( 17 , 20 , 24 , 0.08 );
color : var ( -- gw - green );
font-family : var ( -- font - head );
font-size : 13 px ;
font-weight : 700 ;
}
2026-05-18 09:43:29 +12:00
: global ( . service-card-cta-arrow ) {
2026-05-15 01:28:10 +12:00
font-size : 11 px ;
transition : transform 0.2 s cubic-bezier ( 0.22 , 1 , 0.36 , 1 );
}
/* ── Mobile ── */
2026-05-18 09:43:29 +12:00
@ media ( max-width : 1024px ) {
. section-header {
2026-05-15 01:28:10 +12:00
grid-template-columns : 1 fr ;
2026-05-18 09:43:29 +12:00
text-align : center ;
2026-05-15 01:28:10 +12:00
}
2026-05-18 09:43:29 +12:00
. section-header . section-heading ,
. section-header . section-intro {
max-width : 32 rem ;
justify-self : center ;
text-align : center ;
2026-05-15 01:28:10 +12:00
}
2026-05-18 09:43:29 +12:00
. section-header . section-intro {
padding-top : 0 ;
2026-05-15 01:28:10 +12:00
}
2026-05-18 09:43:29 +12:00
. services-grid {
grid-template-columns : repeat ( 2 , minmax ( 0 , 1 fr ));
2026-05-15 01:28:10 +12:00
}
2026-05-18 09:43:29 +12:00
/* Featured card takes the full width on tablet so it sits on its own
row above the other two — keeps the emphasis without the asymmetric
desktop grid. */
. service-card-featured {
grid-column : 1 / -1 ;
2026-05-15 01:28:10 +12:00
}
2026-05-18 09:43:29 +12:00
}
2026-05-15 01:28:10 +12:00
2026-05-18 09:43:29 +12:00
@ media ( max-width : 768px ) {
. services-intro {
max-width : 34 ch ;
2026-05-15 01:28:10 +12:00
}
. services-grid {
grid-template-columns : 1 fr ;
gap : 16 px ;
margin-top : 24 px ;
}
2026-05-18 09:43:29 +12:00
. service-card-featured {
grid-column : auto ;
}
2026-05-15 01:28:10 +12:00
. service-card-body {
padding : 40 px 22 px 24 px ;
}
. service-card-emblem {
width : 48 px ;
height : 48 px ;
border-radius : 15 px ;
}
. service-card-emblem : global ( . service-card-emblem-glyph ) {
font-size : 20 px ;
}
. service-card-body h3 {
font-size : 20 px ;
2026-05-07 21:47:42 +12:00
}
}
2026-05-11 21:02:24 +12:00
2026-05-15 01:28:10 +12:00
@ 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 ;
2026-05-18 09:43:29 +12:00
transform : translate3d ( 0 , var ( -- reveal - distance , 16 px ), 0 );
2026-05-15 01:28:10 +12:00
transition :
2026-05-18 09:43:29 +12:00
opacity 0.3 s ease ,
transform 0.45 s cubic-bezier ( 0.2 , 0.8 , 0.2 , 1 );
2026-05-15 01:28:10 +12:00
transition-delay : var ( -- reveal - delay , 0 ms );
}
: global ( . reveal-visible . reveal-block ) {
opacity : 1 ;
transform : translate3d ( 0 , 0 , 0 );
}
2026-05-26 08:30:08 +12:00
/* Tier choreography — header settles, then the three cards cascade. */
@ media ( prefers-reduced-motion : no-preference ) {
: global ( . reveal-visible ) . section-header {
transition-delay : 40 ms ;
}
: global ( . reveal-visible ) . services-grid > : nth-child ( 1 ) { transition-delay : 180 ms ; }
: global ( . reveal-visible ) . services-grid > : nth-child ( 2 ) { transition-delay : 260 ms ; }
: global ( . reveal-visible ) . services-grid > : nth-child ( 3 ) { transition-delay : 340 ms ; }
}
2026-05-02 11:24:11 +12:00
</ style >