Compare commits
2 Commits
e2c5f38d55
...
a665368d02
| Author | SHA1 | Date | |
|---|---|---|---|
| a665368d02 | |||
| 0eed557f95 |
+15
-1
@@ -24,6 +24,12 @@ $NginxConfigSource = 'nginx/goodwalk.co.nz.svelte.conf.example'
|
|||||||
$NginxConfigTarget = '/docker/nginx/conf.d/goodwalk.co.nz.conf'
|
$NginxConfigTarget = '/docker/nginx/conf.d/goodwalk.co.nz.conf'
|
||||||
$NginxComposeFile = '/docker/nginx/docker-compose.yml'
|
$NginxComposeFile = '/docker/nginx/docker-compose.yml'
|
||||||
$NginxProjectName = 'nginx'
|
$NginxProjectName = 'nginx'
|
||||||
|
# Host paths used for the maintenance page. The directory must be bind-mounted
|
||||||
|
# into the shared nginx container at /var/www/maintenance:ro (see DEPLOYMENT.md).
|
||||||
|
# The flag file lives in the existing conf.d bind mount; nginx ignores non-.conf
|
||||||
|
# files, so it does not pollute the include glob.
|
||||||
|
$MaintenanceHostDir = '/docker/nginx/maintenance'
|
||||||
|
$MaintenanceFlagPath = '/docker/nginx/conf.d/maintenance.flag'
|
||||||
|
|
||||||
# Optional deployment settings.
|
# Optional deployment settings.
|
||||||
$VerifyUrl = 'https://www.goodwalk.co.nz/api/health'
|
$VerifyUrl = 'https://www.goodwalk.co.nz/api/health'
|
||||||
@@ -169,6 +175,8 @@ Assert-NotBlank -Name 'NginxConfigSource' -Value $NginxConfigSource
|
|||||||
Assert-NotBlank -Name 'NginxConfigTarget' -Value $NginxConfigTarget
|
Assert-NotBlank -Name 'NginxConfigTarget' -Value $NginxConfigTarget
|
||||||
Assert-NotBlank -Name 'NginxComposeFile' -Value $NginxComposeFile
|
Assert-NotBlank -Name 'NginxComposeFile' -Value $NginxComposeFile
|
||||||
Assert-NotBlank -Name 'NginxProjectName' -Value $NginxProjectName
|
Assert-NotBlank -Name 'NginxProjectName' -Value $NginxProjectName
|
||||||
|
Assert-NotBlank -Name 'MaintenanceHostDir' -Value $MaintenanceHostDir
|
||||||
|
Assert-NotBlank -Name 'MaintenanceFlagPath' -Value $MaintenanceFlagPath
|
||||||
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($Service)) {
|
if (-not [string]::IsNullOrWhiteSpace($Service)) {
|
||||||
$Service = $Service.Trim()
|
$Service = $Service.Trim()
|
||||||
@@ -206,6 +214,8 @@ Write-Host "[deploy] Remote compose file: $ComposeFileName"
|
|||||||
Write-Host "[deploy] Docker project name: $DockerProjectName"
|
Write-Host "[deploy] Docker project name: $DockerProjectName"
|
||||||
Write-Host "[deploy] Shared nginx config: $NginxConfigTarget"
|
Write-Host "[deploy] Shared nginx config: $NginxConfigTarget"
|
||||||
Write-Host "[deploy] Shared nginx compose file: $NginxComposeFile"
|
Write-Host "[deploy] Shared nginx compose file: $NginxComposeFile"
|
||||||
|
Write-Host "[deploy] Maintenance host dir: $MaintenanceHostDir (must be bind-mounted at /var/www/maintenance:ro)"
|
||||||
|
Write-Host "[deploy] Maintenance flag path: $MaintenanceFlagPath"
|
||||||
Write-Host "[deploy] SSH target: $sshTarget"
|
Write-Host "[deploy] SSH target: $sshTarget"
|
||||||
Write-Host "[deploy] SSH config: $SshConfigPath"
|
Write-Host "[deploy] SSH config: $SshConfigPath"
|
||||||
if (-not [string]::IsNullOrWhiteSpace($Service)) {
|
if (-not [string]::IsNullOrWhiteSpace($Service)) {
|
||||||
@@ -272,7 +282,11 @@ try {
|
|||||||
'--nginx-compose-file',
|
'--nginx-compose-file',
|
||||||
$NginxComposeFile,
|
$NginxComposeFile,
|
||||||
'--nginx-project-name',
|
'--nginx-project-name',
|
||||||
$NginxProjectName
|
$NginxProjectName,
|
||||||
|
'--maintenance-host-dir',
|
||||||
|
$MaintenanceHostDir,
|
||||||
|
'--maintenance-flag',
|
||||||
|
$MaintenanceFlagPath
|
||||||
) + $(if (-not [string]::IsNullOrWhiteSpace($Service)) { @('--service', $Service) } else { @() }))
|
) + $(if (-not [string]::IsNullOrWhiteSpace($Service)) { @('--service', $Service) } else { @() }))
|
||||||
|
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
|
|||||||
@@ -59,16 +59,16 @@ server {
|
|||||||
error_page 503 /maintenance.html;
|
error_page 503 /maintenance.html;
|
||||||
|
|
||||||
location = /maintenance.html {
|
location = /maintenance.html {
|
||||||
root /var/www/html;
|
root /var/www/maintenance;
|
||||||
internal;
|
internal;
|
||||||
add_header Cache-Control "no-store" always;
|
add_header Cache-Control "no-store" always;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Static assets used only by the maintenance page (logo, etc.). Served
|
# Static assets used only by the maintenance page (logo, etc.). Served
|
||||||
# directly from the nginx html mount so they remain reachable while the
|
# from a dedicated bind mount so they remain reachable while the
|
||||||
# SvelteKit app is down.
|
# SvelteKit app is down and do not collide with any other site's html dir.
|
||||||
location /m/ {
|
location /m/ {
|
||||||
root /var/www/html;
|
root /var/www/maintenance;
|
||||||
access_log off;
|
access_log off;
|
||||||
add_header Cache-Control "public, max-age=3600" always;
|
add_header Cache-Control "public, max-age=3600" always;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "goodwalk-svelte-port",
|
"name": "goodwalk-svelte-port",
|
||||||
"version": "4.1.0",
|
"version": "4.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+46
-15
@@ -10,6 +10,8 @@ NGINX_SOURCE=""
|
|||||||
NGINX_TARGET=""
|
NGINX_TARGET=""
|
||||||
NGINX_COMPOSE_FILE=""
|
NGINX_COMPOSE_FILE=""
|
||||||
NGINX_PROJECT_NAME=""
|
NGINX_PROJECT_NAME=""
|
||||||
|
MAINTENANCE_HOST_DIR=""
|
||||||
|
MAINTENANCE_FLAG_PATH=""
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
@@ -18,7 +20,8 @@ Usage:
|
|||||||
deploy-remote.sh --archive <path> --deploy-path <path> --compose-file <name> --project-name <name> [--service <name>]
|
deploy-remote.sh --archive <path> --deploy-path <path> --compose-file <name> --project-name <name> [--service <name>]
|
||||||
deploy-remote.sh --archive <path> --deploy-path <path> --compose-file <name> --project-name <name> \
|
deploy-remote.sh --archive <path> --deploy-path <path> --compose-file <name> --project-name <name> \
|
||||||
[--service <name>] [--nginx-source <path>] [--nginx-target <path>] \
|
[--service <name>] [--nginx-source <path>] [--nginx-target <path>] \
|
||||||
[--nginx-compose-file <path>] [--nginx-project-name <name>]
|
[--nginx-compose-file <path>] [--nginx-project-name <name>] \
|
||||||
|
[--maintenance-host-dir <path>] [--maintenance-flag <path>]
|
||||||
|
|
||||||
This script only updates the main Goodwalk compose project at the specified
|
This script only updates the main Goodwalk compose project at the specified
|
||||||
deployment path. It does not touch unrelated Docker projects or global Docker
|
deployment path. It does not touch unrelated Docker projects or global Docker
|
||||||
@@ -69,6 +72,14 @@ while [[ $# -gt 0 ]]; do
|
|||||||
NGINX_PROJECT_NAME="${2:-}"
|
NGINX_PROJECT_NAME="${2:-}"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--maintenance-host-dir)
|
||||||
|
MAINTENANCE_HOST_DIR="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--maintenance-flag)
|
||||||
|
MAINTENANCE_FLAG_PATH="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
@@ -103,6 +114,8 @@ if (( nginx_args_present )); then
|
|||||||
[[ -n "$NGINX_TARGET" ]] || fail "--nginx-target is required when nginx deployment is enabled"
|
[[ -n "$NGINX_TARGET" ]] || fail "--nginx-target is required when nginx deployment is enabled"
|
||||||
[[ -n "$NGINX_COMPOSE_FILE" ]] || fail "--nginx-compose-file is required when nginx deployment is enabled"
|
[[ -n "$NGINX_COMPOSE_FILE" ]] || fail "--nginx-compose-file is required when nginx deployment is enabled"
|
||||||
[[ -n "$NGINX_PROJECT_NAME" ]] || fail "--nginx-project-name is required when nginx deployment is enabled"
|
[[ -n "$NGINX_PROJECT_NAME" ]] || fail "--nginx-project-name is required when nginx deployment is enabled"
|
||||||
|
[[ -n "$MAINTENANCE_HOST_DIR" ]] || fail "--maintenance-host-dir is required when nginx deployment is enabled"
|
||||||
|
[[ -n "$MAINTENANCE_FLAG_PATH" ]] || fail "--maintenance-flag is required when nginx deployment is enabled"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
if docker compose version >/dev/null 2>&1; then
|
||||||
@@ -118,9 +131,8 @@ MAINTENANCE_ACTIVE=0
|
|||||||
|
|
||||||
clear_maintenance_flag() {
|
clear_maintenance_flag() {
|
||||||
if (( MAINTENANCE_ACTIVE )) && (( nginx_args_present )); then
|
if (( MAINTENANCE_ACTIVE )) && (( nginx_args_present )); then
|
||||||
echo "[deploy-remote] Clearing maintenance flag"
|
echo "[deploy-remote] Clearing maintenance flag at $MAINTENANCE_FLAG_PATH"
|
||||||
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" \
|
rm -f "$MAINTENANCE_FLAG_PATH" || true
|
||||||
exec -T nginx rm -f /etc/nginx/conf.d/maintenance.flag || true
|
|
||||||
MAINTENANCE_ACTIVE=0
|
MAINTENANCE_ACTIVE=0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -144,6 +156,8 @@ if (( nginx_args_present )); then
|
|||||||
echo "[deploy-remote] Nginx config target: $NGINX_TARGET"
|
echo "[deploy-remote] Nginx config target: $NGINX_TARGET"
|
||||||
echo "[deploy-remote] Nginx compose file: $NGINX_COMPOSE_FILE"
|
echo "[deploy-remote] Nginx compose file: $NGINX_COMPOSE_FILE"
|
||||||
echo "[deploy-remote] Nginx project: $NGINX_PROJECT_NAME"
|
echo "[deploy-remote] Nginx project: $NGINX_PROJECT_NAME"
|
||||||
|
echo "[deploy-remote] Maintenance host dir: $MAINTENANCE_HOST_DIR"
|
||||||
|
echo "[deploy-remote] Maintenance flag path: $MAINTENANCE_FLAG_PATH"
|
||||||
fi
|
fi
|
||||||
echo "[deploy-remote] Staging archive in: $STAGING_DIR"
|
echo "[deploy-remote] Staging archive in: $STAGING_DIR"
|
||||||
|
|
||||||
@@ -278,27 +292,44 @@ if (( nginx_args_present )); then
|
|||||||
[[ -f "$MAINTENANCE_HTML_SRC" ]] || fail "Maintenance page missing from deployment payload: $MAINTENANCE_HTML_SRC"
|
[[ -f "$MAINTENANCE_HTML_SRC" ]] || fail "Maintenance page missing from deployment payload: $MAINTENANCE_HTML_SRC"
|
||||||
[[ -f "$MAINTENANCE_LOGO_SRC" ]] || fail "Maintenance logo missing from deployment payload: $MAINTENANCE_LOGO_SRC"
|
[[ -f "$MAINTENANCE_LOGO_SRC" ]] || fail "Maintenance logo missing from deployment payload: $MAINTENANCE_LOGO_SRC"
|
||||||
|
|
||||||
|
# Pre-flight: the nginx container must have a bind mount whose source is
|
||||||
|
# MAINTENANCE_HOST_DIR and whose destination is /var/www/maintenance. If the
|
||||||
|
# one-time droplet setup has not been done, fail fast with a clear message
|
||||||
|
# rather than silently serving stale content.
|
||||||
|
NGINX_CID="$(docker ps -qf name=^nginx$ | head -n1 || true)"
|
||||||
|
[[ -n "$NGINX_CID" ]] || fail "Shared nginx container is not running (expected name 'nginx'). Bring it up before deploying."
|
||||||
|
|
||||||
|
if ! docker inspect -f '{{range .Mounts}}{{.Source}}|{{.Destination}}{{println}}{{end}}' "$NGINX_CID" \
|
||||||
|
| grep -Fxq "${MAINTENANCE_HOST_DIR}|/var/www/maintenance"; then
|
||||||
|
fail "nginx container is missing the maintenance bind mount.
|
||||||
|
Expected: ${MAINTENANCE_HOST_DIR}:/var/www/maintenance:ro
|
||||||
|
One-time setup on the droplet:
|
||||||
|
mkdir -p ${MAINTENANCE_HOST_DIR}/m
|
||||||
|
# add this volume to ${NGINX_COMPOSE_FILE}:
|
||||||
|
# - ${MAINTENANCE_HOST_DIR}:/var/www/maintenance:ro
|
||||||
|
${COMPOSE_CMD[*]} -p ${NGINX_PROJECT_NAME} -f ${NGINX_COMPOSE_FILE} up -d"
|
||||||
|
fi
|
||||||
|
|
||||||
|
FLAG_DIR="$(dirname "$MAINTENANCE_FLAG_PATH")"
|
||||||
|
[[ -d "$FLAG_DIR" ]] || fail "Maintenance flag directory does not exist on host: $FLAG_DIR"
|
||||||
|
|
||||||
|
echo "[deploy-remote] Writing maintenance assets to host bind dir: $MAINTENANCE_HOST_DIR"
|
||||||
|
mkdir -p "$MAINTENANCE_HOST_DIR/m"
|
||||||
|
install -m 0644 "$MAINTENANCE_HTML_SRC" "$MAINTENANCE_HOST_DIR/maintenance.html"
|
||||||
|
install -m 0644 "$MAINTENANCE_LOGO_SRC" "$MAINTENANCE_HOST_DIR/m/logo.png"
|
||||||
|
|
||||||
echo "[deploy-remote] Updating shared nginx config (pre-rebuild) so maintenance routing is active"
|
echo "[deploy-remote] Updating shared nginx config (pre-rebuild) so maintenance routing is active"
|
||||||
mkdir -p "$(dirname "$NGINX_TARGET")"
|
mkdir -p "$(dirname "$NGINX_TARGET")"
|
||||||
cp "$DEPLOY_PATH/$NGINX_SOURCE" "$NGINX_TARGET"
|
cp "$DEPLOY_PATH/$NGINX_SOURCE" "$NGINX_TARGET"
|
||||||
|
|
||||||
echo "[deploy-remote] Installing maintenance page assets into nginx container"
|
|
||||||
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" \
|
|
||||||
exec -T nginx mkdir -p /var/www/html/m
|
|
||||||
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" \
|
|
||||||
cp "$MAINTENANCE_HTML_SRC" nginx:/var/www/html/maintenance.html
|
|
||||||
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" \
|
|
||||||
cp "$MAINTENANCE_LOGO_SRC" nginx:/var/www/html/m/logo.png
|
|
||||||
|
|
||||||
echo "[deploy-remote] Validating nginx configuration"
|
echo "[deploy-remote] Validating nginx configuration"
|
||||||
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -t
|
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -t
|
||||||
|
|
||||||
echo "[deploy-remote] Reloading shared nginx so the new config (incl. maintenance routing) is live"
|
echo "[deploy-remote] Reloading shared nginx so the new config (incl. maintenance routing) is live"
|
||||||
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -s reload
|
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -s reload
|
||||||
|
|
||||||
echo "[deploy-remote] Engaging maintenance page (touch maintenance.flag)"
|
echo "[deploy-remote] Engaging maintenance page via host flag: $MAINTENANCE_FLAG_PATH"
|
||||||
"${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" \
|
: > "$MAINTENANCE_FLAG_PATH"
|
||||||
exec -T nginx touch /etc/nginx/conf.d/maintenance.flag
|
|
||||||
MAINTENANCE_ACTIVE=1
|
MAINTENANCE_ACTIVE=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,25 @@ export function reveal(node: HTMLElement, options: RevealOptions = {}) {
|
|||||||
node.style.setProperty('--reveal-distance', `${settings.distance}px`);
|
node.style.setProperty('--reveal-distance', `${settings.distance}px`);
|
||||||
node.classList.add('reveal-ready');
|
node.classList.add('reveal-ready');
|
||||||
|
|
||||||
|
// If the element is already visible at all in the initial viewport,
|
||||||
|
// reveal it immediately so the first section below the hero doesn't
|
||||||
|
// appear blank on page load.
|
||||||
|
const initialCheck = () => {
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||||
|
if (rect.top < viewportHeight && rect.bottom > 0) {
|
||||||
|
node.classList.add('reveal-visible');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (initialCheck()) {
|
||||||
|
return {
|
||||||
|
destroy() {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '$lib/components/Icon.svelte';
|
|
||||||
import { reveal } from '$lib/actions/reveal';
|
import { reveal } from '$lib/actions/reveal';
|
||||||
|
import ServicesSection from '$lib/components/ServicesSection.svelte';
|
||||||
import { getImageMetadata } from '$lib/image-metadata';
|
import { getImageMetadata } from '$lib/image-metadata';
|
||||||
import type { AboutPageContent, SiteSharedContent } from '$lib/types';
|
import type { AboutPageContent, SiteSharedContent } from '$lib/types';
|
||||||
|
|
||||||
@@ -43,24 +43,7 @@
|
|||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<section use:reveal={{ delay: 40 }} class="about-services reveal-block">
|
<ServicesSection services={content.services} heading={pageContent.servicesTitle} />
|
||||||
<div class="about-inner">
|
|
||||||
<div class="about-section-heading">
|
|
||||||
<h2>{pageContent.servicesTitle}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="about-service-grid">
|
|
||||||
{#each content.services as service}
|
|
||||||
<a class="about-service-card" href={service.href}>
|
|
||||||
<div class="about-service-icon" aria-hidden="true">
|
|
||||||
<Icon name={service.icon} />
|
|
||||||
</div>
|
|
||||||
<span>{service.title}</span>
|
|
||||||
</a>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section use:reveal={{ delay: 70 }} class="about-contact reveal-block">
|
<section use:reveal={{ delay: 70 }} class="about-contact reveal-block">
|
||||||
<div class="about-inner">
|
<div class="about-inner">
|
||||||
@@ -98,7 +81,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.about-hero h1,
|
.about-hero h1,
|
||||||
.about-section-heading h2,
|
|
||||||
.about-copy h2,
|
.about-copy h2,
|
||||||
.about-contact-card h2 {
|
.about-contact-card h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -109,8 +91,7 @@
|
|||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-hero h1,
|
.about-hero h1 {
|
||||||
.about-section-heading {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,19 +102,19 @@
|
|||||||
.about-section-gradient {
|
.about-section-gradient {
|
||||||
margin: 0 24px 88px;
|
margin: 0 24px 88px;
|
||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
border-radius: 36px;
|
border-radius: 28px;
|
||||||
background: linear-gradient(180deg, #f5efe6 0%, #f9f6ef 100%);
|
background: linear-gradient(180deg, #f5efe6 0%, #f9f6ef 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-section-grid {
|
.about-section-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 0.7fr) minmax(0, 1.3fr);
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
gap: 44px;
|
gap: 44px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-section-reverse {
|
.about-section-reverse {
|
||||||
grid-template-columns: minmax(0, 1.3fr) minmax(0, 0.7fr);
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-section-reverse .about-copy {
|
.about-section-reverse .about-copy {
|
||||||
@@ -159,6 +140,8 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 460px;
|
max-width: 460px;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
height: auto;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
border-radius: 28px;
|
border-radius: 28px;
|
||||||
@@ -180,71 +163,12 @@
|
|||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-services {
|
|
||||||
padding: 0 0 88px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-section-heading {
|
|
||||||
margin-bottom: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-section-heading h2 {
|
|
||||||
font-size: clamp(28px, 3vw, 40px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-service-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-service-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 16px;
|
|
||||||
min-height: 200px;
|
|
||||||
padding: 28px 24px;
|
|
||||||
border-radius: 28px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
|
|
||||||
color: #000;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
transition:
|
|
||||||
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
|
|
||||||
box-shadow 0.22s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
.about-service-card:hover {
|
|
||||||
transform: translateY(-6px) scale(1.012);
|
|
||||||
box-shadow: 0 20px 40px rgba(17, 20, 24, 0.09);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-service-card:active {
|
|
||||||
transform: translateY(-1px) scale(0.992);
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-service-icon {
|
|
||||||
font-size: 42px;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-service-card span {
|
|
||||||
font-family: var(--font-head);
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-contact {
|
.about-contact {
|
||||||
padding: 0 0 88px;
|
padding: 0 0 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-contact-card {
|
.about-contact-card {
|
||||||
border-radius: 36px;
|
border-radius: 28px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 42px 48px;
|
padding: 42px 48px;
|
||||||
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
|
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
|
||||||
@@ -281,7 +205,6 @@
|
|||||||
order: initial;
|
order: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-service-grid,
|
|
||||||
.about-contact-grid {
|
.about-contact-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -297,7 +220,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.about-section,
|
.about-section,
|
||||||
.about-services,
|
|
||||||
.about-contact {
|
.about-contact {
|
||||||
padding-bottom: 64px;
|
padding-bottom: 64px;
|
||||||
}
|
}
|
||||||
@@ -313,7 +235,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.about-copy h2,
|
.about-copy h2,
|
||||||
.about-section-heading h2,
|
|
||||||
.about-contact-card h2 {
|
.about-contact-card h2 {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
}
|
}
|
||||||
@@ -323,10 +244,6 @@
|
|||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-service-card {
|
|
||||||
min-height: 168px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-contact-card {
|
.about-contact-card {
|
||||||
padding: 30px 24px;
|
padding: 30px 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
.booking-page-hero h1 {
|
.booking-page-hero h1 {
|
||||||
margin: 0 0 14px;
|
margin: 0 0 14px;
|
||||||
font-family: var(--font-head);
|
font-family: var(--font-head);
|
||||||
font-size: clamp(32px, 4vw, 52px);
|
font-size: clamp(34px, 4vw, 56px);
|
||||||
line-height: 1.05;
|
line-height: 1.05;
|
||||||
letter-spacing: -0.04em;
|
letter-spacing: -0.04em;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|||||||
@@ -372,7 +372,7 @@
|
|||||||
id="location"
|
id="location"
|
||||||
name="location"
|
name="location"
|
||||||
required
|
required
|
||||||
placeholder="Neighborhood, street..."
|
placeholder="Suburb, street..."
|
||||||
class:input-invalid={errors.location}
|
class:input-invalid={errors.location}
|
||||||
on:input={() => clearError('location')}
|
on:input={() => clearError('location')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="instagram-stage">
|
<div class="instagram-stage">
|
||||||
<div class="instagram-panel">
|
<div class="instagram-panel">
|
||||||
<div class="instagram-copy">
|
<div class="instagram-copy">
|
||||||
<span class="instagram-kicker">Daily walks, happy dogs</span>
|
<span class="eyebrow instagram-kicker">Daily walks, happy dogs</span>
|
||||||
<h2>{instagram.title}</h2>
|
<h2>{instagram.title}</h2>
|
||||||
<p class="instagram-blurb">See our dogs in action — walks, play, and happy pups</p>
|
<p class="instagram-blurb">See our dogs in action — walks, play, and happy pups</p>
|
||||||
<a href={instagram.href} target="_blank" rel="noopener" class="btn btn-green instagram-button">
|
<a href={instagram.href} target="_blank" rel="noopener" class="btn btn-green instagram-button">
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
padding: 24px 320px 24px 44px;
|
padding: 24px 320px 24px 44px;
|
||||||
border-radius: 24px;
|
border-radius: 28px;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.52), transparent 42%),
|
radial-gradient(circle at top left, rgba(255, 255, 255, 0.52), transparent 42%),
|
||||||
linear-gradient(135deg, rgba(255, 252, 242, 0.98), rgba(255, 243, 198, 0.96));
|
linear-gradient(135deg, rgba(255, 252, 242, 0.98), rgba(255, 243, 198, 0.96));
|
||||||
@@ -63,16 +63,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.instagram-kicker {
|
.instagram-kicker {
|
||||||
display: inline-flex;
|
/* All visual styling comes from the shared .eyebrow utility. */
|
||||||
margin-bottom: 10px;
|
display: inline-block;
|
||||||
padding: 6px 12px;
|
margin: 0 0 10px;
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(33, 48, 33, 0.08);
|
|
||||||
color: var(--green);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.instagram-copy :global(h2) {
|
.instagram-copy :global(h2) {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
|
|
||||||
.legal-card {
|
.legal-card {
|
||||||
padding: 40px 44px;
|
padding: 40px 44px;
|
||||||
border-radius: 32px;
|
border-radius: 28px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
|
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
|
|
||||||
.legal-card {
|
.legal-card {
|
||||||
padding: 28px 22px;
|
padding: 28px 22px;
|
||||||
border-radius: 24px;
|
border-radius: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-section + .legal-section {
|
.legal-section + .legal-section {
|
||||||
|
|||||||
@@ -349,7 +349,7 @@
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
width: min(420px, calc(100vw - 32px));
|
width: min(420px, calc(100vw - 32px));
|
||||||
padding: 18px 18px 18px 20px;
|
padding: 18px 18px 18px 20px;
|
||||||
border-radius: 24px;
|
border-radius: 28px;
|
||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(247, 243, 232, 0.98));
|
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(247, 243, 232, 0.98));
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -460,7 +460,7 @@
|
|||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.pricing-plan-card:hover {
|
.pricing-plan-card:hover {
|
||||||
transform: translateY(-8px) scale(1.012);
|
transform: translateY(-6px) scale(1.012);
|
||||||
box-shadow: 0 22px 44px rgba(17, 20, 24, 0.1);
|
box-shadow: 0 22px 44px rgba(17, 20, 24, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,7 +547,7 @@
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 18px 18px 16px;
|
padding: 18px 18px 16px;
|
||||||
border-radius: 20px;
|
border-radius: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meet-greet-copy p {
|
.meet-greet-copy p {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<section class="service-hero">
|
<section class="service-hero">
|
||||||
<div class="service-inner service-hero-grid">
|
<div class="service-inner service-hero-grid">
|
||||||
<div class="service-hero-copy">
|
<div class="service-hero-copy">
|
||||||
<p class="service-eyebrow">{pageContent.hero.eyebrow}</p>
|
<p class="eyebrow">{pageContent.hero.eyebrow}</p>
|
||||||
<h1>{pageContent.hero.title}</h1>
|
<h1>{pageContent.hero.title}</h1>
|
||||||
|
|
||||||
{#each pageContent.hero.paragraphs as paragraph}
|
{#each pageContent.hero.paragraphs as paragraph}
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
{#if pageContent.highlight}
|
{#if pageContent.highlight}
|
||||||
<section use:reveal class="service-highlight reveal-block">
|
<section use:reveal class="service-highlight reveal-block">
|
||||||
<div class="service-inner service-highlight-copy">
|
<div class="service-inner service-highlight-copy">
|
||||||
<p class="service-highlight-eyebrow">{pageContent.highlight.eyebrow}</p>
|
<p class="eyebrow service-highlight-eyebrow">{pageContent.highlight.eyebrow}</p>
|
||||||
<h2>{pageContent.highlight.title}</h2>
|
<h2>{pageContent.highlight.title}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -351,16 +351,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-eyebrow {
|
|
||||||
margin: 0 0 18px;
|
|
||||||
color: var(--green);
|
|
||||||
font-family: var(--font-head);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-hero-copy h1,
|
.service-hero-copy h1,
|
||||||
.service-section-heading h2,
|
.service-section-heading h2,
|
||||||
.service-highlight-copy h2 {
|
.service-highlight-copy h2 {
|
||||||
@@ -391,6 +381,11 @@
|
|||||||
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.08);
|
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.service-hero-media img {
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.service-highlight {
|
.service-highlight {
|
||||||
padding: 0 0 96px;
|
padding: 0 0 96px;
|
||||||
}
|
}
|
||||||
@@ -400,12 +395,13 @@
|
|||||||
margin-bottom: 34px;
|
margin-bottom: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Decorative dingbat eyebrow on the highlight section. Inherits sizing /
|
||||||
|
* colour from the shared .eyebrow utility; this rule only adjusts the
|
||||||
|
* downward spacing relative to the H2 underneath.
|
||||||
|
*/
|
||||||
.service-highlight-eyebrow {
|
.service-highlight-eyebrow {
|
||||||
margin: 0 0 16px;
|
margin: 0 0 16px;
|
||||||
color: var(--yellow);
|
|
||||||
font-family: var(--font-head);
|
|
||||||
font-size: 28px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-highlight-image img {
|
.service-highlight-image img {
|
||||||
@@ -473,7 +469,7 @@
|
|||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.service-plan-card:hover,
|
.service-plan-card:hover,
|
||||||
.service-benefit-card:hover {
|
.service-benefit-card:hover {
|
||||||
transform: translateY(-8px) scale(1.012);
|
transform: translateY(-6px) scale(1.012);
|
||||||
box-shadow: 0 22px 44px rgba(17, 20, 24, 0.1);
|
box-shadow: 0 22px 44px rgba(17, 20, 24, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -683,10 +679,6 @@
|
|||||||
padding-bottom: 72px;
|
padding-bottom: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-highlight-eyebrow {
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-extra-row {
|
.service-extra-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
import type { IconCard } from '$lib/types';
|
import type { IconCard } from '$lib/types';
|
||||||
|
|
||||||
export let services: IconCard[];
|
export let services: IconCard[];
|
||||||
|
export let heading = 'What we do';
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section id="services" use:reveal={{ delay: 20 }} class="reveal-block">
|
<section id="services" use:reveal={{ delay: 20 }} class="reveal-block">
|
||||||
<div class="services-inner">
|
<div class="services-inner">
|
||||||
<h2 class="section-heading">What we do</h2>
|
<h2 class="section-heading">{heading}</h2>
|
||||||
|
|
||||||
<div class="services-grid">
|
<div class="services-grid">
|
||||||
{#each services as service}
|
{#each services as service}
|
||||||
|
|||||||
@@ -326,7 +326,7 @@
|
|||||||
.testimonial-stage {
|
.testimonial-stage {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 24px;
|
border-radius: 28px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 10px 30px rgba(20, 24, 20, 0.06);
|
box-shadow: 0 10px 30px rgba(20, 24, 20, 0.06);
|
||||||
min-height: 620px;
|
min-height: 620px;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const aboutPageContent: AboutPageContent = {
|
|||||||
{
|
{
|
||||||
title: 'Who we are',
|
title: 'Who we are',
|
||||||
body: [
|
body: [
|
||||||
"At GoodWalk, we're not your average dog walking service. We're a team of passionate dog lovers dedicated to providing top-notch care for your furry friends. Specializing in small dogs, we understand their unique needs firsthand, being small dog owners ourselves!",
|
"At GoodWalk, we're not your average dog walking service. We're a team of passionate dog lovers dedicated to providing top-notch care for your furry friends. Specialising in small dogs, we understand their unique needs firsthand, being small dog owners ourselves!",
|
||||||
"Our commitment to excellence has quickly made us a leader in Auckland Central's dog-walking scene. From pack walks to one-on-one sessions, we ensure the happiness and well-being of every dog in our care."
|
"Our commitment to excellence has quickly made us a leader in Auckland Central's dog-walking scene. From pack walks to one-on-one sessions, we ensure the happiness and well-being of every dog in our care."
|
||||||
],
|
],
|
||||||
imageUrl: '/images/auckland-pack-walk-dog.jpg',
|
imageUrl: '/images/auckland-pack-walk-dog.jpg',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const dogWalkingContent: ServicePageContent = {
|
|||||||
title: 'Walks for larger breeds, too!',
|
title: 'Walks for larger breeds, too!',
|
||||||
paragraphs: [
|
paragraphs: [
|
||||||
'Looking for personal attention for your big pup? Our professional dog walking service is perfect for larger dogs who walk well on leash and have good recall. Safety is paramount, so we only accommodate dogs with no reactivity issues. At Goodwalk, we pride ourselves on building relationships with both you and your furry family member.',
|
'Looking for personal attention for your big pup? Our professional dog walking service is perfect for larger dogs who walk well on leash and have good recall. Safety is paramount, so we only accommodate dogs with no reactivity issues. At Goodwalk, we pride ourselves on building relationships with both you and your furry family member.',
|
||||||
"Give your dog his best life while joining our growing community of happy pet parents! For those seeking extra care, we offer specialized one-on-one walks tailored to your dog's individual needs and personality"
|
"Give your dog his best life while joining our growing community of happy pet parents! For those seeking extra care, we offer specialised one-on-one walks tailored to your dog's individual needs and personality"
|
||||||
],
|
],
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'/images/auckland-large-dog-one-on-one-walk.jpg',
|
'/images/auckland-large-dog-one-on-one-walk.jpg',
|
||||||
@@ -14,7 +14,7 @@ export const dogWalkingContent: ServicePageContent = {
|
|||||||
},
|
},
|
||||||
highlight: {
|
highlight: {
|
||||||
eyebrow: '▼・ᴥ・▼',
|
eyebrow: '▼・ᴥ・▼',
|
||||||
title: 'Personalized adventures for your dog!',
|
title: 'Personalised adventures for your dog!',
|
||||||
imageUrl: '/images/auckland-dogs-outdoor-pack.jpg',
|
imageUrl: '/images/auckland-dogs-outdoor-pack.jpg',
|
||||||
imageAlt: 'Goodwalk dogs gathered together outdoors'
|
imageAlt: 'Goodwalk dogs gathered together outdoors'
|
||||||
},
|
},
|
||||||
@@ -46,16 +46,16 @@ export const dogWalkingContent: ServicePageContent = {
|
|||||||
title: 'Benefits of our 1:1 walks',
|
title: 'Benefits of our 1:1 walks',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: 'Individualized Attention',
|
title: 'Individualised Attention',
|
||||||
body: 'Large breeds receive personalized care and undivided attention from the walker, addressing their unique needs and preferences without competition from other dogs.'
|
body: 'Large breeds receive personalised care and undivided attention from the walker, addressing their unique needs and preferences without competition from other dogs.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tailored Exercise',
|
title: 'Tailored Exercise',
|
||||||
body: 'Walkers can customize the pace, duration, and route of the walk to accommodate the energy levels and physical abilities of large breeds, providing an appropriate level of exercise tailored to their specific needs.'
|
body: 'Walkers can customise the pace, duration, and route of the walk to accommodate the energy levels and physical abilities of large breeds, providing an appropriate level of exercise tailored to their specific needs.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Bonding and Socialization',
|
title: 'Bonding and Socialisation',
|
||||||
body: 'During one-on-one walks, large breeds bond closely with their walker and socialize with people and animals encountered, promoting confidence and social skills'
|
body: 'During one-on-one walks, large breeds bond closely with their walker and socialise with people and animals encountered, promoting confidence and social skills'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Enhanced safety',
|
title: 'Enhanced safety',
|
||||||
@@ -63,7 +63,7 @@ export const dogWalkingContent: ServicePageContent = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Training Opportunities',
|
title: 'Training Opportunities',
|
||||||
body: 'One-on-one walks offer dedicated time for training and reinforcement of good behaviors, such as loose leash walking or obedience commands, helping large breeds develop and maintain positive habits.'
|
body: 'One-on-one walks offer dedicated time for training and reinforcement of good behaviours, such as loose leash walking or obedience commands, helping large breeds develop and maintain positive habits.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Stress Reduction',
|
title: 'Stress Reduction',
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const homepageContent: HomePageContent = {
|
|||||||
icon: 'fas fa-heart',
|
icon: 'fas fa-heart',
|
||||||
title: 'Kindness',
|
title: 'Kindness',
|
||||||
body:
|
body:
|
||||||
'With gentle care and genuine affection, we make every walk a calm, happy experience. We use positive reinforcement to encourage good behavior because kindness is at the heart of everything we do.'
|
'With gentle care and genuine affection, we make every walk a calm, happy experience. We use positive reinforcement to encourage good behaviour because kindness is at the heart of everything we do.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'fas fa-camera',
|
icon: 'fas fa-camera',
|
||||||
@@ -176,7 +176,7 @@ export const homepageContent: HomePageContent = {
|
|||||||
nearbyCta: { label: 'Book a Meet & Greet', href: '#newlead' },
|
nearbyCta: { label: 'Book a Meet & Greet', href: '#newlead' },
|
||||||
hoursLabel: 'Opening Hours',
|
hoursLabel: 'Opening Hours',
|
||||||
hours: 'Monday to Friday, 8am - 4pm.',
|
hours: 'Monday to Friday, 8am - 4pm.',
|
||||||
faqTitle: "FAQ's",
|
faqTitle: 'FAQs',
|
||||||
faqs: [
|
faqs: [
|
||||||
{
|
{
|
||||||
question: 'Can any dog use your service?',
|
question: 'Can any dog use your service?',
|
||||||
@@ -219,6 +219,7 @@ 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 Us', href: '/about' },
|
||||||
{ label: 'Contact Us', href: '/contact-us' }
|
{ label: 'Contact Us', href: '/contact-us' }
|
||||||
],
|
],
|
||||||
contactLinks: [
|
contactLinks: [
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ export const packWalksContent: ServicePageContent = {
|
|||||||
title: 'Tiny Gang membership benefits',
|
title: 'Tiny Gang membership benefits',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: 'Socialization with other dogs',
|
title: 'Socialisation with other dogs',
|
||||||
body: 'Tiny Gang pack walks help small and medium-sized dogs mingle and learn social skills from each other, boosting their confidence and positive behavior.'
|
body: 'Tiny Gang pack walks help small and medium-sized dogs mingle and learn social skills from each other, boosting their confidence and positive behaviour.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Taliored peace',
|
title: 'Tailored pace',
|
||||||
body: 'Our handlers can adjust the pace and intensity of the walk to suit the energy levels and abilities of small and medium-sized dogs, ensuring a pleasant and enjoyable experience for all participants.'
|
body: 'Our handlers can adjust the pace and intensity of the walk to suit the energy levels and abilities of small and medium-sized dogs, ensuring a pleasant and enjoyable experience for all participants.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -75,8 +75,8 @@ export const packWalksContent: ServicePageContent = {
|
|||||||
body: 'Tiny Gang pack walks foster stronger bonds between dogs and their walker, as well as between the dogs themselves, enhancing trust and companionship among the group.'
|
body: 'Tiny Gang pack walks foster stronger bonds between dogs and their walker, as well as between the dogs themselves, enhancing trust and companionship among the group.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Individualized attention',
|
title: 'Individualised attention',
|
||||||
body: 'Small pack sizes allow for more personalized care and attention from the walker, addressing the unique needs and preferences of small and medium-sized breeds.'
|
body: 'Small pack sizes allow for more personalised care and attention from the walker, addressing the unique needs and preferences of small and medium-sized breeds.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Safety',
|
title: 'Safety',
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ export const puppyVisitsContent: ServicePageContent = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Reduce anxiety',
|
title: 'Reduce anxiety',
|
||||||
body: "With time your pup will know when to expect a visit, reducing the chances of accidents while you're away. With regular visits, your pup will feel loved and secure, minimizing any time spent at home alone."
|
body: "With time your pup will know when to expect a visit, reducing the chances of accidents while you're away. With regular visits, your pup will feel loved and secure, minimising any time spent at home alone."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Expert advise',
|
title: 'Expert advice',
|
||||||
body: "As experienced dog pawrents, we've been through it all with many adorable puppies. Consider us your go-to for any questions or concerns as your furry friend grows up."
|
body: "As experienced dog pawrents, we've been through it all with many adorable puppies. Consider us your go-to for any questions or concerns as your furry friend grows up."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const termsAndConditionsContent: LegalPageContent = {
|
|||||||
{
|
{
|
||||||
type: 'list',
|
type: 'list',
|
||||||
content: [
|
content: [
|
||||||
'5.1 Following the assessment walks, you must select your preferred permanent days of the week for walks, with a minimum commitment of one walk per week. Dogs must attend on these chosen days regularly for a minimum period of at least 6 months. Walks for dogs that show aggressive behavior may be cancelled with immediate effect.',
|
'5.1 Following the assessment walks, you must select your preferred permanent days of the week for walks, with a minimum commitment of one walk per week. Dogs must attend on these chosen days regularly for a minimum period of at least 6 months. Walks for dogs that show aggressive behaviour may be cancelled with immediate effect.',
|
||||||
'5.2 Walks will not take place during severe weather conditions such as high winds, heavy snow, heavy rain, thunder, and lightning. In these cases, your dog will be returned to your residence, or the walk may be shortened, cancelled or rescheduled.',
|
'5.2 Walks will not take place during severe weather conditions such as high winds, heavy snow, heavy rain, thunder, and lightning. In these cases, your dog will be returned to your residence, or the walk may be shortened, cancelled or rescheduled.',
|
||||||
'5.3 If you decide to cancel a walk due to bad weather or heat, you agree to pay for the cancelled walk in full. However, we will endeavour to walk dogs in heavy rain or hot weather as long as we consider that it is safe for the dogs and our dog walkers.',
|
'5.3 If you decide to cancel a walk due to bad weather or heat, you agree to pay for the cancelled walk in full. However, we will endeavour to walk dogs in heavy rain or hot weather as long as we consider that it is safe for the dogs and our dog walkers.',
|
||||||
'5.4 As part of our service, we will work with you to reinforce recall training, leash training, and car behaviour for your dog, using positive reinforcement methods. However, we do not provide individual training sessions and are not responsible for training your dog or for its behaviour.',
|
'5.4 As part of our service, we will work with you to reinforce recall training, leash training, and car behaviour for your dog, using positive reinforcement methods. However, we do not provide individual training sessions and are not responsible for training your dog or for its behaviour.',
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ section {
|
|||||||
|
|
||||||
.service-card {
|
.service-card {
|
||||||
background: var(--off-white);
|
background: var(--off-white);
|
||||||
border-radius: 20px;
|
border-radius: 28px;
|
||||||
padding: 40px 32px;
|
padding: 40px 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition:
|
transition:
|
||||||
@@ -280,7 +280,7 @@ section {
|
|||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.service-card:hover {
|
.service-card:hover {
|
||||||
transform: translateY(-6px) scale(1.01);
|
transform: translateY(-6px) scale(1.012);
|
||||||
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,7 +355,7 @@ footer {
|
|||||||
|
|
||||||
.value-card {
|
.value-card {
|
||||||
background: rgba(255, 255, 255, 0.07);
|
background: rgba(255, 255, 255, 0.07);
|
||||||
border-radius: 16px;
|
border-radius: 28px;
|
||||||
padding: 32px 28px;
|
padding: 32px 28px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
transition:
|
transition:
|
||||||
@@ -366,7 +366,7 @@ footer {
|
|||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.value-card:hover {
|
.value-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-6px) scale(1.012);
|
||||||
box-shadow: 0 18px 34px rgba(0, 0, 0, 0.14);
|
box-shadow: 0 18px 34px rgba(0, 0, 0, 0.14);
|
||||||
border-color: rgba(255, 255, 255, 0.18);
|
border-color: rgba(255, 255, 255, 0.18);
|
||||||
}
|
}
|
||||||
@@ -390,7 +390,7 @@ footer {
|
|||||||
|
|
||||||
.testimonial-card {
|
.testimonial-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 20px;
|
border-radius: 28px;
|
||||||
padding: 36px 32px;
|
padding: 36px 32px;
|
||||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
|
||||||
transition:
|
transition:
|
||||||
@@ -400,7 +400,7 @@ footer {
|
|||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.testimonial-card:hover {
|
.testimonial-card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-6px) scale(1.012);
|
||||||
box-shadow: 0 18px 34px rgba(0, 0, 0, 0.09);
|
box-shadow: 0 18px 34px rgba(0, 0, 0, 0.09);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,13 +35,28 @@
|
|||||||
|
|
||||||
.hero-text h1 {
|
.hero-text h1 {
|
||||||
font-family: var(--font-head);
|
font-family: var(--font-head);
|
||||||
font-size: 50.2px;
|
font-size: clamp(34px, 4vw, 56px);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1.1;
|
line-height: 1.05;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shared eyebrow utility for the small uppercase label that introduces a
|
||||||
|
* heading. Use this instead of bespoke per-section kickers.
|
||||||
|
*/
|
||||||
|
.eyebrow {
|
||||||
|
font-family: var(--font-head);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--green);
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-text h1 .hero-heading-mobile {
|
.hero-text h1 .hero-heading-mobile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user