v4.0.6 - Simplicity focus

This commit is contained in:
2026-07-04 09:59:57 +12:00
parent bfd96c677f
commit 8eb20813c8
21 changed files with 2410 additions and 5221 deletions
+11 -6
View File
@@ -8,6 +8,9 @@
export let faqs: FaqItem[];
export let emitSchema = true;
export let variant: 'panel' | 'plain' = 'panel';
// When the host page supplies its own heading (e.g. to match a page-wide
// section system), suppress the built-in one but keep the schema + list.
export let showHeading = true;
$: schemaJson = JSON.stringify({
'@context': 'https://schema.org',
@@ -30,12 +33,14 @@
</svelte:head>
<div class:faq-section-plain={variant === 'plain'} class="faq-section">
<h2 class="faq-section-heading">
<span class="faq-section-icon"><Icon name="fas fa-circle-question" /></span>
{title}
</h2>
{#if intro}
<p class="faq-section-intro">{intro}</p>
{#if showHeading}
<h2 class="faq-section-heading">
<span class="faq-section-icon"><Icon name="fas fa-circle-question" /></span>
{title}
</h2>
{#if intro}
<p class="faq-section-intro">{intro}</p>
{/if}
{/if}
<div use:accordion class="faq">
{#each faqs as faq}
@@ -1,929 +0,0 @@
<script lang="ts">
type LabelAnchor = 'top' | 'bottom' | 'left' | 'right';
type LabelTone = 'brand' | 'accent';
type Breakpoint = 'desktop' | 'tablet' | 'mobile';
type Placement = {
x: number;
y: number;
anchor: LabelAnchor;
dx?: number;
dy?: number;
visible?: boolean;
z?: number;
};
type Pin = {
slug: string;
suburb: string;
tone?: LabelTone;
desktop: Placement;
tablet?: Partial<Placement>;
mobile?: Partial<Placement>;
};
const mapWidth = 640;
const mapViewY = 100;
const mapHeight = 350;
const centre = { x: 320, y: 238 };
const city = { x: 344, y: 176 };
const pins: Pin[] = [
{
slug: 'pt-chevalier',
suburb: 'Pt Chevalier',
tone: 'accent',
desktop: { x: 124, y: 228, anchor: 'left', dy: -6, z: 4 },
tablet: { x: 120, y: 230, anchor: 'left', dy: -4 },
mobile: { x: 114, y: 230, anchor: 'right', dx: 8, dy: -2, z: 6 }
},
{
slug: 'herne-bay',
suburb: 'Herne Bay',
tone: 'accent',
desktop: { x: 226, y: 118, anchor: 'top', dx: -10, z: 5 },
tablet: { x: 224, y: 122, anchor: 'top', dx: -14, dy: -2 },
mobile: { x: 214, y: 116, anchor: 'bottom', dx: -14, dy: 6, z: 6 }
},
{
slug: 'freemans-bay',
suburb: 'Freemans Bay',
desktop: { x: 330, y: 124, anchor: 'top', dx: 14, dy: -4, z: 5 },
tablet: { x: 326, y: 130, anchor: 'top', dx: 10, dy: -2 },
mobile: { x: 322, y: 132, anchor: 'top', dx: 8, dy: -4, visible: false }
},
{
slug: 'ponsonby',
suburb: 'Ponsonby',
desktop: { x: 250, y: 178, anchor: 'left', dx: -4, dy: -10, z: 5 },
tablet: { x: 244, y: 184, anchor: 'left', dx: -6, dy: -10 },
mobile: { x: 238, y: 172, anchor: 'top', dx: -6, dy: -10, z: 6 }
},
{
slug: 'grey-lynn',
suburb: 'Grey Lynn',
desktop: { x: 214, y: 220, anchor: 'left', dx: -10, dy: -2, z: 5 },
tablet: { x: 208, y: 222, anchor: 'left', dx: -10, dy: 2 },
mobile: { x: 196, y: 222, anchor: 'left', dx: -6, dy: 0, z: 6 }
},
{
slug: 'kingsland',
suburb: 'Kingsland',
desktop: { x: 248, y: 258, anchor: 'left', dx: -6, dy: -2 },
tablet: { x: 236, y: 260, anchor: 'left', dx: -8, dy: -2 },
mobile: { x: 232, y: 260, anchor: 'left', dx: -2, visible: false }
},
{
slug: 'morningside',
suburb: 'Morningside',
desktop: { x: 196, y: 290, anchor: 'left', dx: -10, dy: 2 },
tablet: { x: 186, y: 290, anchor: 'left', dx: -8, dy: 4 },
mobile: { x: 184, y: 288, anchor: 'left', dx: -2, dy: 2, visible: false }
},
{
slug: 'mt-albert',
suburb: 'Mt Albert',
tone: 'accent',
desktop: { x: 148, y: 314, anchor: 'left', dx: -6, dy: 2, z: 4 },
tablet: { x: 148, y: 314, anchor: 'left', dx: -8, dy: 4 },
mobile: { x: 144, y: 312, anchor: 'left', dx: -2, dy: 0, z: 5 }
},
{
slug: 'sandringham',
suburb: 'Sandringham',
desktop: { x: 244, y: 344, anchor: 'bottom', dx: -10, dy: 0, z: 4 },
tablet: { x: 238, y: 340, anchor: 'bottom', dx: -14, dy: 2 },
mobile: { x: 238, y: 340, anchor: 'bottom', dx: -14, dy: 2, visible: false }
},
{
slug: 'mt-eden',
suburb: 'Mt Eden',
desktop: { x: 344, y: 280, anchor: 'right', dx: 6, dy: -8, z: 5 },
tablet: { x: 334, y: 282, anchor: 'right', dx: 6, dy: -6, z: 5 },
mobile: { x: 332, y: 280, anchor: 'right', dx: 4, dy: -8, z: 6 }
},
{
slug: 'balmoral',
suburb: 'Balmoral',
desktop: { x: 314, y: 334, anchor: 'right', dx: 8, dy: 4, z: 4 },
tablet: { x: 306, y: 338, anchor: 'bottom', dx: 0, dy: 6, z: 4 },
mobile: { x: 304, y: 338, anchor: 'bottom', dx: 0, dy: 6, visible: false }
},
{
slug: 'remuera',
suburb: 'Remuera',
tone: 'accent',
desktop: { x: 470, y: 272, anchor: 'right', dx: 10, dy: -10, z: 4 },
tablet: { x: 452, y: 280, anchor: 'right', dx: 8, dy: -8, z: 4 },
mobile: { x: 438, y: 276, anchor: 'right', dx: 6, dy: -6, z: 5 }
},
{
slug: 'greenlane',
suburb: 'Greenlane',
desktop: { x: 426, y: 348, anchor: 'right', dx: 10, dy: 6, z: 4 },
tablet: { x: 410, y: 346, anchor: 'right', dx: 8, dy: 6, z: 4 },
mobile: { x: 404, y: 340, anchor: 'right', dx: 4, dy: 6, z: 5 }
},
{
slug: 'mt-roskill',
suburb: 'Mt Roskill',
desktop: { x: 182, y: 384, anchor: 'left', dx: -2, dy: 4, z: 3 },
tablet: { x: 180, y: 384, anchor: 'left', dx: -2, dy: 4, z: 3 },
mobile: { x: 178, y: 382, anchor: 'left', dx: 4, dy: 2, z: 5 }
},
{
slug: 'three-kings',
suburb: 'Three Kings',
desktop: { x: 296, y: 390, anchor: 'bottom', dx: 8, dy: 0, z: 4 },
tablet: { x: 292, y: 388, anchor: 'bottom', dx: 8, dy: 0, z: 4 },
mobile: { x: 292, y: 390, anchor: 'bottom', dx: 6, dy: 0, z: 6 }
},
{
slug: 'hillsborough',
suburb: 'Hillsborough',
tone: 'accent',
desktop: { x: 216, y: 426, anchor: 'bottom', dx: -6, dy: 0, z: 4 },
tablet: { x: 214, y: 422, anchor: 'bottom', dx: -8, dy: 0, z: 4 },
mobile: { x: 218, y: 418, anchor: 'top', dx: -2, dy: -8, z: 6 }
},
{
slug: 'onehunga',
suburb: 'Onehunga',
desktop: { x: 364, y: 426, anchor: 'bottom', dx: 8, dy: 0, z: 3 },
tablet: { x: 354, y: 424, anchor: 'bottom', dx: 8, dy: 0, z: 3 },
mobile: { x: 352, y: 420, anchor: 'top', dx: 10, dy: -8, visible: false }
}
];
function percentX(x: number) {
return `${(x / mapWidth) * 100}%`;
}
function percentY(y: number) {
return `${((y - mapViewY) / mapHeight) * 100}%`;
}
function resolvePlacement(pin: Pin, breakpoint: Breakpoint): Placement {
const desktop = pin.desktop;
const override =
breakpoint === 'desktop' ? {} : breakpoint === 'tablet' ? pin.tablet ?? {} : pin.mobile ?? {};
return {
...desktop,
...override
};
}
function placementTokens(prefix: string, placement: Placement, gap: number, stem: number) {
const dx = placement.dx ?? 0;
const dy = placement.dy ?? 0;
if (placement.anchor === 'left') {
return [
`--${prefix}-direction: row-reverse`,
`--${prefix}-transform: translate(calc(-100% - ${gap}px + ${dx}px), calc(-50% + ${dy}px))`,
`--${prefix}-stem-w: ${stem}px`,
`--${prefix}-stem-h: 1.5px`
];
}
if (placement.anchor === 'right') {
return [
`--${prefix}-direction: row`,
`--${prefix}-transform: translate(calc(${gap}px + ${dx}px), calc(-50% + ${dy}px))`,
`--${prefix}-stem-w: ${stem}px`,
`--${prefix}-stem-h: 1.5px`
];
}
if (placement.anchor === 'top') {
return [
`--${prefix}-direction: column-reverse`,
`--${prefix}-transform: translate(calc(-50% + ${dx}px), calc(-100% - ${gap}px + ${dy}px))`,
`--${prefix}-stem-w: 1.5px`,
`--${prefix}-stem-h: ${stem}px`
];
}
return [
`--${prefix}-direction: column`,
`--${prefix}-transform: translate(calc(-50% + ${dx}px), calc(${gap}px + ${dy}px))`,
`--${prefix}-stem-w: 1.5px`,
`--${prefix}-stem-h: ${stem}px`
];
}
function displayToken(placement: Placement | undefined) {
return placement?.visible === false ? 'none' : 'inline-flex';
}
function pinStyle(pin: Pin, index: number) {
const desktop = resolvePlacement(pin, 'desktop');
const tablet = resolvePlacement(pin, 'tablet');
const mobile = resolvePlacement(pin, 'mobile');
return [
`--desktop-left: ${percentX(desktop.x)}`,
`--desktop-top: ${percentY(desktop.y)}`,
`--desktop-z: ${desktop.z ?? 3}`,
`--desktop-display: ${displayToken(desktop)}`,
...placementTokens('desktop', desktop, 14, 16),
`--tablet-left: ${percentX(tablet.x)}`,
`--tablet-top: ${percentY(tablet.y)}`,
`--tablet-z: ${tablet.z ?? desktop.z ?? 3}`,
`--tablet-display: ${displayToken(tablet)}`,
...placementTokens('tablet', tablet, 12, 14),
`--mobile-left: ${percentX(mobile.x)}`,
`--mobile-top: ${percentY(mobile.y)}`,
`--mobile-z: ${mobile.z ?? tablet.z ?? desktop.z ?? 3}`,
`--mobile-display: ${displayToken(mobile)}`,
...placementTokens('mobile', mobile, 10, 12),
`--pin-delay: ${((index * 0.11) % 1.6).toFixed(2)}s`
].join('; ');
}
function routePath(pin: Pin) {
const target = pin.desktop;
const controlX = (centre.x + target.x) / 2;
const controlY = (centre.y + target.y) / 2 + (target.y >= centre.y ? 14 : -14);
return `M ${centre.x} ${centre.y} Q ${controlX} ${controlY} ${target.x} ${target.y}`;
}
</script>
<figure class="area-map" aria-labelledby="area-map-caption">
<div class="area-map-shell">
<div class="area-map-stage">
<svg
class="area-map-svg"
viewBox={`0 ${mapViewY} ${mapWidth} ${mapHeight}`}
role="presentation"
aria-hidden="true"
preserveAspectRatio="xMidYMid meet"
>
<defs>
<linearGradient id="area-map-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="rgba(var(--white-rgb), 0.98)" />
<stop offset="58%" stop-color="rgba(var(--white-rgb), 0.94)" />
<stop offset="100%" stop-color="rgba(var(--accent-rgb), 0.1)" />
</linearGradient>
<radialGradient id="area-map-glow" cx="50%" cy="46%" r="54%">
<stop offset="0%" stop-color="rgba(var(--accent-rgb), 0.14)" />
<stop offset="48%" stop-color="rgba(var(--accent-rgb), 0.04)" />
<stop offset="100%" stop-color="rgba(var(--accent-rgb), 0)" />
</radialGradient>
<radialGradient id="area-map-core-glow" cx="50%" cy="50%" r="58%">
<stop offset="0%" stop-color="rgba(var(--brand-rgb), 0.12)" />
<stop offset="70%" stop-color="rgba(var(--brand-rgb), 0.015)" />
<stop offset="100%" stop-color="rgba(var(--brand-rgb), 0)" />
</radialGradient>
<linearGradient id="area-map-district-main" x1="18%" y1="12%" x2="82%" y2="88%">
<stop offset="0%" stop-color="rgba(var(--brand-rgb), 0.16)" />
<stop offset="100%" stop-color="rgba(var(--brand-rgb), 0.05)" />
</linearGradient>
<linearGradient id="area-map-district-soft" x1="10%" y1="20%" x2="95%" y2="85%">
<stop offset="0%" stop-color="rgba(var(--accent-rgb), 0.1)" />
<stop offset="100%" stop-color="rgba(var(--accent-rgb), 0.02)" />
</linearGradient>
<linearGradient id="area-map-route-flow" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="rgba(var(--brand-rgb), 0)" />
<stop offset="40%" stop-color="rgba(var(--brand-rgb), 0.04)" />
<stop offset="56%" stop-color="rgba(var(--accent-rgb), 0.44)" />
<stop offset="72%" stop-color="rgba(var(--brand-rgb), 0.08)" />
<stop offset="100%" stop-color="rgba(var(--brand-rgb), 0)" />
</linearGradient>
<pattern id="area-map-grid" width="52" height="52" patternUnits="userSpaceOnUse">
<path d="M 52 0 L 0 0 0 52" fill="none" stroke="rgba(var(--brand-rgb), 0.045)" stroke-width="1" />
</pattern>
<filter id="area-map-shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="10" stdDeviation="12" flood-color="rgba(var(--brand-rgb), 0.1)" />
</filter>
</defs>
<rect x="0" y="0" width={mapWidth} height="500" rx="32" fill="url(#area-map-bg)" />
<rect x="0" y="0" width={mapWidth} height="500" rx="32" fill="url(#area-map-grid)" />
<ellipse cx="324" cy="234" rx="206" ry="154" fill="url(#area-map-glow)" />
<g class="area-map-waterways">
<path
d="M 26 108 C 96 74, 176 66, 244 84 C 302 100, 336 124, 396 122 C 470 120, 548 76, 620 90"
class="area-map-waterway"
/>
<path
d="M 78 454 C 140 430, 186 416, 240 420 C 292 424, 330 460, 392 462 C 472 466, 560 428, 616 384"
class="area-map-waterway area-map-waterway-soft"
/>
</g>
<g class="area-map-districts" filter="url(#area-map-shadow)">
<path
d="M 94 206 C 132 162, 198 132, 274 126 C 334 122, 396 134, 446 160 C 500 188, 532 240, 530 300 C 528 362, 492 412, 430 434 C 342 466, 224 458, 150 412 C 92 376, 70 282, 94 206 Z"
fill="url(#area-map-district-main)"
/>
<path
d="M 126 214 C 168 180, 222 164, 284 164 C 348 164, 402 180, 442 214 C 474 242, 490 280, 486 320 C 482 366, 448 402, 394 420 C 316 446, 214 438, 154 394 C 112 360, 98 274, 126 214 Z"
fill="rgba(var(--white-rgb), 0.32)"
stroke="rgba(var(--brand-rgb), 0.08)"
stroke-width="1"
/>
<path
d="M 300 158 C 360 158, 420 176, 462 210 C 500 240, 522 278, 522 322 C 522 370, 500 402, 462 414 C 420 426, 362 412, 332 378 C 304 346, 302 312, 338 282 C 366 256, 384 230, 378 202 C 372 178, 346 162, 300 158 Z"
fill="url(#area-map-district-soft)"
/>
<path
d="M 156 248 C 188 236, 224 244, 248 268 C 268 288, 278 320, 266 348 C 252 382, 218 402, 180 402 C 148 400, 120 384, 104 356 C 88 324, 92 286, 116 262 C 128 250, 142 244, 156 248 Z"
fill="rgba(var(--white-rgb), 0.16)"
/>
</g>
<g class="area-map-roads" aria-hidden="true">
<path d="M 148 248 C 204 250, 244 266, 286 292 C 322 316, 360 354, 406 390" class="area-map-road" />
<path d="M 240 126 C 270 160, 292 194, 316 238 C 336 278, 356 340, 364 430" class="area-map-road area-map-road-strong" />
<path d="M 122 300 C 182 304, 244 300, 306 290 C 356 282, 418 264, 468 238" class="area-map-road area-map-road-soft" />
</g>
<g class="area-map-routes">
{#each pins as pin}
<path d={routePath(pin)} class="area-map-route-base" pathLength="1" />
<path d={routePath(pin)} class="area-map-route-flow" pathLength="1" />
{/each}
</g>
<g class="area-map-core">
<circle cx={centre.x} cy={centre.y} r="64" class="area-map-core-aura" />
<circle cx={centre.x} cy={centre.y} r="48" class="area-map-core-halo" />
<circle cx={centre.x} cy={centre.y} r="24" class="area-map-core-pulse" />
<circle cx={centre.x} cy={centre.y} r="15" class="area-map-core-ring" />
<circle cx={centre.x} cy={centre.y} r="10" class="area-map-core-dot" />
</g>
<g class="area-map-skyline" transform={`translate(${city.x - 46} ${city.y - 54}) scale(0.94)`}>
<ellipse cx="42" cy="74" rx="34" ry="8" class="area-map-tower-shadow" />
<path d="M 16 72 C 24 58, 29 41, 32 21 C 34 10, 36 6, 38.5 2.5 C 39.4 1.2, 40.2 0.5, 40.8 0 C 41.5 0.5, 42.3 1.2, 43.2 2.5 C 45.8 6, 47.8 10, 49.8 21 C 52.8 41, 57.8 58, 65.5 72 L 59 72 C 54.8 63.5, 51.8 49.5, 48.4 31 L 47.2 31 L 47.2 20.5 C 47.2 17.6, 46.3 15.7, 44.8 13.8 L 43.9 12.4 L 45 12.4 L 44 8.6 L 42.5 8.6 L 42.8 4.8 L 41.4 4.8 L 40.2 4.8 L 38.8 4.8 L 39.1 8.6 L 37.6 8.6 L 36.6 12.4 L 37.7 12.4 L 36.8 13.8 C 35.3 15.7, 34.4 17.6, 34.4 20.5 L 34.4 31 L 33.2 31 C 29.8 49.5, 26.8 63.5, 22.6 72 Z" class="area-map-tower-body" />
<path d="M 34 21.5 C 34 16.5, 36.8 13.6, 40.8 13.6 C 44.8 13.6, 47.6 16.5, 47.6 21.5 C 47.6 25.3, 45.8 28.2, 43 29.5 L 43 34.2 C 45.8 34.9, 48 36.5, 49.6 39.3 L 31.8 39.3 C 33.5 36.5, 35.6 34.9, 38.6 34.2 L 38.6 29.5 C 35.8 28.2, 34 25.3, 34 21.5 Z" class="area-map-tower-observation" />
<path d="M 30.8 39.3 L 50.6 39.3 L 52 42.9 L 29.4 42.9 Z" class="area-map-tower-band" />
<path d="M 29.4 42.9 C 32.8 46.4, 36.1 47.8, 40.8 47.8 C 45.5 47.8, 48.8 46.4, 52.2 42.9 L 51.1 49.4 C 47.8 51.5, 45 52.2, 40.8 52.2 C 36.6 52.2, 33.8 51.5, 30.5 49.4 Z" class="area-map-tower-ring" />
<path d="M 33.2 52.2 L 48.4 52.2 L 50 72 L 31.6 72 Z" class="area-map-tower-stem" />
<path d="M 37.4 55.6 L 39.4 55.6 L 39.4 68.8 L 37.4 68.8 Z M 42.2 55.6 L 44.2 55.6 L 44.2 68.8 L 42.2 68.8 Z" class="area-map-tower-slit" />
<circle cx="40.8" cy="10.4" r="1.9" class="area-map-tower-beacon" />
</g>
</svg>
<div class="area-map-overlay">
<span
class="area-map-label area-map-label-static area-map-label-city"
style={`left:${percentX(city.x)}; top:${percentY(city.y)};`}
aria-hidden="true"
>
<span class="area-map-label-stem"></span>
<span class="area-map-label-dot-wrap">
<span class="area-map-label-dot area-map-label-dot-city"></span>
</span>
<span class="area-map-label-pill area-map-label-pill-city">City</span>
</span>
{#each pins as pin, index}
<a
href={`/locations/${pin.slug}`}
class={`area-map-label ${pin.tone === 'accent' ? 'area-map-label-accent' : ''}`}
style={pinStyle(pin, index)}
aria-label={`View ${pin.suburb} location page`}
>
<span class="area-map-label-stem" aria-hidden="true"></span>
<span class="area-map-label-dot-wrap" aria-hidden="true">
<span class="area-map-label-pulse"></span>
<span class="area-map-label-dot"></span>
</span>
<span class="area-map-label-pill">
<span class="area-map-label-text-full">{pin.suburb}</span>
</span>
</a>
{/each}
</div>
</div>
</div>
<figcaption id="area-map-caption" class="area-map-caption">
Tap a suburb to open its local page with parks, routes, and service details.
</figcaption>
</figure>
<style>
.area-map {
margin: 0;
}
@media (max-width: 768px) {
.area-map {
display: none;
}
}
.area-map-shell {
display: grid;
place-items: center;
padding: 0 clamp(12px, 1.8vw, 18px) clamp(14px, 2vw, 20px);
overflow: visible;
}
.area-map-stage {
position: relative;
width: min(100%, 54rem);
overflow: hidden;
border-radius: clamp(26px, 2.8vw, 34px);
background:
radial-gradient(circle at 16% 12%, rgba(var(--accent-rgb), 0.1), transparent 30%),
linear-gradient(180deg, rgba(var(--white-rgb), 0.24), rgba(var(--white-rgb), 0));
box-shadow:
inset 0 0 0 1px rgba(var(--brand-rgb), 0.08),
0 18px 42px rgba(var(--ink-rgb), 0.08);
}
.area-map-svg {
display: block;
width: 100%;
height: auto;
aspect-ratio: 640 / 340;
}
.area-map-overlay {
position: absolute;
inset: clamp(14px, 2vw, 20px) clamp(28px, 4vw, 48px);
overflow: visible;
pointer-events: none;
}
.area-map-waterway {
fill: none;
stroke: rgba(var(--white-rgb), 0.56);
stroke-width: 10;
stroke-linecap: round;
opacity: 0.56;
}
.area-map-waterway-soft {
stroke-width: 7;
opacity: 0.28;
}
.area-map-road {
fill: none;
stroke: rgba(var(--brand-rgb), 0.1);
stroke-width: 1.8;
stroke-linecap: round;
stroke-dasharray: 1 9;
opacity: 0.66;
}
.area-map-road-soft {
opacity: 0.38;
stroke-dasharray: 1 11;
}
.area-map-road-strong {
stroke: rgba(var(--brand-rgb), 0.15);
stroke-width: 2.2;
opacity: 0.72;
}
.area-map-route-base {
fill: none;
stroke: rgba(var(--brand-rgb), 0.08);
stroke-width: 1.2;
stroke-linecap: round;
opacity: 0.72;
}
.area-map-route-flow {
fill: none;
stroke: url(#area-map-route-flow);
stroke-width: 2.4;
stroke-linecap: round;
stroke-dasharray: 0.18 0.82;
animation: areaRouteFlow 7.2s linear infinite;
opacity: 0.76;
}
.area-map-core-aura {
fill: url(#area-map-core-glow);
}
.area-map-core-halo {
fill: rgba(var(--accent-rgb), 0.12);
}
.area-map-core-pulse {
fill: rgba(var(--brand-rgb), 0.1);
transform-origin: 320px 238px;
animation: areaCorePulse 4.8s ease-out infinite;
}
.area-map-core-ring {
fill: rgba(var(--white-rgb), 0.86);
stroke: rgba(var(--brand-rgb), 0.16);
stroke-width: 1.25;
}
.area-map-core-dot {
fill: var(--gw-green);
stroke: rgba(var(--accent-rgb), 0.9);
stroke-width: 2.2;
}
.area-map-skyline {
pointer-events: none;
opacity: 0.62;
filter: drop-shadow(0 6px 12px rgba(var(--ink-rgb), 0.12));
}
.area-map-tower-shadow {
fill: rgba(var(--ink-rgb), 0.08);
}
.area-map-tower-body,
.area-map-tower-observation,
.area-map-tower-band,
.area-map-tower-ring,
.area-map-tower-stem,
.area-map-tower-slit {
animation: areaTowerFloat 8s ease-in-out infinite;
transform-origin: 40.8px 74px;
}
.area-map-tower-body {
fill: rgba(var(--white-rgb), 0.92);
stroke: rgba(var(--ink-rgb), 0.24);
stroke-width: 1.05;
}
.area-map-tower-observation {
fill: rgba(var(--white-rgb), 0.96);
stroke: rgba(var(--ink-rgb), 0.3);
stroke-width: 1;
}
.area-map-tower-band {
fill: rgba(var(--ink-rgb), 0.72);
}
.area-map-tower-ring {
fill: rgba(var(--white-rgb), 0.98);
stroke: rgba(var(--ink-rgb), 0.26);
stroke-width: 0.9;
}
.area-map-tower-stem {
fill: rgba(var(--white-rgb), 0.84);
stroke: rgba(var(--ink-rgb), 0.2);
stroke-width: 0.85;
}
.area-map-tower-slit {
fill: rgba(var(--ink-rgb), 0.76);
}
.area-map-tower-beacon {
fill: var(--yellow);
opacity: 0.7;
animation: areaBeaconBlink 4.2s ease-in-out infinite;
}
.area-map-label {
position: absolute;
left: var(--desktop-left);
top: var(--desktop-top);
z-index: var(--desktop-z);
display: var(--desktop-display);
flex-direction: var(--desktop-direction);
align-items: center;
gap: clamp(6px, 0.85vw, 9px);
transform: var(--desktop-transform);
color: inherit;
text-decoration: none;
pointer-events: auto;
outline: none;
}
.area-map-label-static {
left: 53.75%;
top: 32.38%;
z-index: 2;
transform: translate(-50%, calc(-100% - 12px));
pointer-events: none;
}
.area-map-label-stem {
display: block;
flex: 0 0 auto;
width: var(--desktop-stem-w);
height: var(--desktop-stem-h);
border-radius: 999px;
background: rgba(var(--brand-rgb), 0.18);
transition: background var(--motion-fast);
}
.area-map-label-dot-wrap {
position: relative;
flex: 0 0 auto;
width: clamp(11px, 1.4vw, 14px);
height: clamp(11px, 1.4vw, 14px);
}
.area-map-label-dot {
position: absolute;
inset: 0;
margin: auto;
width: clamp(7px, 1vw, 9px);
height: clamp(7px, 1vw, 9px);
border-radius: 50%;
background: var(--gw-green);
border: 2px solid rgba(var(--white-rgb), 0.96);
box-shadow: 0 0 0 5px rgba(var(--accent-rgb), 0.11);
transition:
transform var(--motion-fast),
background var(--motion-fast),
box-shadow var(--motion-fast);
}
.area-map-label-pulse {
position: absolute;
inset: 0;
border-radius: 999px;
background: rgba(var(--brand-rgb), 0.14);
transform: scale(0.5);
opacity: 0;
animation: areaPinPulse 4.6s ease-out infinite;
animation-delay: var(--pin-delay);
}
.area-map-label-pill {
display: inline-flex;
align-items: center;
min-height: clamp(30px, 3vw, 34px);
padding: clamp(6px, 0.95vw, 8px) clamp(10px, 1.35vw, 14px);
border: 1px solid rgba(var(--brand-rgb), 0.12);
border-radius: 999px;
background: rgba(var(--white-rgb), 0.95);
color: var(--text-brand);
font-family: var(--font-head);
font-size: clamp(0.61rem, 0.52rem + 0.24vw, 0.78rem);
font-weight: 700;
letter-spacing: -0.015em;
line-height: 1;
white-space: nowrap;
box-shadow:
inset 0 1px 0 rgba(var(--white-rgb), 0.82),
0 8px 18px rgba(var(--ink-rgb), 0.06);
transition:
transform var(--motion-fast),
background var(--motion-fast),
border-color var(--motion-fast),
box-shadow var(--motion-fast),
color var(--motion-fast);
backdrop-filter: blur(8px);
}
.area-map-label-accent .area-map-label-pill {
background: rgba(var(--accent-rgb), 0.12);
border-color: rgba(var(--accent-rgb), 0.2);
}
.area-map-label-pill-city {
min-height: 28px;
padding: 6px 10px;
background: rgba(var(--brand-rgb), 0.1);
border-color: rgba(var(--brand-rgb), 0.16);
font-size: 0.66rem;
box-shadow: 0 6px 14px rgba(var(--ink-rgb), 0.05);
}
.area-map-label-dot-city {
background: var(--yellow);
box-shadow: 0 0 0 5px rgba(var(--brand-rgb), 0.1);
}
.area-map-label:hover .area-map-label-pill,
.area-map-label:focus-visible .area-map-label-pill {
background: var(--gw-green);
border-color: rgba(var(--brand-rgb), 0.18);
color: var(--text-inverse);
transform: translateY(-1px);
box-shadow: 0 10px 22px rgba(var(--brand-rgb), 0.14);
}
.area-map-label:hover .area-map-label-stem,
.area-map-label:focus-visible .area-map-label-stem {
background: rgba(var(--brand-rgb), 0.32);
}
.area-map-label:hover .area-map-label-dot,
.area-map-label:focus-visible .area-map-label-dot {
background: var(--yellow);
transform: scale(1.12);
box-shadow: 0 0 0 6px rgba(var(--accent-rgb), 0.16);
}
.area-map-caption {
margin: 12px auto 0;
max-width: 54rem;
color: var(--text-subtle);
font-size: 13px;
line-height: 1.55;
text-align: center;
}
@keyframes areaCorePulse {
0% {
transform: scale(0.8);
opacity: 0.42;
}
72% {
transform: scale(1.2);
opacity: 0;
}
100% {
transform: scale(1.2);
opacity: 0;
}
}
@keyframes areaPinPulse {
0% {
transform: scale(0.52);
opacity: 0.32;
}
78% {
transform: scale(2.25);
opacity: 0;
}
100% {
transform: scale(2.25);
opacity: 0;
}
}
@keyframes areaRouteFlow {
from {
stroke-dashoffset: 0;
}
to {
stroke-dashoffset: -1.3;
}
}
@keyframes areaTowerFloat {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-1.5px);
}
}
@keyframes areaBeaconBlink {
0%,
100% {
opacity: 0.3;
transform: scale(0.92);
}
50% {
opacity: 0.74;
transform: scale(1.04);
}
}
@media (max-width: 1120px) {
.area-map-shell {
padding-block: clamp(14px, 2vw, 20px);
}
.area-map-stage {
width: min(100%, 48rem);
}
.area-map-overlay {
inset: 16px 18px 18px;
}
.area-map-label {
left: var(--tablet-left);
top: var(--tablet-top);
z-index: var(--tablet-z);
display: var(--tablet-display);
flex-direction: var(--tablet-direction);
transform: var(--tablet-transform);
}
.area-map-label-stem {
width: var(--tablet-stem-w);
height: var(--tablet-stem-h);
}
}
@media (max-width: 768px) {
.area-map-stage {
width: 100%;
border-radius: 26px;
}
.area-map-overlay {
inset: 14px 16px 18px;
}
.area-map-label-pill {
box-shadow:
inset 0 1px 0 rgba(var(--white-rgb), 0.84),
0 6px 14px rgba(var(--ink-rgb), 0.06);
}
}
@media (max-width: 640px) {
.area-map-shell {
padding-inline: 4px;
padding-bottom: 14px;
}
.area-map-stage {
border-radius: 22px;
}
.area-map-overlay {
inset: 16px 14px 22px;
}
.area-map-label {
left: var(--mobile-left);
top: var(--mobile-top);
z-index: var(--mobile-z);
display: var(--mobile-display);
flex-direction: var(--mobile-direction);
gap: 5px;
transform: var(--mobile-transform);
}
.area-map-label-stem {
width: var(--mobile-stem-w);
height: var(--mobile-stem-h);
}
.area-map-label-pill {
min-height: 28px;
padding: 6px 9px;
font-size: clamp(0.56rem, 0.52rem + 0.18vw, 0.64rem);
}
.area-map-label-pill-city {
min-height: 26px;
padding-inline: 8px;
font-size: 0.6rem;
}
.area-map-label-dot-wrap {
width: 11px;
height: 11px;
}
.area-map-label-dot {
width: 7px;
height: 7px;
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.1);
}
}
@media (max-width: 430px) {
.area-map-shell {
padding-inline: 0;
}
.area-map-overlay {
inset: 18px 12px 24px;
}
.area-map-label-pill {
min-height: 27px;
padding: 5px 8px;
font-size: 0.55rem;
letter-spacing: -0.012em;
}
.area-map-caption {
font-size: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.area-map-route-flow,
.area-map-core-pulse,
.area-map-label-pulse,
.area-map-tower-body,
.area-map-tower-observation,
.area-map-tower-band,
.area-map-tower-ring,
.area-map-tower-stem,
.area-map-tower-slit,
.area-map-tower-beacon {
animation: none;
}
}
</style>