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-15 01:28:10 +12:00
export let heading = 'Choose the walk style that suits your dog best.' ;
2026-05-07 21:47:42 +12:00
export let intro =
2026-05-18 09:43:29 +12:00
'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.' ;
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-02 11:24:11 +12:00
< section id = "services" use:reveal = {{ delay : 20 }} class="reveal-block" >
2026-05-02 08:26:18 +12:00
< div class = "services-inner" >
2026-05-15 01:28:10 +12:00
< div class = "section-header" >
< h2 class = "section-heading" > { heading } </ h2 >
< p class = "section-intro services-intro" > { intro } </ p >
</ div >
2026-05-02 08:26:18 +12:00
< div class = "services-grid" >
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 }
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 }
</ 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-02 11:24:11 +12:00
</ style >