Component library
Back to templates

Page template

Freight platform homepage

A dark-and-lime homepage for a logistics company with software: ticker, glass overlay header, photo hero with a live-style dashboard card, partner band, KPI strip, colour service grid, control-tower showcase, industry tabs, route price estimator, FAQ, image CTA and footer. Ships with stock photos.

homepagelogisticssaasdarklimeestimatorstock photos

Open preview in a new tab

Use this template

Two ways to get Freight platform homepage into one of your own projects. The first two carry the template's files with them, so nothing has to fetch this page.

Start here

Placement prompt

The whole page: what to do, where it goes, what must be replaced, and all 25 files appended underneath — the page file plus every component it uses — each with the path it belongs at. Paste it into Claude Code inside the destination project.

Read it first — the files are added underneath when you copy
Build a page in this Astro project from the "Freight platform homepage" page template in my Astro Component Library.

Library page, for reference only:
https://astro.baysixmedia.com/templates/freight-platform-homepage/

Route:
[ENTER THE ROUTE THIS PAGE SHOULD BECOME, e.g. / or /services/drain-cleaning]

Business and content:
[ENTER THE BUSINESS NAME, SERVICES, SERVICE AREA, PHONE, AND ANY APPROVED COPY — OR POINT AT THE FILE THAT HOLDS THEM]

Every file you need is appended below, each fenced with the path it belongs at. The page file is src/pages/freight-platform-homepage.astro; every other file is a component it imports. Do not import anything from the deployed library at runtime, and do not fetch the library page — the files below are the source of truth.

1. Inspect this project first: its shared layout, global stylesheet, existing header and footer, and whether the target route already exists. Report what you found before writing anything.
2. Write the component files to their paths. If a file with the same name already exists and differs, stop and ask rather than overwriting it.
3. Put the page at the route above, not at src/pages/freight-platform-homepage.astro unless that is the route. If the route already has a page, replace it only after confirming with me. Update the page's relative imports to match wherever it ends up.
4. If this project has a shared layout, render the page through it instead of the template's own <html> shell, and keep the layout's head, analytics and scripts. Keep exactly one header, one footer and one <main>, and keep a working skip link.
5. Replace ALL demo content in the page's props — every name, heading, paragraph, testimonial, logo, link, image and form destination — with this business's real content. Do not ship demo copy, invented reviews, invented statistics or placeholder links. Where real content is missing, leave a clearly marked TODO rather than inventing it.
6. Map the header and footer links to this project's real routes. Every href must resolve.
7. Adapt colours and typography to the project through documented props where possible. Preserve each component's layout, responsive behaviour, accessibility, interactions and reduced-motion behaviour.

Test the page at 1440, 1280, 1024, 834, 390 and 320px: no horizontal overflow, keyboard operation throughout, and nothing hidden under the header. Run the project's check and build commands. Report the files written, the route, what content is still TODO, and anything you could not verify. Do not commit or deploy.

Install kit

The 25 files and nothing else: the page file and every component, each fenced with its path, followed by each component's verification status. For placing it yourself.

Read it first
Page template kit: "Freight platform homepage"
Library page: https://astro.baysixmedia.com/templates/freight-platform-homepage/
Page file: src/pages/freight-platform-homepage.astro
Files: 25 required

Write each block below to the path named in its header, relative to the project root. The page file imports every component by relative path, so keep the destinations as given or update its imports to match. A block marked (required) is one the page does not work without; (supporting) is documentation. Nothing here needs downloading except a block that says so in as many words.

===== FILE (required): src/pages/freight-platform-homepage.astro =====
---
/**
 * Freight platform homepage — a page template from the Astro Component Library.
 * https://astro.baysixmedia.com/templates/freight-platform-homepage/
 *
 * Every section is a library component with its demo content passed in as
 * props. Replace the props with this site's own content, and move the page
 * inside this project's own layout if it has one.
 */
import GlassNavHeader from "../components/GlassNavHeader.astro";
import SignalTickerBar from "../components/SignalTickerBar.astro";
import DashboardGlassHero from "../components/DashboardGlassHero.astro";
import PartnerChipBand from "../components/PartnerChipBand.astro";
import KpiCardStrip from "../components/KpiCardStrip.astro";
import ColorServiceGrid from "../components/ColorServiceGrid.astro";
import ControlTowerShowcase from "../components/ControlTowerShowcase.astro";
import IndustryTabsPanel from "../components/IndustryTabsPanel.astro";
import RouteQuoteEstimator from "../components/RouteQuoteEstimator.astro";
import SplitIntroFaq from "../components/SplitIntroFaq.astro";
import DarkImageCtaCard from "../components/DarkImageCtaCard.astro";
import LimeContactFooter from "../components/LimeContactFooter.astro";

const glassNavHeaderProps = {
	"items": [
		{
			"label": "Services",
			"href": "#services"
		},
		{
			"label": "Platform",
			"href": "#platform"
		},
		{
			"label": "Industries",
			"href": "#industries"
		},
		{
			"label": "FAQ",
			"href": "#faq"
		},
		{
			"label": "About Us",
			"href": "/about"
		}
	],
	"brandName": "Halcyon",
	"secondary": {
		"label": "Contact us",
		"href": "/contact"
	},
	"cta": {
		"label": "Get a quote",
		"href": "/contact"
	},
	"currentPath": "/"
};

const signalTickerBarProps = {
	"items": [
		"Live carrier capacity",
		"Customs cleared in 4 hours",
		"Around-the-clock dispatch desk",
		"99.2% scan compliance"
	]
};

const dashboardGlassHeroProps = {
	"eyebrow": "Freight intelligence, worldwide",
	"headline": [
		"Ship",
		"anything.",
		"See",
		"everything."
	],
	"body": "One control tower for road, sea, air and final-mile freight, with live tracking, predicted arrival times and one team answering for every lane.",
	"primaryCta": {
		"label": "Plan your shipment",
		"href": "/contact"
	},
	"secondaryCta": {
		"label": "Open route brief",
		"href": "#platform"
	},
	"labels": [
		"Worldwide reach",
		"24/7 dispatch",
		"Enterprise grade"
	],
	"signal": "Live network: 14 lanes flowing",
	"background": {
		"src": "/dashboard-glass-hero/desert-highway.webp",
		"alt": ""
	},
	"dashboard": {
		"eyebrow": "Halcyon control tower",
		"title": "All lanes running clear",
		"stats": [
			{
				"label": "Risk level",
				"value": "LOW",
				"note": "Steady conditions",
				"icon": "shield"
			},
			{
				"label": "On-time arrivals",
				"value": "98.6%",
				"note": "Beating target",
				"icon": "clock"
			},
			{
				"label": "Carbon saved",
				"value": "18%",
				"note": "Down on last month",
				"icon": "leaf"
			},
			{
				"label": "Capacity used",
				"value": "72%",
				"note": "Room to spare",
				"icon": "box"
			}
		],
		"network": [
			{
				"label": "Road lanes",
				"value": "11,860",
				"icon": "road"
			},
			{
				"label": "Sea routes",
				"value": "342",
				"icon": "ship"
			},
			{
				"label": "Air routes",
				"value": "96",
				"icon": "plane"
			},
			{
				"label": "Warehouses",
				"value": "214",
				"icon": "warehouse"
			}
		],
		"insight": {
			"label": "Forecast",
			"text": "No delays forecast on any of your active lanes this week.",
			"detail": "Next model run at 06:00 UTC. Crosswinds on the Rotterdam to Antwerp corridor are being watched and sit well inside tolerance."
		}
	},
	"trust": {
		"title": "Top-rated carrier partner",
		"text": "Relied on by 1,900 shipping teams",
		"stars": true,
		"avatars": [
			{
				"src": "/dashboard-glass-hero/avatar-1.webp",
				"alt": ""
			},
			{
				"src": "/dashboard-glass-hero/avatar-2.webp",
				"alt": ""
			},
			{
				"src": "/dashboard-glass-hero/avatar-3.webp",
				"alt": ""
			}
		]
	}
};

const partnerChipBandProps = {
	"eyebrow": "Trusted across the supply chain",
	"heading": "Chosen by the teams that keep shelves stocked.",
	"partners": [
		{
			"name": "CASPIAN",
			"hue": "115 245 223",
			"glyph": "peak"
		},
		{
			"name": "KESTREL",
			"hue": "200 255 56",
			"glyph": "star"
		},
		{
			"name": "HALDEN",
			"hue": "168 140 255",
			"glyph": "bars"
		},
		{
			"name": "SOLACE",
			"hue": "255 184 107",
			"glyph": "orbit"
		},
		{
			"name": "TIDEWELL",
			"hue": "110 168 255",
			"glyph": "diamond"
		},
		{
			"name": "MARLOW",
			"hue": "247 143 179",
			"glyph": "wave"
		}
	]
};

const kpiCardStripProps = {
	"eyebrow": "Halcyon by the numbers",
	"heading": "Results you can hold us to.",
	"cards": [
		{
			"label": "Global reach",
			"value": "140+",
			"caption": "Countries on the network",
			"accent": "#73f5df"
		},
		{
			"label": "Loads moved",
			"value": "3.1M",
			"caption": "Shipments booked since 2016",
			"accent": "#a88cff"
		},
		{
			"label": "On-time arrival",
			"value": "97.9%",
			"caption": "Rolling 12-month average",
			"accent": "#c8ff38"
		},
		{
			"label": "Dispatch desk",
			"value": "24/7",
			"caption": "A person on the phone, always",
			"accent": "#ffb86b"
		}
	]
};

const colorServiceGridProps = {
	"eyebrow": "What we move",
	"heading": "Every mode. One accountable team.",
	"intro": "From one pallet that has to be there by Friday to a year of contracted ocean space, Halcyon plans it, books it and watches it until it lands.",
	"cards": [
		{
			"title": "Road freight",
			"text": "Full and part loads, reefer and flatbed, dispatched against live traffic and tracked by the minute.",
			"href": "/services/road-freight",
			"tone": "ink",
			"icon": "truck",
			"image": {
				"src": "/color-service-grid/road-freight.webp",
				"alt": "Articulated lorry on an open highway at dusk"
			}
		},
		{
			"title": "Ocean freight",
			"text": "FCL and LCL on every major trade lane, with paperless bills of lading and early warning on port congestion.",
			"href": "/services/ocean-freight",
			"tone": "purple",
			"icon": "ship"
		},
		{
			"title": "Air cargo",
			"text": "Next-flight-out and consolidated air for the shipments that cannot wait for a vessel.",
			"href": "/services/air-cargo",
			"tone": "orange",
			"icon": "plane"
		},
		{
			"title": "Warehousing",
			"text": "Short and long-term storage with live stock counts and same-day pick, pack and ship.",
			"href": "/services/warehousing",
			"tone": "aqua",
			"icon": "warehouse"
		},
		{
			"title": "Last mile",
			"text": "Doorstep delivery with a tracking link your customers will actually open.",
			"href": "/services/last-mile",
			"tone": "lime",
			"icon": "bike"
		},
		{
			"title": "Customs clearance",
			"text": "Licensed brokers who file it right the first time, at sixty border crossings.",
			"href": "/services/customs",
			"tone": "green",
			"icon": "shield"
		}
	]
};

const controlTowerShowcaseProps = {
	"eyebrow": "Halcyon Control Tower",
	"heading": "Your freight, live on one screen.",
	"body": "Follow every truck, container and milestone as it happens. Halcyon flags the delays it can see coming and tells your team exactly what to do about them.",
	"features": [
		"Delay forecasts before the carrier calls",
		"Views built for planners, finance and the floor",
		"Connects to your ERP, WMS and TMS",
		"Carbon reported per shipment, audit-ready"
	],
	"cta": {
		"label": "Explore the platform",
		"href": "/platform"
	},
	"dashboard": {
		"title": "Global operations",
		"timestamp": "Monday, 08:42 UTC",
		"status": "System healthy",
		"image": {
			"src": "/control-tower-showcase/analytics-screen.webp",
			"alt": "Monitor showing shipment analytics charts"
		},
		"onTime": {
			"label": "On-time performance",
			"value": "98.7%",
			"bars": [
				25,
				45,
				32,
				52,
				58,
				48
			],
			"highlight": [
				1,
				4
			]
		},
		"exceptions": {
			"label": "Open exceptions",
			"value": "07",
			"delta": "↓ 18% vs last week"
		},
		"carbon": {
			"label": "CO₂ avoided this month",
			"value": "48.2t"
		}
	}
};

const industryTabsPanelProps = {
	"eyebrow": "Built for your sector",
	"heading": "Freight built around the way your industry works.",
	"tabs": [
		{
			"label": "Automotive",
			"eyebrow": "Automotive logistics",
			"heading": "Parts at the line before the line asks.",
			"text": "Sequenced inbound deliveries, buffer stock held beside the plant and a recovery desk that re-routes a late part before it stops production.",
			"stats": [
				{
					"value": "99.6%",
					"label": "On-time line feed"
				},
				{
					"value": "12 min",
					"label": "Median exception response"
				}
			],
			"image": {
				"src": "/industry-tabs-panel/automotive.webp",
				"alt": "Orange robotic arms working along a car assembly line"
			},
			"cta": {
				"label": "View solution",
				"href": "#automotive"
			}
		},
		{
			"label": "Retail & eCommerce",
			"eyebrow": "Retail & eCommerce",
			"heading": "Peak season without the backlog.",
			"text": "Store replenishment and parcel fulfilment run from one network, so a promotion that sells out in a day does not leave shelves empty for a week.",
			"stats": [
				{
					"value": "48 h",
					"label": "Store restock window"
				},
				{
					"value": "2.1M",
					"label": "Orders shipped at peak"
				}
			],
			"image": {
				"src": "/industry-tabs-panel/retail.webp",
				"alt": "Stacked online-order parcels and cardboard boxes waiting for dispatch"
			},
			"cta": {
				"label": "View solution",
				"href": "#retail"
			}
		},
		{
			"label": "Healthcare",
			"eyebrow": "Healthcare logistics",
			"heading": "A cold chain you can audit.",
			"text": "Validated temperature lanes, GDP-compliant handling and chain-of-custody records for medicines, devices and clinical trial material.",
			"stats": [
				{
					"value": "2–8 °C",
					"label": "Validated lanes"
				},
				{
					"value": "100%",
					"label": "Custody scans logged"
				}
			],
			"image": {
				"src": "/industry-tabs-panel/healthcare.webp",
				"alt": "Warehouse workers in orange coveralls picking boxed stock from shelving"
			},
			"cta": {
				"label": "View solution",
				"href": "#healthcare"
			}
		},
		{
			"label": "Industrial",
			"eyebrow": "Industrial logistics",
			"heading": "Heavy freight, handled precisely.",
			"text": "Project cargo, oversized machinery and spare-part programmes planned around site access, lift windows and the real cost of a day's downtime.",
			"stats": [
				{
					"value": "60 t",
					"label": "Single-lift capacity"
				},
				{
					"value": "24/7",
					"label": "Critical spares desk"
				}
			],
			"image": {
				"src": "/industry-tabs-panel/industrial.webp",
				"alt": "Tower cranes silhouetted against a dusk sky"
			},
			"cta": {
				"label": "View solution",
				"href": "#industrial"
			}
		},
		{
			"label": "Food & beverage",
			"eyebrow": "Food & beverage logistics",
			"heading": "Fresh from dock to shelf.",
			"text": "Multi-temperature trailers, sensor-tracked pallets and shelf-life-aware routing that gets perishables to store with days of life to spare.",
			"stats": [
				{
					"value": "−25 °C",
					"label": "Frozen-lane floor"
				},
				{
					"value": "+3 days",
					"label": "Average shelf life gained"
				}
			],
			"image": {
				"src": "/industry-tabs-panel/food.webp",
				"alt": "Baskets of fresh leafy greens on a produce shelf"
			},
			"cta": {
				"label": "View solution",
				"href": "#food"
			}
		}
	]
};

const routeQuoteEstimatorProps = {
	"eyebrow": "Instant planning",
	"heading": "See the lane before you book it.",
	"text": "Pick a route and a load for an indicative price range and transit time. A Halcyon planner confirms the final plan before anything moves.",
	"badges": [
		"No commitment",
		"Reply in 30 min"
	],
	"lanes": [
		{
			"origin": "Rotterdam, NL",
			"destination": "Berlin, DE",
			"base": 700,
			"perKg": 0.464,
			"transitDays": [
				1,
				2
			]
		},
		{
			"origin": "Rotterdam, NL",
			"destination": "Paris, FR",
			"base": 715,
			"perKg": 0.464,
			"transitDays": [
				1,
				2
			]
		},
		{
			"origin": "Rotterdam, NL",
			"destination": "Milan, IT",
			"base": 980,
			"perKg": 0.58,
			"transitDays": [
				2,
				3
			]
		},
		{
			"origin": "Rotterdam, NL",
			"destination": "Warsaw, PL",
			"base": 890,
			"perKg": 0.52,
			"transitDays": [
				2,
				3
			]
		},
		{
			"origin": "Antwerp, BE",
			"destination": "Berlin, DE",
			"base": 760,
			"perKg": 0.49,
			"transitDays": [
				1,
				2
			]
		},
		{
			"origin": "Antwerp, BE",
			"destination": "Paris, FR",
			"base": 560,
			"perKg": 0.38,
			"transitDays": [
				1,
				1
			]
		},
		{
			"origin": "Antwerp, BE",
			"destination": "Milan, IT",
			"base": 940,
			"perKg": 0.56,
			"transitDays": [
				2,
				3
			]
		},
		{
			"origin": "Antwerp, BE",
			"destination": "Warsaw, PL",
			"base": 980,
			"perKg": 0.58,
			"transitDays": [
				2,
				3
			]
		},
		{
			"origin": "Hamburg, DE",
			"destination": "Berlin, DE",
			"base": 520,
			"perKg": 0.36,
			"transitDays": [
				1,
				1
			]
		},
		{
			"origin": "Hamburg, DE",
			"destination": "Paris, FR",
			"base": 900,
			"perKg": 0.52,
			"transitDays": [
				2,
				3
			]
		},
		{
			"origin": "Hamburg, DE",
			"destination": "Milan, IT",
			"base": 1060,
			"perKg": 0.6,
			"transitDays": [
				2,
				3
			]
		},
		{
			"origin": "Hamburg, DE",
			"destination": "Warsaw, PL",
			"base": 760,
			"perKg": 0.44,
			"transitDays": [
				1,
				2
			]
		}
	],
	"modes": [
		{
			"label": "Road freight",
			"multiplier": 1,
			"transitScale": 1
		},
		{
			"label": "Rail freight",
			"multiplier": 0.82,
			"transitScale": 2
		},
		{
			"label": "Dedicated express",
			"multiplier": 1.45,
			"transitScale": 0.5
		}
	],
	"shipmentTypes": [
		{
			"label": "Standard palletised cargo",
			"multiplier": 1
		},
		{
			"label": "Temperature-controlled",
			"multiplier": 1.28
		},
		{
			"label": "Oversized / non-stackable",
			"multiplier": 1.22
		},
		{
			"label": "Dangerous goods (ADR)",
			"multiplier": 1.4
		}
	],
	"currency": "USD",
	"quoteLabel": "Request verified quote",
	"quoteHref": "#quote"
};

const splitIntroFaqProps = {
	"heading": "Clear answers before your first load.",
	"intro": "Our operations desk is staffed around the clock, so a question about a lane, a rate or a login gets a person, not a queue.",
	"eyebrow": "Straight answers",
	"items": [
		{
			"question": "Can I see road, ocean and air freight in one place?",
			"answer": "Yes. Every mode feeds the same Halcyon control tower, so a container at sea and a pallet on a regional truck sit on one map with one status language."
		},
		{
			"question": "How long does it take to get set up?",
			"answer": "Most teams are live in ten working days. We map your lanes, import open orders and train the people who will use it daily before the first booking goes through."
		},
		{
			"question": "Do you handle chilled and frozen cargo?",
			"answer": "We do. Reefer loads carry continuous temperature logging, and a reading outside the agreed band raises an alert to your team and ours within two minutes."
		},
		{
			"question": "Will it connect to the systems we already run?",
			"answer": "Halcyon ships connectors for the common ERP and warehouse platforms, plus a documented API and flat-file drops for anything older."
		},
		{
			"question": "What happens when a shipment is delayed?",
			"answer": "The exception lands on a named coordinator, who owns it until it is resolved. You see the cause, the revised ETA and the options on the same shipment page."
		},
		{
			"question": "Can finance and operations see different things?",
			"answer": "Yes. Roles decide who sees rates, who can book and who can only track, and each person gets the view that matches their job."
		}
	]
};

const darkImageCtaCardProps = {
	"eyebrow": "Your next shipment",
	"heading": "Let's get your freight moving.",
	"text": "Tell us where it's going and when it has to land. We'll come back with a lane plan and a firm price.",
	"primaryCta": {
		"label": "Request a Quote",
		"href": "/contact"
	},
	"secondaryCta": {
		"label": "Talk to an Expert",
		"href": "/contact"
	},
	"image": {
		"src": "/dark-image-cta-card/winter-highway.webp",
		"alt": "Aerial view of a semi truck on a snow-lined highway at sunset"
	}
};

const limeContactFooterProps = {
	"brandName": "Halcyon",
	"brandSub": "Logistics",
	"tagline": "Freight that moves on time, and data that shows you it did.",
	"social": [
		{
			"label": "Halcyon network map",
			"href": "/network",
			"glyph": "globe",
			"color": "#3b82f6"
		},
		{
			"label": "Halcyon video briefings",
			"href": "/briefings",
			"glyph": "play",
			"color": "#9c9797"
		},
		{
			"label": "Chat with dispatch",
			"href": "/chat",
			"glyph": "chat",
			"color": "#3b82f6"
		},
		{
			"label": "Halcyon newsletter",
			"href": "/newsletter",
			"glyph": "mail",
			"color": "#e5484d"
		}
	],
	"columns": [
		{
			"heading": "Explore",
			"links": [
				{
					"label": "Services",
					"href": "#services"
				},
				{
					"label": "Platform",
					"href": "#platform"
				},
				{
					"label": "Industries",
					"href": "#industries"
				},
				{
					"label": "Customer stories",
					"href": "/stories"
				},
				{
					"label": "Field notes",
					"href": "/notes"
				}
			]
		},
		{
			"heading": "Company",
			"links": [
				{
					"label": "About Halcyon",
					"href": "/about"
				},
				{
					"label": "Emissions pledge",
					"href": "/emissions"
				},
				{
					"label": "Contact",
					"href": "/contact"
				},
				{
					"label": "Careers",
					"href": "/careers"
				}
			]
		}
	],
	"contact": {
		"heading": "Contact",
		"phone": "+1 (415) 555-0188",
		"email": "desk@halcyon.example",
		"address": [
			"48 Harbor Point Road,",
			"Oakland, CA 94607, USA"
		]
	},
	"cta": {
		"label": "Talk to dispatch",
		"href": "/contact"
	},
	"legalLinks": [
		{
			"label": "Disclaimer",
			"href": "/disclaimer"
		},
		{
			"label": "Privacy",
			"href": "/privacy"
		},
		{
			"label": "Terms",
			"href": "/terms"
		},
		{
			"label": "Accessibility",
			"href": "/accessibility"
		}
	]
};
---

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title>[Business name] — [Freight + platform promise]</title>
		<meta name="description" content="[One-sentence description of the carrier, its network and its tracking platform.]" />
		<style is:global>
			*,
			*::before,
			*::after {
				box-sizing: border-box;
			}

			body {
				margin: 0;
				background: #ffffff;
				font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
				color: #111;
			}

			.skip-link {
				position: absolute;
				left: 1rem;
				top: -100px;
				z-index: 1000;
				padding: 0.6rem 1rem;
				background: #111;
				color: #fff;
				border-radius: 6px;
			}

			.skip-link:focus {
				top: 1rem;
			}
		</style>
	</head>
	<body>
		<a class="skip-link" href="#main">Skip to content</a>

		<!-- Glass nav header -->
		<GlassNavHeader {...glassNavHeaderProps} />

		<main id="main">
			<!-- Signal ticker bar -->
			<SignalTickerBar {...signalTickerBarProps} />

			<!-- Dashboard glass hero -->
			<DashboardGlassHero {...dashboardGlassHeroProps} />

			<!-- Partner chip band -->
			<PartnerChipBand {...partnerChipBandProps} />

			<!-- KPI card strip -->
			<KpiCardStrip {...kpiCardStripProps} />

			<!-- Color service grid -->
			<ColorServiceGrid {...colorServiceGridProps} />

			<!-- Control tower showcase -->
			<ControlTowerShowcase {...controlTowerShowcaseProps} />

			<!-- Industry tabs panel -->
			<IndustryTabsPanel {...industryTabsPanelProps} />

			<!-- Route quote estimator -->
			<RouteQuoteEstimator {...routeQuoteEstimatorProps} />

			<!-- Split intro FAQ -->
			<SplitIntroFaq {...splitIntroFaqProps} />

			<!-- Dark image CTA card -->
			<DarkImageCtaCard {...darkImageCtaCardProps} />
		</main>

		<!-- Lime contact footer -->
		<LimeContactFooter {...limeContactFooterProps} />
	</body>
</html>

===== END src/pages/freight-platform-homepage.astro =====

===== FILE (required): src/components/GlassNavHeader.astro =====
---
/**
 * GlassNavHeader
 *
 * A 1170px dark glass bar floating over a hero: logo mark and wordmark on
 * the left, links in the middle, a "Contact us" text link and a lime quote
 * pill on the right. Below 960px it folds to the logo and a real menu
 * button that opens a glass card under the bar.
 *
 * `overlay` (default true) positions it absolutely at `top` inside its
 * containing block — 52px by default, which clears a 38px ticker strip
 * with the measured 14px gap. `overlay={false}` puts it in the flow on an
 * ink band.
 *
 * Reads --nv-* tokens from any ancestor, with fallbacks.
 */

export interface GlassNavHeaderItem {
	label: string;
	href: string;
}

export interface GlassNavHeaderProps {
	items?: GlassNavHeaderItem[];
	/** Pass Astro.url.pathname. Matching links get aria-current="page". */
	currentPath?: string;
	brandName?: string;
	brandHref?: string;
	/** Replaces the drawn mark and wordmark with an image. */
	logo?: { src: string; alt: string; width?: number; height?: number };
	/** The plain text link beside the pill. `null` drops it. */
	secondary?: { label: string; href: string } | null;
	/** The lime pill. `null` drops it. */
	cta?: { label: string; href: string } | null;
	/** Absolute over the hero (true) or in the flow on an ink band (false). */
	overlay?: boolean;
	/** Offset from the top of the containing block in overlay mode, desktop and tablet. */
	top?: string;
	/** The same below 810px, where the ticker is 36px and the gap 12px. */
	topPhone?: string;
	navLabel?: string;
	menuLabel?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends GlassNavHeaderProps {}

/* No invented site: links, brand, text link and pill are all the caller's.
   Each block renders only when supplied; with none, nothing renders. */
const {
	items = [],
	currentPath,
	brandName,
	brandHref = "/",
	logo,
	secondary = null,
	cta = null,
	overlay = true,
	top = "52px",
	topPhone = "48px",
	navLabel = "Primary",
	menuLabel = "Menu",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	id,
} = Astro.props;

/* Ids for aria-controls, seeded from `id` or a random suffix so a second
   instance never collides with the first. */
const seed = id ? id : `gnh-${Math.random().toString(36).slice(2, 8)}`;
const menuId = `${seed}-menu`;

const norm = (p: string) => (p.length > 1 ? p.replace(/\/+$/, "") : p);
const isCurrent = (href?: string) =>
	Boolean(href && currentPath && norm(href) === norm(currentPath));

const style = `--gnh-top:${top};--gnh-top-phone:${topPhone}`;

const brand = brandName?.trim() ?? "";
const hasBrand = Boolean(logo || brand);
const hasItems = items.length > 0;
const hasMenu = hasItems || Boolean(secondary || cta);
const hasContent = hasBrand || hasMenu;
---

{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<header
	class:list={["gnh", overlay ? "gnh--overlay" : "gnh--flow"]}
	id={id}
	style={style}
	data-header-root
	data-gnh-root
>
	<div class="gnh-inner">
		<div class="gnh-bar">
			{hasBrand && (
			<a class="gnh-brand" href={brandHref} aria-label={logo ? undefined : `${brand} home`}>
				{
					logo ? (
						<img
							class="gnh-logo-img"
							src={logo.src}
							alt={logo.alt}
							width={logo.width ?? 110}
							height={logo.height ?? 26}
						/>
					) : (
						<>
							<svg class="gnh-mark" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
								<rect x="4" y="4" width="7" height="24" rx="2" fill="var(--nv-lime, #c8ff38)" />
								<rect x="21" y="4" width="7" height="24" rx="2" fill="#fff" />
								<path d="M11 18.5 21 10v6.2L11 24.7z" fill="var(--nv-lime, #c8ff38)" />
							</svg>
							<span class="gnh-wordmark" aria-hidden="true">{brand}</span>
						</>
					)
				}
			</a>
			)}

			{hasItems && (
			<nav class="gnh-nav" aria-label={navLabel}>
				<ul class="gnh-links" role="list">
					{
						items.map((item) => (
							<li>
								<a
									class="gnh-link"
									href={item.href}
									aria-current={isCurrent(item.href) ? "page" : undefined}
								>
									{item.label}
								</a>
							</li>
						))
					}
				</ul>
			</nav>
			)}

			{
				(secondary || cta) && (
					<div class="gnh-actions">
						{secondary && (
							<a
								class="gnh-secondary"
								href={secondary.href}
								aria-current={isCurrent(secondary.href) ? "page" : undefined}
							>
								{secondary.label}
							</a>
						)}
						{cta && (
							<a class="gnh-cta" href={cta.href}>
								<span>{cta.label}</span>
								<svg class="gnh-arrow" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
									<path d="M2.5 8h10M9 4.5 12.5 8 9 11.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
								</svg>
							</a>
						)}
					</div>
				)
			}

			{hasMenu && (
			<button
				type="button"
				class="gnh-burger"
				aria-expanded="false"
				aria-controls={menuId}
				data-gnh-burger
			>
				<span class="gnh-sr">{menuLabel}</span>
				<svg class="gnh-icon gnh-icon--open" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
					<path d="M4 7h16M4 12h16M4 17h16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
				</svg>
				<svg class="gnh-icon gnh-icon--close" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
					<path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
				</svg>
			</button>
			)}
		</div>

		{hasMenu && (
		<nav class="gnh-menu" id={menuId} aria-label={navLabel} data-gnh-menu>
			{(hasItems || secondary) && (
			<ul class="gnh-menu-list" role="list">
				{
					items.map((item) => (
						<li>
							<a
								class="gnh-menu-link"
								href={item.href}
								aria-current={isCurrent(item.href) ? "page" : undefined}
							>
								{item.label}
							</a>
						</li>
					))
				}
				{
					secondary && (
						<li>
							<a
								class="gnh-menu-link"
								href={secondary.href}
								aria-current={isCurrent(secondary.href) ? "page" : undefined}
							>
								{secondary.label}
							</a>
						</li>
					)
				}
			</ul>
			)}
			{cta && <a class="gnh-menu-cta" href={cta.href}>{cta.label}</a>}
		</nav>
		)}
	</div>
</header>
)}

<style>
	.gnh {
		--gnh-ink: var(--nv-ink, #08110f);
		--gnh-lime: #b7ff39;
		--gnh-font: var(--nv-font, Manrope, ui-sans-serif, system-ui, sans-serif);
		--gnh-dur: 0.3s;
		--gnh-ease: cubic-bezier(0.22, 1, 0.36, 1);

		box-sizing: border-box;
		width: 100%;
		padding: 0 20px;
		font-family: var(--gnh-font);
		color: #fff;
		z-index: 30;
	}

	.gnh *,
	.gnh *::before,
	.gnh *::after {
		box-sizing: border-box;
	}

	.gnh ul {
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.gnh--overlay {
		position: absolute;
		top: var(--gnh-top, 52px);
		left: 0;
		right: 0;
		background: transparent;
	}

	.gnh--flow {
		position: relative;
		padding-top: 14px;
		padding-bottom: 14px;
		background: var(--gnh-ink);
	}

	.gnh-inner {
		position: relative;
		max-width: 1170px;
		margin: 0 auto;
	}

	/* ---- the glass bar ---- */
	.gnh-bar {
		position: relative;
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 16px;
		height: 58px;
		padding: 0 12px;
		border: 1px solid rgb(255 255 255 / 0.1);
		border-radius: 18px;
		background: rgb(8 17 15 / 0.66);
		box-shadow: 0 16px 36px rgb(0 0 0 / 0.22);
		-webkit-backdrop-filter: blur(22px);
		backdrop-filter: blur(22px);
	}

	/* ---- brand ---- */
	.gnh-brand {
		display: inline-flex;
		align-items: center;
		gap: 10px;
		min-height: 44px;
		color: #fff;
		text-decoration: none;
		border-radius: 8px;
		flex: none;
	}

	.gnh-mark {
		display: block;
		width: 26px;
		height: 26px;
		flex: none;
	}

	.gnh-wordmark {
		font-size: 15px;
		font-weight: 800;
		line-height: 15px;
		letter-spacing: -0.04em;
		text-transform: uppercase;
	}

	.gnh-logo-img {
		display: block;
		height: 26px;
		width: auto;
	}

	/* ---- links (desktop) ---- */
	.gnh-nav,
	.gnh-actions {
		display: none;
	}

	.gnh .gnh-links {
		display: flex;
		align-items: center;
		gap: 30px;
	}

	.gnh-link {
		display: inline-flex;
		align-items: center;
		/* 14px type in a 44px target; the negative margin hands the space
		   back so the measured 30px rhythm holds. */
		padding: 15px 4px;
		margin: -15px -4px;
		border-radius: 8px;
		color: rgb(255 255 255 / 0.76);
		font-size: 14px;
		font-weight: 700;
		line-height: 14px;
		text-decoration: none;
		white-space: nowrap;
	}

	/* No hover change was measured on these links; the current page is
	   marked in full white with an underline. */
	.gnh-link[aria-current="page"] {
		color: #fff;
		text-decoration: underline;
		text-decoration-color: var(--gnh-lime);
		text-decoration-thickness: 2px;
		text-underline-offset: 6px;
	}

	/* ---- actions ---- */
	.gnh-actions {
		align-items: center;
		gap: 10px;
		flex: none;
	}

	.gnh-secondary {
		display: inline-flex;
		align-items: center;
		min-height: 44px;
		padding: 0 4px;
		border-radius: 8px;
		color: #fff;
		font-size: 13px;
		font-weight: 800;
		line-height: 13px;
		text-decoration: none;
		white-space: nowrap;
	}

	.gnh-cta {
		display: inline-flex;
		align-items: center;
		gap: 6px;
		height: 46px;
		padding: 0 18px;
		border-radius: 100px;
		background: var(--gnh-lime);
		color: var(--gnh-ink);
		font-size: 13px;
		font-weight: 800;
		line-height: 13px;
		text-decoration: none;
		white-space: nowrap;
		/* The hover grows lime around the label with a spread shadow in the
		   pill's own colour, so the pill swells without reflowing the bar. */
		box-shadow: 0 0 0 0 var(--gnh-lime);
		transition: box-shadow var(--gnh-dur) var(--gnh-ease), background-color var(--gnh-dur) var(--gnh-ease);
	}

	.gnh-cta:hover {
		box-shadow: 0 0 0 5px var(--gnh-lime);
	}

	.gnh-arrow {
		width: 14px;
		height: 14px;
		flex: none;
	}

	/* ---- menu button ---- */
	.gnh-burger {
		display: grid;
		place-items: center;
		width: 44px;
		height: 44px;
		padding: 0;
		border: 0;
		border-radius: 12px;
		background: rgb(255 255 255 / 0.1);
		color: #fff;
		cursor: pointer;
		flex: none;
		transition: background-color var(--gnh-dur) var(--gnh-ease);
	}

	.gnh-burger:hover {
		background: rgb(255 255 255 / 0.16);
	}

	.gnh-icon {
		grid-area: 1 / 1;
		width: 24px;
		height: 24px;
	}

	.gnh-burger[aria-expanded="true"] .gnh-icon--open,
	.gnh-burger[aria-expanded="false"] .gnh-icon--close {
		display: none;
	}

	/* No script, no toggle: the card is shown open under the bar instead, so
	   a button that could do nothing is not offered. */
	.gnh:not([data-gnh-ready]) .gnh-burger {
		display: none;
	}

	/* ---- menu card ---- */
	.gnh-menu {
		display: flex;
		flex-direction: column;
		gap: 8px;
		margin-top: 14px;
		padding: 14px 20px 20px;
		border: 1px solid rgb(255 255 255 / 0.1);
		border-radius: 24px;
		background: rgb(8 17 15 / 0.72);
		box-shadow: 0 16px 36px rgb(0 0 0 / 0.22);
		-webkit-backdrop-filter: blur(22px);
		backdrop-filter: blur(22px);
	}

	/* With script the card hangs under the bar out of the flow, covering the
	   hero rather than pushing the page down, and a closed card is
	   visibility: hidden — out of the tab order and the accessibility tree. */
	.gnh[data-gnh-ready] .gnh-menu {
		position: absolute;
		top: calc(100% + 14px);
		left: 0;
		right: 0;
		margin: 0;
		transition:
			opacity 0.22s var(--gnh-ease),
			transform 0.22s var(--gnh-ease),
			visibility 0s linear 0.22s;
	}

	.gnh[data-gnh-ready]:not([data-gnh-open]) .gnh-menu {
		opacity: 0;
		transform: translateY(-6px);
		visibility: hidden;
		pointer-events: none;
	}

	.gnh[data-gnh-ready][data-gnh-open] .gnh-menu {
		transition:
			opacity 0.22s var(--gnh-ease),
			transform 0.22s var(--gnh-ease),
			visibility 0s;
	}

	.gnh-menu-list {
		display: flex;
		flex-direction: column;
	}

	.gnh-menu-link {
		display: flex;
		align-items: center;
		min-height: 44px;
		border-radius: 8px;
		color: rgb(255 255 255 / 0.88);
		font-size: 15px;
		font-weight: 700;
		line-height: 15px;
		text-decoration: none;
	}

	.gnh-menu-link[aria-current="page"] {
		color: var(--gnh-lime);
	}

	.gnh-menu-cta {
		display: flex;
		align-items: center;
		justify-content: center;
		min-height: 44px;
		padding: 14px 16px;
		border-radius: 12px;
		background: linear-gradient(100deg, #c8ff38 0%, #73f5df 52%, #ffb86b 100%);
		color: var(--gnh-ink);
		font-size: 15px;
		font-weight: 800;
		line-height: 15px;
		text-decoration: none;
	}

	/* ---- focus ---- */
	.gnh a:focus-visible,
	.gnh button:focus-visible {
		outline: 2px solid var(--gnh-lime);
		outline-offset: 2px;
	}

	.gnh-link:focus-visible,
	.gnh-secondary:focus-visible {
		color: #fff;
	}

	.gnh-cta:focus-visible,
	.gnh-menu-cta:focus-visible {
		outline-color: #fff;
		outline-offset: 3px;
	}

	.gnh-sr {
		position: absolute;
		width: 1px;
		height: 1px;
		margin: -1px;
		padding: 0;
		overflow: hidden;
		clip: rect(0 0 0 0);
		white-space: nowrap;
		border: 0;
	}

	/* ---- phone: the reference's own offsets ---- */
	@media (max-width: 809.98px) {
		.gnh {
			padding: 0 12px;
		}

		.gnh--overlay {
			top: var(--gnh-top-phone, 48px);
		}

		.gnh--flow {
			padding-top: 12px;
			padding-bottom: 12px;
		}
	}

	/* ---- desktop: the links and actions appear, the menu goes ---- */
	@media (min-width: 960px) {
		.gnh-bar {
			height: 64px;
			padding: 0 18px;
			border-radius: 22px;
			background: rgb(8 17 15 / 0.62);
		}

		.gnh-wordmark {
			font-size: 18px;
			line-height: 18px;
		}

		.gnh-nav {
			display: block;
			position: absolute;
			left: 50%;
			transform: translateX(-50%);
		}

		.gnh-actions {
			display: flex;
		}

		.gnh-burger,
		.gnh .gnh-menu {
			display: none;
		}
	}

	/* Between 960px and 1100px the centred links would run into the
	   actions; let them sit in the flow between the brand and the actions. */
	@media (min-width: 960px) and (max-width: 1099.98px) {
		.gnh-nav {
			position: static;
			transform: none;
		}

		.gnh .gnh-links {
			gap: 24px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.gnh {
			--gnh-dur: 1ms;
		}

		.gnh[data-gnh-ready] .gnh-menu {
			transform: none !important;
		}
	}

	@media (forced-colors: active) {
		.gnh-bar,
		.gnh-menu {
			border-color: CanvasText;
		}
	}
</style>

<script>
	/**
	 * GlassNavHeader controller: the phone menu only.
	 *
	 * One closure per instance, every lookup scoped to its root, every
	 * listener on an AbortController aborted before a client-side navigation
	 * swaps the document. No body scroll lock: the menu is a card under the
	 * bar, not a sheet over the page. Never moves focus except in response
	 * to the user closing the menu.
	 */
	function initGlassNavHeader(root: HTMLElement) {
		if (root.dataset.gnhReady === "") return;

		const burger = root.querySelector<HTMLButtonElement>("[data-gnh-burger]");
		const menu = root.querySelector<HTMLElement>("[data-gnh-menu]");
		if (!burger || !menu) return;

		root.dataset.gnhReady = "";
		const controller = new AbortController();
		const { signal } = controller;
		const desktop = window.matchMedia("(min-width: 960px)");

		let open = false;

		function setMenu(next: boolean, returnFocus = false) {
			if (open === next) return;
			open = next;
			root.toggleAttribute("data-gnh-open", next);
			burger!.setAttribute("aria-expanded", String(next));
			if (!next && returnFocus) burger!.focus({ preventScroll: true });
		}

		burger.addEventListener("click", () => setMenu(!open), { signal });

		/* Choosing a destination closes the menu. */
		menu.addEventListener(
			"click",
			(event) => {
				if ((event.target as HTMLElement).closest("a[href]")) setMenu(false);
			},
			{ signal },
		);

		/* Escape on the document, not the root: after a client-side navigation
		   focus can sit outside the header and Escape must still close it. */
		document.addEventListener(
			"keydown",
			(event) => {
				if (event.key !== "Escape" || !open) return;
				event.preventDefault();
				setMenu(false, true);
			},
			{ signal },
		);

		/* Outside click closes. */
		document.addEventListener(
			"pointerdown",
			(event) => {
				if (open && !root.contains(event.target as Node)) setMenu(false);
			},
			{ signal },
		);

		/* Focus leaving the header entirely (Tab past the last link) closes it,
		   so an open card never hangs behind the focus. */
		root.addEventListener(
			"focusout",
			(event) => {
				const next = event.relatedTarget as Node | null;
				if (open && next && !root.contains(next)) setMenu(false);
			},
			{ signal },
		);

		/* Crossing to desktop resets: the menu must not be waiting open the next
		   time the viewport narrows. */
		desktop.addEventListener("change", () => setMenu(false), { signal });

		function destroy() {
			setMenu(false);
			controller.abort();
			delete root.dataset.gnhReady;
		}

		document.addEventListener("astro:before-swap", destroy, { once: true, signal });
	}

	const boot = () =>
		document.querySelectorAll<HTMLElement>("[data-gnh-root]").forEach(initGlassNavHeader);

	if (document.readyState === "loading") {
		document.addEventListener("DOMContentLoaded", boot, { once: true });
	} else {
		boot();
	}

	/* Registered once per module, so it cannot accumulate. */
	document.addEventListener("astro:page-load", boot);
</script>

===== END src/components/GlassNavHeader.astro =====

===== FILE (required): src/components/SignalTickerBar.astro =====
---
/**
 * SignalTickerBar
 *
 * The thin lime strip that runs across the very top of a page: uppercase,
 * tracked, heavy items separated by "+". Static by default, as measured.
 * `marquee` opts into a CSS-only scrolling loop that pauses on hover and
 * focus and never runs under reduced motion. No script.
 *
 * Reads --nv-* tokens from any ancestor, with fallbacks.
 */

export type SignalTickerItem = string | { label: string; href?: string };

export interface SignalTickerBarProps {
	items?: SignalTickerItem[];
	/** Opt-in scrolling loop. Off by default: the reference is static. */
	marquee?: boolean;
	/** Seconds for one full loop when `marquee` is on. */
	duration?: number;
	/** Accessible name of the strip's region landmark. */
	label?: string;
	/** Strip colour. Defaults to the measured ticker lime. */
	background?: string;
	/** Text colour. */
	color?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends SignalTickerBarProps {}

const {
	items = [],
	marquee = false,
	duration = 38,
	label = "Service highlights",
	background,
	color,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	id,
} = Astro.props;

/* The items are the caller's own claims; none are defaulted. An item with no
   label is dropped, and no items means no strip at all. */
const list = (items ?? [])
	.map((item) => (typeof item === "string" ? { label: item } : item))
	.filter((item) => item && typeof item.label === "string" && item.label.trim() !== "");

/* A marquee loop needs each group to be at least as wide as the viewport,
   or the seam shows as a gap. Estimate the group width from the copy (12px
   800 Manrope, tracked, runs about 8.6px per character) and repeat it
   inside the group until it clears a wide 1920px screen. Only the first
   copy of the first group is read out; every repeat is aria-hidden. */
const estWidth = list.reduce((w, i) => w + i.label.length * 8.6 + 34, 0) + 64;
const repeats = marquee ? Math.max(1, Math.ceil(1920 / Math.max(estWidth, 1))) : 1;

const style = [
	background ? `--stb-bg:${background}` : "",
	color ? `--stb-ink:${color}` : "",
	marquee ? `--stb-dur:${duration}s` : "",
]
	.filter(Boolean)
	.join(";");
---

{list.length > 0 && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{list.length > 0 && (
<section
	class:list={["stb", marquee && "stb--marquee"]}
	id={id}
	aria-label={label}
	style={style || undefined}
	data-stb-root
>
	<div class="stb-track">
		{
			Array.from({ length: marquee ? 2 : 1 }).map((_, g) => (
				<div class="stb-group" aria-hidden={g > 0 ? "true" : undefined}>
					{Array.from({ length: repeats }).map((_, r) => {
						const hidden = g > 0 || r > 0;
						return (
							<ul class="stb-list" role="list" aria-hidden={hidden && g === 0 ? "true" : undefined}>
								{list.map((item) => (
									<li class="stb-item">
										{item.href ? (
											<a class="stb-link" href={item.href} tabindex={hidden ? -1 : undefined}>
												{item.label}
											</a>
										) : (
											<span>{item.label}</span>
										)}
										<span class="stb-sep" aria-hidden="true">+</span>
									</li>
								))}
							</ul>
						);
					})}
				</div>
			))
		}
	</div>
</section>
)}

<style>
	.stb {
		--stb-bg-v: var(--stb-bg, var(--nv-lime, #b7ff39));
		--stb-ink-v: var(--stb-ink, var(--nv-ink, #08110f));

		box-sizing: border-box;
		position: relative;
		display: flex;
		align-items: center;
		width: 100%;
		height: 38px;
		overflow: hidden;
		background: var(--stb-bg-v);
		color: var(--stb-ink-v);
		font-family: var(--nv-font, Manrope, ui-sans-serif, system-ui, sans-serif);
	}

	.stb *,
	.stb *::before,
	.stb *::after {
		box-sizing: border-box;
	}

	.stb-track {
		display: flex;
		flex: none;
		align-items: center;
		width: max-content;
		padding-left: 20px;
	}

	.stb-group {
		display: flex;
		flex: none;
		align-items: center;
	}

	/* `.stb .stb-list`: beats any host `ul` reset or margin. */
	.stb .stb-list {
		display: flex;
		flex: none;
		align-items: center;
		margin: 0;
		/* The measured 64px between repeats of the message. */
		padding: 0 64px 0 0;
		list-style: none;
	}

	.stb-item {
		display: flex;
		align-items: center;
		white-space: nowrap;
		font-size: 12px;
		font-weight: 800;
		line-height: 14.4px;
		letter-spacing: 0.08em;
		text-transform: uppercase;
	}

	/* Two spaces either side of the "+" in the reference: about 0.7em at
	   this size and tracking. */
	.stb-sep {
		margin: 0 0.72em 0 0.62em;
	}

	.stb-link {
		color: inherit;
		text-decoration: none;
		border-radius: 3px;
	}

	.stb-link:hover {
		text-decoration: underline;
		text-underline-offset: 2px;
	}

	.stb-link:focus-visible {
		outline: 2px solid var(--stb-ink-v);
		outline-offset: 2px;
	}

	/* ---- marquee (opt-in) ---- */
	.stb--marquee .stb-track {
		padding-left: 0;
	}

	@media (prefers-reduced-motion: no-preference) {
		.stb--marquee .stb-track {
			animation: stb-loop var(--stb-dur, 38s) linear infinite;
		}

		.stb--marquee:hover .stb-track,
		.stb--marquee:focus-within .stb-track {
			animation-play-state: paused;
		}
	}

	/* Reduced motion: the marquee version sits still, starting where the
	   static strip does. */
	@media (prefers-reduced-motion: reduce) {
		.stb--marquee .stb-track {
			padding-left: 20px;
		}
	}

	@keyframes stb-loop {
		from {
			transform: translate3d(0, 0, 0);
		}
		to {
			transform: translate3d(-50%, 0, 0);
		}
	}

	/* ---- phone ---- */
	@media (max-width: 809.98px) {
		.stb {
			height: 36px;
		}

		.stb-item {
			font-size: 11px;
			line-height: 13.2px;
		}
	}

	@media (forced-colors: active) {
		.stb {
			border-bottom: 1px solid CanvasText;
		}
	}
</style>

===== END src/components/SignalTickerBar.astro =====

===== FILE (required): src/components/DashboardGlassHero.astro =====
---
/**
 * DashboardGlassHero
 *
 * An 860px full-bleed photo hero under a dark glass wash. Left: a pill
 * eyebrow, a four-line display headline whose alternate lines are lime, a
 * paragraph, two pill buttons, a row of tracked labels, a "network signal"
 * pill and a trust card with three round avatars. Right: a translucent glass
 * operations dashboard — status title, four stat cells, a four-up live
 * network row and an expandable forecast row.
 *
 * Layout: side by side from 1200px, dashboard stacked under the copy from
 * 810px, dashboard hidden below 810px (as the reference does on a phone).
 * Leaves 88px at the top for an overlay header.
 *
 * No script. The load-in is a CSS animation that ends at rest (so no-JS
 * shows everything) and is skipped under prefers-reduced-motion. The
 * forecast row is a native <details> disclosure.
 *
 * Reads --nv-* design tokens from any ancestor, with fallbacks.
 */

type IconName =
	| "shield"
	| "clock"
	| "leaf"
	| "box"
	| "road"
	| "ship"
	| "plane"
	| "warehouse";

export interface DashboardGlassHeroStat {
	label: string;
	value: string;
	note?: string;
	icon?: IconName;
	/** Any CSS colour. Defaults cycle lime, aqua, green, purple. */
	color?: string;
}

export interface DashboardGlassHeroNetworkItem {
	label: string;
	value: string;
	icon?: IconName;
	color?: string;
}

export interface DashboardGlassHeroProps {
	eyebrow?: string;
	/**
	 * One entry per line. Lines alternate white / lime, starting white.
	 * Trailing punctuation on a line is drawn white, as measured.
	 */
	headline?: string[];
	body?: string;
	primaryCta?: { label: string; href: string } | null;
	secondaryCta?: { label: string; href: string } | null;
	/** The tracked labels under the buttons, separated by lime dots. */
	labels?: string[];
	/** The "network signal" pill. "" drops it. */
	signal?: string;
	/**
	 * The trust card: real social proof only. Omit it (or pass `null`) and
	 * there is no card. Nothing in it has a default — no title, no line, no
	 * stars, no faces — because a claim nobody made is not a placeholder.
	 */
	trust?: {
		title?: string;
		text?: string;
		/** Five stars after the title. Only when `true`: a rating is never assumed. */
		stars?: boolean;
		avatars?: { src?: string; alt?: string }[];
	} | null;
	/** The photo behind everything. Omitted, or `src: ""`, renders a labelled placeholder. */
	background?: { src?: string; alt?: string };
	/**
	 * `null` drops the dashboard; the copy stays left-aligned. So does a
	 * dashboard with no eyebrow, title, stats, network or insight: nothing in
	 * it is filled in for you.
	 */
	dashboard?: {
		eyebrow?: string;
		title?: string;
		stats?: DashboardGlassHeroStat[];
		networkTitle?: string;
		network?: DashboardGlassHeroNetworkItem[];
		/**
		 * The forecast row. With `detail` it is a disclosure that reveals the
		 * detail; with `href` it is a link; with neither it is static text.
		 */
		insight?: { label: string; text: string; detail?: string; href?: string } | null;
	} | null;
	/** Plays the load-in rise. Skipped under reduced motion regardless. */
	animate?: boolean;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends DashboardGlassHeroProps {}

/* No copy, figures, photo or links of its own: the catalog demo passes them
   through the registry's props, so a copied hero never ships an invented
   business or an invented number. */
const {
	eyebrow = "",
	headline = [],
	body = "",
	primaryCta = null,
	secondaryCta = null,
	labels = [],
	signal = "",
	trust = null,
	background = {},
	dashboard = {},
	animate = true,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	id,
} = Astro.props;

/* Only the photo passed. */
const bg = { src: background?.src ?? "", alt: background?.alt ?? "" };

/* A button needs both halves: a label with no destination, or the reverse, is not drawn. */
const link = (l: { label: string; href: string } | null | undefined) => (l && l.label && l.href ? l : null);
const primary = link(primaryCta);
const secondary = link(secondaryCta);

/*
 * Only what the caller supplied. A card with nothing in it is not drawn, and
 * stars appear only on an explicit `stars: true` - the component never
 * manufactures a rating, a customer count or a face.
 */
const trustData =
	trust && (trust.title || trust.text || (trust.avatars && trust.avatars.length > 0))
		? { title: trust.title, text: trust.text, stars: trust.stars === true, avatars: trust.avatars ?? [] }
		: null;

const TONES = ["#c8ff38", "#73f5df", "#73ef80", "#a88cff"];

/*
 * The dashboard shows only what the caller passed: no status line, figure,
 * network count or forecast is ever filled in, so an invented 98.6% never
 * sits beside real numbers. The network heading is generic chrome and is
 * the one thing that still merges. A dashboard with nothing in it is not
 * drawn.
 */
const dash = dashboard
	? {
			networkTitle: "Live network",
			...dashboard,
		}
	: null;
const showDash =
	!!dash && !!(dash.eyebrow || dash.title || (dash.stats && dash.stats.length > 0) || (dash.network && dash.network.length > 0) || dash.insight);

const uid = id ?? `dgh-${Math.random().toString(36).slice(2, 8)}`;
const headingId = `${uid}-title`;
const insightId = `${uid}-insight`;

/* Split trailing punctuation off a headline line so it can be drawn white. */
const lines = headline.map((line, i) => {
	const m = line.match(/^(.*?)([.!?,;:…]+)$/);
	return { word: m ? m[1] : line, punct: m ? m[2] : "", accent: i % 2 === 1 };
});

const ICONS: Record<IconName, string> = {
	shield: '<path d="M12 3l7 3v5.2c0 4.3-2.9 7.8-7 9.8-4.1-2-7-5.5-7-9.8V6l7-3z"/><path d="M9 12l2 2 4-4"/>',
	clock: '<circle cx="12" cy="12" r="8"/><path d="M12 8v4l2.8 1.8"/>',
	leaf: '<path d="M5 19c0-8 6-14 14-14 0 8-6 14-14 14z"/><path d="M5 19l7.5-7.5"/>',
	box: '<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3z"/><path d="M4 7.5l8 4.5 8-4.5M12 12v9"/>',
	road: '<path d="M9 3L5 21M15 3l4 18M12 4v2.5M12 10.5v3M12 17.5v3"/>',
	ship: '<path d="M3 15.5l2.2 4.5h13.6l2.2-4.5H3z"/><path d="M6 15.5V10h12v5.5M9.5 10V6.5h5V10M12 3.5v3"/>',
	plane: '<path d="M10.5 20l1.5-.8 1.5.8M12 3.5c.8 0 1.3.7 1.3 1.6V10l7.2 4v1.8l-7.2-2.1v4.3l2 1.5V20L12 19.2 9.2 20v-.5l2-1.5v-4.3L4 15.8V14l7.2-4V5.1c0-.9.5-1.6.8-1.6z"/>',
	warehouse: '<path d="M3 21V9l9-5 9 5v12"/><path d="M7 21v-8h10v8M7 17h10"/>',
};
const iconFor = (name: IconName | undefined, fallback: IconName) => ICONS[name ?? fallback] ?? ICONS[fallback];
const STAT_FALLBACK: IconName[] = ["shield", "clock", "leaf", "box"];
const NET_FALLBACK: IconName[] = ["road", "ship", "plane", "warehouse"];

const arrow = '<path d="M7 17L17 7M8.5 7H17v8.5"/>';

/* A photo alone is not a hero: with no copy, link, proof or dashboard, render nothing. */
const hasUtility = labels.length > 0 || !!signal || !!trustData;
const hasContent = !!eyebrow || lines.length > 0 || !!body || !!primary || !!secondary || hasUtility || showDash;
---

{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<section
	class:list={["dgh", { "dgh--animate": animate }]}
	id={id}
	aria-labelledby={lines.length > 0 ? headingId : undefined}
>
	<div class="dgh-bg">
		{
			bg.src ? (
				<img
					src={bg.src}
					alt={bg.alt ?? ""}
					width="2000"
					height="1200"
					loading="eager"
					fetchpriority="high"
					decoding="async"
				/>
			) : (
				<div class="dgh-ph" role="img" aria-label="Image placeholder: wide landscape photo">
					<div class="dgh-ph-text" aria-hidden="true">
						<span class="dgh-ph-label">Background photo</span>
						<span class="dgh-ph-hint">Wide landscape photo</span>
						<span class="dgh-ph-spec">5:3 · 2000 × 1200 px</span>
					</div>
				</div>
			)
		}
	</div>
	<div class="dgh-wash" aria-hidden="true"></div>

	<div class="dgh-inner">
		<div class="dgh-copy">
			{
				eyebrow && (
					<p class="dgh-eyebrow">
						<span class="dgh-dot" aria-hidden="true" />
						{eyebrow}
					</p>
				)
			}
			{lines.length > 0 && (
			<h1 class="dgh-title" id={headingId}>
				{
					lines.map((l) => (
						<span class:list={["dgh-line", { "dgh-line--accent": l.accent }]}>
							{l.word}{l.punct && <span class="dgh-punct">{l.punct}</span>}
						</span>
					))
				}
			</h1>
			)}
			{body && <p class="dgh-body">{body}</p>}
			{
				(primary || secondary) && (
					<div class="dgh-ctas">
						{primary && (
							<a class="dgh-btn dgh-btn--primary" href={primary.href}>
								<span>{primary.label}</span>
								<svg class="dgh-btn-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false" set:html={arrow} />
							</a>
						)}
						{secondary && (
							<a class="dgh-btn dgh-btn--secondary" href={secondary.href}>
								<span>{secondary.label}</span>
								<svg class="dgh-btn-icon dgh-btn-icon--sm" viewBox="0 0 24 24" aria-hidden="true" focusable="false" set:html={arrow} />
							</a>
						)}
					</div>
				)
			}
			{hasUtility && (
			<div class="dgh-utility">
				{
					labels.length > 0 && (
						<ul class="dgh-labels">
							{labels.map((label) => (
								<li>{label}</li>
							))}
						</ul>
					)
				}
				{
					signal && (
						<p class="dgh-signal">
							<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
								<circle cx="12" cy="12" r="1.8" fill="currentColor" stroke="none" />
								<path class="dgh-wave dgh-wave--1" d="M8.6 8.6a4.8 4.8 0 0 0 0 6.8M15.4 8.6a4.8 4.8 0 0 1 0 6.8" />
								<path class="dgh-wave dgh-wave--2" d="M5.8 5.8a8.8 8.8 0 0 0 0 12.4M18.2 5.8a8.8 8.8 0 0 1 0 12.4" />
							</svg>
							<span>{signal}</span>
						</p>
					)
				}
				{
					trustData && (
						<div class="dgh-trust">
							{trustData.avatars && trustData.avatars.length > 0 && (
								<ul class="dgh-avatars">
									{trustData.avatars.map((a) => (
										<li class="dgh-avatar">
											{a.src ? (
												<img src={a.src} alt={a.alt ?? ""} width="34" height="34" loading="eager" decoding="async" />
											) : (
												<span class="dgh-avatar-ph" role="img" aria-label="Avatar placeholder: customer headshot, square 160 × 160 px" />
											)}
										</li>
									))}
								</ul>
							)}
							<div class="dgh-trust-copy">
								{(trustData.title || trustData.stars) && (
									<p class="dgh-trust-title">
										{trustData.title}
										{trustData.stars && <span class="dgh-stars" aria-hidden="true">★★★★★</span>}
									</p>
								)}
								{trustData.text && <p class="dgh-trust-text">{trustData.text}</p>}
							</div>
						</div>
					)
				}
			</div>
			)}
		</div>

		{
			showDash && dash && (
				<div class="dgh-dash">
					<span class="dgh-glow dgh-glow--lime" aria-hidden="true" />
					<span class="dgh-glow dgh-glow--aqua" aria-hidden="true" />
					<div class="dgh-dash-head">
						{dash.eyebrow && (
							<p class="dgh-dash-eyebrow">
								<span class="dgh-dot dgh-dot--sm" aria-hidden="true" />
								{dash.eyebrow}
							</p>
						)}
						<svg class="dgh-pulse" viewBox="0 0 224 27" aria-hidden="true" focusable="false">
							<path
								class="dgh-pulse-line"
								pathLength="1"
								d="M1 15.5H58l4-6 4.5 11 4-16 4.5 18 3.5-7H116l3.5-4 3.5 8 4.5-12.5 4 14.5 3.5-6H176l4.5-8.5 4 11.5 3.5-3H219"
							/>
							<circle class="dgh-pulse-dot" cx="219" cy="15.5" r="2.6" />
						</svg>
					</div>
					{dash.title && <h2 class="dgh-dash-title">{dash.title}</h2>}

					{dash.stats && dash.stats.length > 0 && (
						<ul class="dgh-stats">
							{dash.stats.map((s, i) => (
								<li class="dgh-stat" style={`--dgh-tone: ${s.color ?? TONES[i % TONES.length]};`}>
									<span class="dgh-stat-icon" aria-hidden="true">
										<svg viewBox="0 0 24 24" focusable="false" set:html={iconFor(s.icon, STAT_FALLBACK[i % 4])} />
									</span>
									<span class="dgh-stat-label">{s.label}</span>
									<strong class="dgh-stat-value">{s.value}</strong>
									{s.note && <span class="dgh-stat-note">{s.note}</span>}
								</li>
							))}
						</ul>
					)}

					{dash.network && dash.network.length > 0 && (
						<div class="dgh-net">
							{dash.networkTitle && <h3 class="dgh-net-title">{dash.networkTitle}</h3>}
							<ul class="dgh-net-grid">
								{dash.network.map((n, i) => (
									<li class="dgh-net-item" style={`--dgh-tone: ${n.color ?? TONES[i % TONES.length]};`}>
										<svg class="dgh-net-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false" set:html={iconFor(n.icon, NET_FALLBACK[i % 4])} />
										<strong class="dgh-net-value">{n.value}</strong>
										<span class="dgh-net-label">{n.label}</span>
									</li>
								))}
							</ul>
						</div>
					)}

					{dash.insight &&
						(dash.insight.detail && !dash.insight.href ? (
							<details class="dgh-insight">
								<summary class="dgh-insight-row">
									<span class="dgh-insight-tag">
										<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
											<path d="M12 3v5M12 16v5M3 12h5M16 12h5M6.3 6.3l2.5 2.5M15.2 15.2l2.5 2.5M6.3 17.7l2.5-2.5M15.2 8.8l2.5-2.5" />
										</svg>
										{dash.insight.label}
									</span>
									<span class="dgh-insight-text">{dash.insight.text}</span>
									<span class="dgh-insight-chev" aria-hidden="true">
										<svg viewBox="0 0 24 24" focusable="false"><path d="M9.5 6l6 6-6 6" /></svg>
									</span>
								</summary>
								<p class="dgh-insight-detail" id={insightId}>{dash.insight.detail}</p>
							</details>
						) : dash.insight.href ? (
							<a class="dgh-insight-row dgh-insight-row--link" href={dash.insight.href}>
								<span class="dgh-insight-tag">
									<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
										<path d="M12 3v5M12 16v5M3 12h5M16 12h5M6.3 6.3l2.5 2.5M15.2 15.2l2.5 2.5M6.3 17.7l2.5-2.5M15.2 8.8l2.5-2.5" />
									</svg>
									{dash.insight.label}
								</span>
								<span class="dgh-insight-text">{dash.insight.text}</span>
								<span class="dgh-insight-chev" aria-hidden="true">
									<svg viewBox="0 0 24 24" focusable="false"><path d="M9.5 6l6 6-6 6" /></svg>
								</span>
							</a>
						) : (
							<div class="dgh-insight-row dgh-insight-row--static">
								<span class="dgh-insight-tag">{dash.insight.label}</span>
								<span class="dgh-insight-text">{dash.insight.text}</span>
							</div>
						))}
				</div>
			)
		}
	</div>
</section>
)}

<style>
	.dgh {
		--dgh-ink: var(--nv-ink, #08110f);
		--dgh-lime: var(--nv-lime, #c8ff38);
		--dgh-text: #f7faf7;
		--dgh-font: var(--nv-font, Manrope, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
		--dgh-ui: var(--nv-font-ui, Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
		--dgh-ease: cubic-bezier(0.22, 1, 0.36, 1);

		position: relative;
		box-sizing: border-box;
		display: flex;
		flex-direction: column;
		justify-content: center;
		min-height: 800px;
		padding: 96px 20px 56px;
		overflow: hidden;
		isolation: isolate;
		background: var(--dgh-ink);
		color: var(--dgh-text);
		font-family: var(--dgh-font);
	}

	.dgh *,
	.dgh *::before,
	.dgh *::after {
		box-sizing: border-box;
	}

	/* Zero-specificity resets, so every class below wins over them. */
	:where(.dgh) :where(p, h1, h2, h3, ul) {
		margin: 0;
	}

	:where(.dgh) :where(ul) {
		padding: 0;
		list-style: none;
	}

	/* ---- background ---- */

	.dgh-bg {
		position: absolute;
		inset: 0;
		z-index: -2;
	}

	.dgh-bg img {
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
		object-position: center 70%;
	}

	/* The measured wash runs 0.25 → 0.08 left to right. The default dusk photo
	   is already dark, so the wash sits close to that: a little deeper behind
	   the copy column (0.42) for white type, near-clear on the right where the
	   glass dashboard carries its own contrast. See README → Approximations. */
	.dgh-wash {
		position: absolute;
		inset: 0;
		z-index: -1;
		background:
			linear-gradient(180deg, rgb(6 16 13 / 0.2) 0%, rgb(6 16 13 / 0) 26%, rgb(6 16 13 / 0) 70%, rgb(6 16 13 / 0.35) 100%),
			linear-gradient(90deg, rgb(6 16 13 / 0.42) 0%, rgb(6 16 13 / 0.25) 38%, rgb(6 16 13 / 0.08) 100%);
		-webkit-backdrop-filter: blur(1px);
		backdrop-filter: blur(1px);
	}

	.dgh-ph {
		display: grid;
		place-items: end;
		width: 100%;
		height: 100%;
		padding: 20px 24px;
		text-align: right;
		background: repeating-linear-gradient(135deg, #1a2522 0 12px, #202d29 12px 24px);
		color: rgb(247 250 247 / 0.6);
	}

	.dgh-ph-text {
		display: flex;
		flex-direction: column;
		align-items: flex-end;
		gap: 6px;
	}

	.dgh-ph-label {
		font: 600 12px/1 var(--dgh-ui);
		letter-spacing: 0.08em;
		text-transform: uppercase;
	}

	.dgh-ph-hint {
		max-width: 30ch;
		font: 500 14px/1.4 var(--dgh-ui);
	}

	.dgh-ph-spec {
		font: 400 12px/1.2 var(--dgh-ui);
	}

	/* ---- layout ---- */

	.dgh-inner {
		display: flex;
		flex-direction: column;
		gap: 28px;
		width: 100%;
		max-width: 1310px;
		margin: 0 auto;
	}

	.dgh-copy {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		width: 100%;
		max-width: 490px;
		min-width: 0;
	}

	/* ---- copy ---- */

	.dgh-eyebrow {
		display: inline-flex;
		align-items: center;
		gap: 8px;
		min-height: 34px;
		max-width: 100%;
		padding: 7px 15px;
		border-radius: 999px;
		background: rgb(8 17 15 / 0.54);
		-webkit-backdrop-filter: blur(10px);
		backdrop-filter: blur(10px);
		color: var(--dgh-lime);
		font: 800 12px/14.4px var(--dgh-font);
		letter-spacing: 0.72px;
		text-transform: uppercase;
	}

	.dgh-dot {
		flex: none;
		width: 8px;
		height: 8px;
		border-radius: 999px;
		background: var(--dgh-lime);
	}

	.dgh-dot--sm {
		width: 6px;
		height: 6px;
	}

	.dgh-title {
		margin-top: 26px;
		color: #fff;
		font-family: var(--dgh-font);
		font-weight: 800;
		font-size: clamp(44px, 16.4vw, 62px);
		line-height: 1.02;
		letter-spacing: -0.055em;
	}

	.dgh-line {
		display: block;
	}

	.dgh-line--accent {
		color: var(--dgh-lime);
	}

	.dgh-punct {
		color: #fff;
	}

	.dgh-body {
		margin-top: 28px;
		color: rgb(255 255 255 / 0.78);
		font: 400 16px/24.8px var(--dgh-font);
	}

	.dgh-ctas {
		display: flex;
		flex-direction: column;
		gap: 12px;
		width: 100%;
		margin-top: 32px;
	}

	.dgh-btn {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: 10px;
		min-height: 56px;
		padding: 0 22px;
		border-radius: 999px;
		font: 800 15px/18px var(--dgh-font);
		text-decoration: none;
		transition:
			transform 0.3s var(--dgh-ease),
			background-color 0.3s var(--dgh-ease);
	}

	.dgh-btn--primary {
		background: var(--dgh-lime);
		color: var(--dgh-ink);
	}

	.dgh-btn--secondary {
		background: rgb(16 27 24 / 0.92);
		-webkit-backdrop-filter: blur(12px);
		backdrop-filter: blur(12px);
		color: #f7fafc;
	}

	.dgh-btn:hover {
		transform: scale(1.1) translateY(-2px);
	}

	.dgh-btn:focus-visible {
		outline: 2px solid var(--dgh-lime);
		outline-offset: 3px;
	}

	.dgh-btn--primary:focus-visible {
		outline-color: #fff;
	}

	.dgh-btn-icon {
		flex: none;
		width: 18px;
		height: 18px;
		fill: none;
		stroke: currentColor;
		stroke-width: 2;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	.dgh-btn-icon--sm {
		width: 16px;
		height: 16px;
	}

	.dgh-utility {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 14px;
		width: 100%;
		margin-top: 28px;
	}

	.dgh-labels {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: 6px 7px;
		color: #fff;
		font: 700 12px/14.4px var(--dgh-font);
		letter-spacing: 0.6px;
		text-transform: uppercase;
	}

	.dgh-labels li {
		display: inline-flex;
		align-items: center;
		gap: 7px;
	}

	.dgh-labels li + li::before {
		content: "";
		width: 5px;
		height: 5px;
		border-radius: 999px;
		background: var(--dgh-lime);
	}

	.dgh-signal {
		display: inline-flex;
		align-items: center;
		gap: 10px;
		min-height: 38px;
		padding: 10px 14px;
		border-radius: 999px;
		background: rgb(8 17 15 / 0.66);
		-webkit-backdrop-filter: blur(16px);
		backdrop-filter: blur(16px);
		color: #f7fafc;
		font: 800 10px/12px var(--dgh-font);
		letter-spacing: 0.7px;
		text-transform: uppercase;
	}

	.dgh-signal svg {
		flex: none;
		width: 16px;
		height: 16px;
		color: var(--dgh-lime);
		fill: none;
		stroke: currentColor;
		stroke-width: 1.8;
		stroke-linecap: round;
	}

	.dgh-trust {
		display: flex;
		align-items: center;
		gap: 8px;
		width: 100%;
		max-width: 410px;
		min-height: 66px;
		padding: 10px 12px;
		border-radius: 18px;
		background: rgb(8 17 15 / 0.58);
		-webkit-backdrop-filter: blur(14px);
		backdrop-filter: blur(14px);
	}

	.dgh-avatars {
		display: flex;
		flex: none;
		gap: 8px;
	}

	.dgh-avatar img,
	.dgh-avatar-ph {
		display: block;
		width: 34px;
		height: 34px;
		border-radius: 999px;
		object-fit: cover;
	}

	.dgh-avatar-ph {
		background: repeating-linear-gradient(135deg, #2b3934 0 5px, #34443e 5px 10px);
		box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.18);
	}

	.dgh-trust-copy {
		display: flex;
		flex-direction: column;
		gap: 2px;
		min-width: 0;
	}

	.dgh-trust-title {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: 0 7px;
		color: #f7fafc;
		font: 700 10px/12px var(--dgh-font);
		letter-spacing: 0.5px;
		text-transform: uppercase;
	}

	.dgh-stars {
		color: var(--dgh-lime);
		font: 700 9px/11px var(--dgh-ui);
		letter-spacing: 1.32px;
	}

	.dgh-trust-text {
		color: #f7fafc;
		font: 600 11px/13.2px var(--dgh-font);
	}

	/* ---- dashboard ---- */

	.dgh-dash {
		position: relative;
		display: none;
		flex-direction: column;
		width: 100%;
		max-width: 668px;
		min-height: 478px;
		padding: 20px;
		overflow: hidden;
		isolation: isolate;
		border: 0.67px solid rgb(255 255 255 / 0.18);
		border-radius: 22px;
		background-color: rgb(8 15 13 / 0.66);
		background-image: linear-gradient(145deg, rgb(255 255 255 / 0.075), rgb(255 255 255 / 0.02));
		box-shadow:
			0 22px 63px rgb(0 0 0 / 0.42),
			inset 0 1px 0 rgb(255 255 255 / 0.1),
			inset 0 -1px 0 rgb(255 255 255 / 0.024);
		-webkit-backdrop-filter: blur(18px) saturate(1.35);
		backdrop-filter: blur(18px) saturate(1.35);
		opacity: 0.98;
		font-family: var(--dgh-ui);
	}

	.dgh-glow {
		position: absolute;
		z-index: -1;
		border-radius: 50%;
		pointer-events: none;
	}

	.dgh-glow--lime {
		top: -153px;
		right: -97px;
		width: 252px;
		height: 252px;
		background: radial-gradient(circle, rgb(200 255 56 / 0.19), transparent 68%);
	}

	.dgh-glow--aqua {
		bottom: -146px;
		left: -62px;
		width: 203px;
		height: 203px;
		background: radial-gradient(circle, rgb(115 245 223 / 0.13), transparent 70%);
	}

	.dgh-dash-head {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 17px;
		min-height: 42px;
		padding-bottom: 15px;
	}

	.dgh-dash-eyebrow {
		display: flex;
		align-items: center;
		gap: 7px;
		color: var(--dgh-lime);
		font: 800 8px/1.2 var(--dgh-ui);
		letter-spacing: 0.96px;
		text-transform: uppercase;
		white-space: nowrap;
	}

	.dgh-pulse {
		flex: 0 1 224px;
		min-width: 0;
		height: 27px;
		filter: drop-shadow(0 0 5px rgb(200 255 56 / 0.36));
	}

	.dgh-pulse-line {
		fill: none;
		stroke: var(--dgh-lime);
		stroke-width: 1.3;
		stroke-linejoin: round;
		stroke-linecap: round;
	}

	.dgh-pulse-dot {
		fill: var(--dgh-lime);
	}

	.dgh-dash-title {
		color: var(--dgh-text);
		font: 750 32px/32px var(--dgh-ui);
		letter-spacing: -1.44px;
	}

	.dgh-stats {
		display: grid;
		grid-template-columns: repeat(4, minmax(0, 1fr));
		margin-top: 18px;
		border-top: 1px solid rgb(255 255 255 / 0.08);
		border-bottom: 1px solid rgb(255 255 255 / 0.08);
	}

	.dgh-stat {
		display: flex;
		flex-direction: column;
		align-items: center;
		padding: 17px 13px 15px;
		text-align: center;
	}

	.dgh-stat + .dgh-stat,
	.dgh-net-item + .dgh-net-item {
		border-left: 1px solid rgb(255 255 255 / 0.08);
	}

	.dgh-stat-icon {
		display: grid;
		place-items: center;
		width: 45px;
		height: 45px;
		border: 0.67px solid rgb(255 255 255 / 0.11);
		border-radius: 50%;
		background: rgb(255 255 255 / 0.035);
		box-shadow: inset 0 0 17px rgb(255 255 255 / 0.024);
		color: var(--dgh-tone);
	}

	.dgh-stat-icon svg,
	.dgh-net-icon {
		width: 20px;
		height: 20px;
		fill: none;
		stroke: currentColor;
		stroke-width: 1.7;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	.dgh-stat-label {
		display: flex;
		align-items: center;
		justify-content: center;
		min-height: 24px;
		margin-top: 12px;
		color: rgb(247 250 247 / 0.62);
		font: 750 8px/11.2px var(--dgh-ui);
		letter-spacing: 0.64px;
		text-transform: uppercase;
	}

	.dgh-stat-value {
		margin-top: 6px;
		color: var(--dgh-tone);
		font: 820 20px/20px var(--dgh-ui);
		letter-spacing: -0.7px;
	}

	.dgh-stat-note {
		margin-top: 4px;
		color: rgb(247 250 247 / 0.72);
		font: 400 8px/10.8px var(--dgh-ui);
	}

	.dgh-net {
		padding-top: 18px;
	}

	.dgh-net-title {
		color: rgb(247 250 247 / 0.72);
		font: 800 8px/10px var(--dgh-ui);
		letter-spacing: 0.88px;
		text-transform: uppercase;
	}

	.dgh-net-grid {
		display: grid;
		grid-template-columns: repeat(4, minmax(0, 1fr));
		margin-top: 13px;
	}

	.dgh-net-item {
		display: flex;
		flex-direction: column;
		align-items: center;
		padding: 0 13px 15px;
		text-align: center;
	}

	.dgh-net-icon {
		width: 24px;
		height: 24px;
		color: var(--dgh-tone);
	}

	.dgh-net-value {
		margin-top: 10px;
		color: var(--dgh-text);
		font: 800 19px/19px var(--dgh-ui);
		letter-spacing: -0.665px;
	}

	.dgh-net-label {
		margin-top: 5px;
		color: rgb(247 250 247 / 0.62);
		font: 400 8px/10px var(--dgh-ui);
	}

	/* ---- forecast row ---- */

	.dgh-insight {
		margin-top: 0;
	}

	.dgh-insight-row {
		display: grid;
		grid-template-columns: auto minmax(0, 1fr) 24px;
		align-items: center;
		gap: 10px;
		min-height: 44px;
		padding: 8px 10px 8px 8px;
		border: 0.67px solid rgb(255 255 255 / 0.11);
		border-radius: 999px;
		background: rgb(255 255 255 / 0.035);
		color: inherit;
		text-decoration: none;
		cursor: pointer;
		list-style: none;
		transition:
			border-color 0.18s ease,
			background-color 0.18s ease,
			transform 0.18s ease;
	}

	.dgh-insight-row::-webkit-details-marker {
		display: none;
	}

	.dgh-insight-row--static {
		grid-template-columns: auto minmax(0, 1fr);
		cursor: default;
	}

	.dgh-insight-row:not(.dgh-insight-row--static):hover {
		background: rgb(255 255 255 / 0.07);
		transform: translateX(1px);
	}

	.dgh-insight-row:focus-visible {
		outline: 2px solid var(--dgh-lime);
		outline-offset: 2px;
	}

	.dgh-insight-tag {
		display: inline-flex;
		align-items: center;
		gap: 5px;
		min-height: 24px;
		padding: 0 8px;
		border-radius: 999px;
		background: rgb(200 255 56 / 0.08);
		color: var(--dgh-lime);
		font: 850 8px/1 var(--dgh-ui);
		letter-spacing: 0.56px;
		text-transform: uppercase;
		white-space: nowrap;
	}

	.dgh-insight-tag svg {
		width: 11px;
		height: 11px;
		fill: none;
		stroke: currentColor;
		stroke-width: 2;
		stroke-linecap: round;
	}

	.dgh-insight-text {
		color: rgb(247 250 247 / 0.78);
		font: 400 8px/11.6px var(--dgh-ui);
	}

	.dgh-insight-chev {
		display: grid;
		place-items: center;
		width: 24px;
		height: 24px;
		border-radius: 50%;
		color: var(--dgh-text);
		transition: transform 0.18s ease;
	}

	.dgh-insight-chev svg {
		width: 13px;
		height: 13px;
		fill: none;
		stroke: currentColor;
		stroke-width: 2;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	.dgh-insight[open] .dgh-insight-chev {
		transform: rotate(90deg);
	}

	.dgh-insight-detail {
		padding: 10px 16px 0;
		color: rgb(247 250 247 / 0.72);
		font: 400 10px/15px var(--dgh-ui);
	}

	/* ---- load-in ----
	   A CSS animation, not a script: it always finishes at rest, so without
	   JavaScript the content still ends visible, and nothing is hidden when
	   motion is reduced because the animation never applies. */
	@media (prefers-reduced-motion: no-preference) {
		.dgh--animate .dgh-copy {
			animation: dgh-rise 0.5s var(--dgh-ease) both;
		}

		.dgh--animate .dgh-dash {
			animation: dgh-rise 0.6s var(--dgh-ease) 0.12s both;
		}

		.dgh--animate .dgh-pulse-line {
			stroke-dasharray: 1;
			animation: dgh-draw 1.4s ease-out 0.35s both;
		}

		.dgh-wave--1 {
			animation: dgh-wave 2.4s ease-in-out infinite;
		}

		.dgh-wave--2 {
			animation: dgh-wave 2.4s ease-in-out 0.3s infinite;
		}
	}

	@keyframes dgh-rise {
		from {
			opacity: 0;
			transform: translateY(18px);
		}
		to {
			opacity: 1;
			transform: none;
		}
	}

	@keyframes dgh-draw {
		from {
			stroke-dashoffset: 1;
		}
		to {
			stroke-dashoffset: 0;
		}
	}

	@keyframes dgh-wave {
		0%,
		100% {
			opacity: 1;
		}
		50% {
			opacity: 0.35;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.dgh-btn:hover,
		.dgh-insight-row:not(.dgh-insight-row--static):hover {
			transform: none;
		}

		.dgh-insight-chev {
			transition: none;
		}
	}

	/* Phone: the reference keeps only the first two labels on one line
	   rather than wrapping a third under them. */
	@media (max-width: 809.98px) {
		.dgh-labels li:nth-child(n + 3) {
			display: none;
		}
	}

	/* ---- tablet: dashboard stacks under the copy ---- */
	@media (min-width: 810px) {
		.dgh {
			min-height: 860px;
			padding: 88px 40px 92px;
		}

		.dgh-inner {
			gap: 48px;
		}

		.dgh-title {
			font-size: 66px;
			line-height: 61.38px;
			letter-spacing: -3.6px;
		}

		.dgh-ctas {
			flex-direction: row;
			flex-wrap: wrap;
			gap: 16px;
			width: auto;
		}

		.dgh-labels {
			gap: 10px;
		}

		.dgh-labels li {
			gap: 10px;
		}

		.dgh-dash {
			display: flex;
		}
	}

	/* ---- desktop: side by side ---- */
	@media (min-width: 1200px) {
		/* Measured: the copy starts 168px down and the dashboard 267px down
		   in an 860px hero. The overlay header occupies the top 78px. */
		.dgh {
			padding: 151px 64px 29px;
		}

		.dgh-inner {
			flex-direction: row;
			align-items: center;
			justify-content: space-between;
			gap: 42px;
		}

		.dgh-copy {
			flex: 0 0 490px;
		}

		.dgh-dash {
			flex: 0 1 668px;
			margin-top: 32px;
		}
	}
</style>

===== END src/components/DashboardGlassHero.astro =====

===== FILE (required): public/dashboard-glass-hero/desert-highway.webp =====
[binary asset — download https://astro.baysixmedia.com/dashboard-glass-hero/desert-highway.webp and save it to public/dashboard-glass-hero/desert-highway.webp]

===== FILE (required): public/dashboard-glass-hero/avatar-1.webp =====
[binary asset — download https://astro.baysixmedia.com/dashboard-glass-hero/avatar-1.webp and save it to public/dashboard-glass-hero/avatar-1.webp]

===== FILE (required): public/dashboard-glass-hero/avatar-2.webp =====
[binary asset — download https://astro.baysixmedia.com/dashboard-glass-hero/avatar-2.webp and save it to public/dashboard-glass-hero/avatar-2.webp]

===== FILE (required): public/dashboard-glass-hero/avatar-3.webp =====
[binary asset — download https://astro.baysixmedia.com/dashboard-glass-hero/avatar-3.webp and save it to public/dashboard-glass-hero/avatar-3.webp]

===== FILE (required): src/components/PartnerChipBand.astro =====
---
/**
 * PartnerChipBand
 *
 * A dark ink band of partner "logos": a lime-dot eyebrow, a centred heading
 * and a row of rounded chips, each tinted in its own hue at 0.08 alpha and
 * carrying a small glyph, a hue dot and a text wordmark. Two soft glow blobs
 * sit in the corners.
 *
 * Hover lifts a chip (scale 1.02, -4px) and doubles its tint to 0.16.
 * The chips fade up 16px, staggered, the first time the band scrolls into
 * view. The hidden starting pose only applies once the script has marked the
 * root ready, so without JavaScript or with reduced motion every chip is at
 * rest from the start.
 *
 * Reads --nv-* design tokens from any ancestor, with fallbacks.
 */

type Glyph = "peak" | "star" | "bars" | "orbit" | "diamond" | "wave";

export interface PartnerChip {
	name: string;
	/** Chip hue as "r g b" or any CSS colour. Tint and dot derive from it. */
	hue?: string;
	glyph?: Glyph;
	/** Optional link. Without one the chip is not interactive. */
	href?: string;
}

export interface PartnerChipBandProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3" | "h4";
	partners?: PartnerChip[];
	/** Accessible name of the chip list. */
	listLabel?: string;
	/** The two corner glow blobs. */
	glow?: boolean;
	/** The staggered fade-up on first view. Skipped under reduced motion. */
	appear?: boolean;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends PartnerChipBandProps {}

const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	partners: rawPartners = [],
	listLabel = "Partners",
	glow = true,
	appear = true,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	id,
} = Astro.props;

const Heading = headingLevel;
/* A chip without a name is dropped. No partners, no band: the heading alone would be a claim. */
const partners = rawPartners.filter((p) => p?.name);

const GLYPHS: Record<Glyph, string> = {
	peak: '<path d="M4 16L10 5l6 11h-3.2L10 10.6 7.2 16z"/>',
	star: '<path d="M10 3.5l1.9 4.3 4.6.4-3.5 3 1.1 4.6L10 13.4l-4.1 2.4 1.1-4.6-3.5-3 4.6-.4z"/>',
	bars: '<path d="M4 15V9h2.6v6zM8.7 15V5h2.6v10zM13.4 15v-4H16v4z"/>',
	orbit: '<path d="M10 4a6 6 0 1 0 0 12 6 6 0 0 0 0-12zm0 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/>',
	diamond: '<path d="M10 3.5l6.5 6.5-6.5 6.5L3.5 10z"/>',
	wave: '<path d="M3.5 8.2c2.2-1.8 4.3-1.8 6.5 0s4.3 1.8 6.5 0v3c-2.2 1.8-4.3 1.8-6.5 0s-4.3-1.8-6.5 0zM3.5 13.4c2.2-1.8 4.3-1.8 6.5 0s4.3 1.8 6.5 0v2.2c-2.2 1.8-4.3 1.8-6.5 0s-4.3-1.8-6.5 0z"/>',
};
const GLYPH_ORDER: Glyph[] = ["peak", "star", "bars", "orbit", "diamond", "wave"];
const HUES = ["115 245 223", "200 255 56", "168 140 255", "255 184 107", "110 168 255", "247 143 179"];

/* "r g b" triplets get the measured alpha tints; any other colour is mixed. */
const hueStyle = (hue: string, i: number) => {
	const rgb = /^\d{1,3}\s+\d{1,3}\s+\d{1,3}$/.test(hue.trim());
	const solid = rgb ? `rgb(${hue})` : hue;
	const tint = (a: number) =>
		rgb ? `rgb(${hue} / ${a})` : `color-mix(in srgb, ${hue} ${a * 100}%, transparent)`;
	return `--pcb-hue: ${solid}; --pcb-tint: ${tint(0.08)}; --pcb-tint-hover: ${tint(0.16)}; --pcb-edge: ${tint(0.16)}; --pcb-i: ${i};`;
};
---

{partners.length > 0 && (
<>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section
	class="pcb"
	id={id}
	data-pcb-root
	data-pcb-appear={appear ? "1" : "0"}
>
	{
		glow && (
			<>
				<span class="pcb-glow pcb-glow--aqua" aria-hidden="true" />
				<span class="pcb-glow pcb-glow--violet" aria-hidden="true" />
			</>
		)
	}
	<div class="pcb-inner">
		<div class="pcb-head">
			{
				eyebrow && (
					<p class="pcb-eyebrow">
						<span class="pcb-dot" aria-hidden="true" />
						{eyebrow}
					</p>
				)
			}
			{heading && <Heading class="pcb-heading">{heading}</Heading>}
		</div>
		{
			partners.length > 0 && (
				<ul class="pcb-chips" aria-label={listLabel}>
					{partners.map((p, i) => {
						const inner = (
							<>
								<span class="pcb-glyph" aria-hidden="true">
									<svg viewBox="0 0 20 20" focusable="false" set:html={GLYPHS[p.glyph ?? GLYPH_ORDER[i % 6]]} />
								</span>
								<span class="pcb-hue-dot" aria-hidden="true" />
								<span class="pcb-name">{p.name}</span>
							</>
						);
						return (
							<li class="pcb-chip" style={hueStyle(p.hue ?? HUES[i % 6], i)}>
								{p.href ? (
									<a class="pcb-chip-body pcb-chip-body--link" href={p.href}>
										{inner}
									</a>
								) : (
									<span class="pcb-chip-body">{inner}</span>
								)}
							</li>
						);
					})}
				</ul>
			)
		}
	</div>
</section>
</>
)}

<style>
	.pcb {
		--pcb-ink: var(--nv-ink, #08110f);
		--pcb-lime: var(--nv-lime, #c8ff38);
		--pcb-text: #f7fafc;
		--pcb-font: var(--nv-font, Manrope, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
		--pcb-ease: cubic-bezier(0.22, 1, 0.36, 1);

		position: relative;
		box-sizing: border-box;
		overflow: hidden;
		isolation: isolate;
		padding: 64px 20px;
		background: var(--pcb-ink);
		color: var(--pcb-text);
		font-family: var(--pcb-font);
	}

	.pcb *,
	.pcb *::before,
	.pcb *::after {
		box-sizing: border-box;
	}

	:where(.pcb) :where(p, h2, h3, h4, ul) {
		margin: 0;
	}

	.pcb-glow {
		position: absolute;
		z-index: -1;
		border-radius: 50%;
		pointer-events: none;
	}

	.pcb-glow--aqua {
		top: -150px;
		left: -150px;
		width: 460px;
		height: 460px;
		background: radial-gradient(50% 50%, rgb(115 245 223 / 0.28) 0%, rgb(115 245 223 / 0) 70%);
	}

	.pcb-glow--violet {
		top: 47px;
		right: -150px;
		width: 500px;
		height: 500px;
		background: radial-gradient(50% 50%, rgb(168 140 255 / 0.26) 0%, rgb(168 140 255 / 0) 70%);
	}

	.pcb-inner {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 30px;
		max-width: 1337px;
		margin: 0 auto;
	}

	.pcb-head {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 12px;
		max-width: 680px;
		text-align: center;
	}

	.pcb-eyebrow {
		display: inline-flex;
		align-items: center;
		gap: 8px;
		color: rgb(247 250 252 / 0.72);
		font: 700 11px/13.2px var(--pcb-font);
		letter-spacing: 1.32px;
		text-transform: uppercase;
	}

	.pcb-dot {
		flex: none;
		width: 10px;
		height: 10px;
		border-radius: 999px;
		background: var(--pcb-lime);
		opacity: 0.64;
	}

	.pcb-heading {
		color: var(--pcb-text);
		font: 700 34px/36.72px var(--pcb-font);
		letter-spacing: -1.2px;
	}

	.pcb-chips {
		display: flex;
		flex-wrap: wrap;
		justify-content: center;
		gap: 14px;
		padding: 0;
		list-style: none;
	}

	.pcb-chip {
		flex: 0 0 180px;
		/* The appear pose lives on `translate`, the hover on `transform`, so
		   the two never fight over one property. */
		transition:
			opacity 0.6s var(--pcb-ease) calc(var(--pcb-i, 0) * 80ms),
			translate 0.6s var(--pcb-ease) calc(var(--pcb-i, 0) * 80ms);
	}

	.pcb-chip-body {
		display: flex;
		align-items: center;
		justify-content: center;
		gap: 10px;
		height: 100px;
		padding: 0 18px;
		border-radius: 18px;
		background-color: var(--pcb-tint);
		box-shadow: inset 0 0 0 1px var(--pcb-edge);
		color: inherit;
		text-decoration: none;
		transition:
			transform 0.3s var(--pcb-ease),
			background-color 0.3s var(--pcb-ease);
	}

	.pcb-chip-body:hover {
		transform: scale(1.02) translateY(-4px);
		background-color: var(--pcb-tint-hover);
	}

	.pcb-chip-body--link:focus-visible {
		outline: 2px solid var(--pcb-lime);
		outline-offset: 3px;
		background-color: var(--pcb-tint-hover);
	}

	.pcb-glyph {
		display: grid;
		flex: none;
		place-items: center;
		width: 20px;
		height: 20px;
		border-radius: 3px;
		background: #f7f8f6;
		color: var(--pcb-ink);
	}

	.pcb-glyph svg {
		width: 16px;
		height: 16px;
		fill: currentColor;
	}

	.pcb-hue-dot {
		flex: none;
		width: 10px;
		height: 10px;
		border-radius: 999px;
		background: var(--pcb-hue);
	}

	.pcb-name {
		color: var(--pcb-text);
		font: 700 16px/16px var(--pcb-font);
		letter-spacing: 0.64px;
		white-space: nowrap;
	}

	/* Appear: only once the script is running and motion is allowed. */
	.pcb[data-pcb-ready] .pcb-chip {
		opacity: 0;
		translate: 0 16px;
	}

	.pcb[data-pcb-ready][data-pcb-in] .pcb-chip {
		opacity: 1;
		translate: 0 0;
	}

	@media (prefers-reduced-motion: no-preference) {
		.pcb-dot {
			animation: pcb-pulse 2.4s ease-in-out infinite;
		}
	}

	@keyframes pcb-pulse {
		0%,
		100% {
			opacity: 1;
			transform: scale(1);
		}
		50% {
			opacity: 0.55;
			transform: scale(1.36);
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.pcb-chip-body:hover {
			transform: none;
		}

		.pcb-chip {
			transition: none;
		}
	}

	/* Phone: the violet glow drops to the foot of the stacked column, as in
	   the reference. */
	@media (max-width: 809.98px) {
		.pcb-glow--violet {
			top: auto;
			right: auto;
			bottom: -200px;
			left: calc(50% - 280px);
		}
	}

	@media (min-width: 810px) {
		.pcb {
			padding: 64px 44px;
		}
	}
</style>

<script>
	/**
	 * PartnerChipBand controller: the staggered appear, once per instance.
	 *
	 * Sets data-pcb-ready (which applies the hidden pose) only when it can also
	 * reveal the chips, then data-pcb-in when the band enters the viewport.
	 */
	function initPartnerChipBand(root: HTMLElement): void {
		if (root.dataset.pcbBooted === "") return;
		if (root.dataset.pcbAppear !== "1") return;
		if (!("IntersectionObserver" in window)) return;
		if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

		root.dataset.pcbBooted = "";
		const controller = new AbortController();

		const observer = new IntersectionObserver(
			(entries) => {
				if (entries.some((e) => e.isIntersecting)) {
					root.dataset.pcbIn = "";
					observer.disconnect();
				}
			},
			{ threshold: 0.2 },
		);

		root.dataset.pcbReady = "";
		observer.observe(root);
		controller.signal.addEventListener("abort", () => observer.disconnect());

		document.addEventListener(
			"astro:before-swap",
			() => {
				controller.abort();
				delete root.dataset.pcbBooted;
				delete root.dataset.pcbReady;
				delete root.dataset.pcbIn;
			},
			{ once: true, signal: controller.signal },
		);
	}

	const boot = () =>
		document.querySelectorAll<HTMLElement>("[data-pcb-root]").forEach(initPartnerChipBand);

	if (document.readyState === "loading") {
		document.addEventListener("DOMContentLoaded", boot, { once: true });
	} else {
		boot();
	}

	document.addEventListener("astro:page-load", boot);
</script>

===== END src/components/PartnerChipBand.astro =====

===== FILE (required): src/components/KpiCardStrip.astro =====
---
/**
 * KpiCardStrip
 *
 * A pale blue-grey band with a lime-dot eyebrow, a heading and a scoreboard:
 * one white panel divided by hairlines into metric cells. Each cell carries a
 * short accent bar, one big number, a label and a caption. Four across from
 * 1000px of the section's own width, two by two from 560px, and on phones one
 * row per metric with the number beside its words.
 *
 * Numbers only as supplied: a card needs a label and a value, or it is not
 * drawn, and with no cards the section renders nothing.
 *
 * Static: no script, no hover, no motion. Reads --nv-* design tokens from any
 * ancestor, with fallbacks.
 */

export interface KpiCard {
	/** Short label for the number. */
	label: string;
	/** The big figure, as displayed: "120+", "2.4M", "98.7%". */
	value: string;
	/** One line under the label. */
	caption?: string;
	/** Any CSS colour for the cell's accent bar. */
	accent?: string;
}

export interface KpiCardStripProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3" | "h4";
	cards?: KpiCard[];
	/** The band's surface. */
	background?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends KpiCardStripProps {}

const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	cards = [],
	background = "#dde8f0",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	id,
} = Astro.props;

const Heading = headingLevel;
const list = Array.isArray(cards) ? cards.filter((c) => c && c.label && c.value) : [];
---

{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{list.length > 0 && (
<section class="kcs" id={id} style={`--kcs-bg: ${background};`}>
	<div class="kcs-inner">
		{
			(eyebrow || heading) && (
				<header class="kcs-head">
					{eyebrow && (
						<p class="kcs-eyebrow">
							<span class="kcs-dot" aria-hidden="true" />
							{eyebrow}
						</p>
					)}
					{heading && <Heading class="kcs-heading">{heading}</Heading>}
				</header>
			)
		}
		<dl class:list={["kcs-board", { "kcs-board--odd": list.length % 2 === 1 }]} style={`--kcs-n: ${Math.min(list.length, 4)};`}>
			{list.map((card) => (
				<div class="kcs-cell" style={card.accent ? `--kcs-accent: ${card.accent};` : undefined}>
					<dt class="kcs-label">{card.label}</dt>
					<dd class="kcs-value">
						<span class="kcs-bar" aria-hidden="true" />
						{card.value}
					</dd>
					{card.caption && <dd class="kcs-caption">{card.caption}</dd>}
				</div>
			))}
		</dl>
	</div>
</section>
)}

<style>
	.kcs {
		--kcs-font: var(--nv-font, Manrope, Inter, ui-sans-serif, system-ui, sans-serif);
		--kcs-lime: var(--nv-lime, #c8ff38);
		--kcs-ink: #071a2a;
		--kcs-soft: #4a6076;
		--kcs-rule: #d7e1ea;

		container-type: inline-size;
		box-sizing: border-box;
		background: var(--kcs-bg);
		font-family: var(--kcs-font);
	}

	.kcs *,
	.kcs *::before,
	.kcs *::after {
		box-sizing: border-box;
	}

	.kcs-inner {
		display: flex;
		flex-direction: column;
		gap: 28px;
		max-width: 1240px;
		margin: 0 auto;
		padding: 64px 20px;
	}

	.kcs-head {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 12px;
	}

	.kcs-eyebrow {
		display: flex;
		align-items: center;
		gap: 10px;
		margin: 0;
		font: 700 14px/1.3 var(--kcs-font);
		color: #34506a;
	}

	.kcs-dot {
		flex: none;
		width: 9px;
		height: 9px;
		border-radius: 50%;
		background: var(--kcs-lime);
		box-shadow: 0 0 0 2px var(--kcs-ink);
	}

	.kcs-heading {
		max-width: 20ch;
		margin: 0;
		font: 800 clamp(30px, 6.4cqi, 46px) / 1.04 var(--kcs-font);
		letter-spacing: -0.035em;
		color: #0a1c2a;
		text-wrap: balance;
	}

	/* ---- the scoreboard: one panel, cells split by hairlines ---- */

	.kcs-board {
		display: grid;
		margin: 0;
		overflow: hidden;
		border: 1px solid var(--kcs-rule);
		border-radius: 18px;
		background: #fff;
	}

	/* Phones: one row per metric — the number beside its words. */
	.kcs-cell {
		display: grid;
		grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
		grid-template-rows: auto 1fr;
		column-gap: 16px;
		align-items: start;
		min-width: 0;
		padding: 22px 20px;
	}

	.kcs-cell + .kcs-cell {
		border-top: 1px solid var(--kcs-rule);
	}

	/* The label comes first in the markup (dt before dd); the number is shown first. */
	.kcs-value {
		grid-column: 1;
		grid-row: 1 / span 2;
		margin: 0;
		font: 800 clamp(34px, 9cqi, 64px) / 0.95 var(--kcs-font);
		letter-spacing: -0.045em;
		font-variant-numeric: tabular-nums;
		color: var(--kcs-ink);
		overflow-wrap: anywhere;
	}

	.kcs-bar {
		display: block;
		width: 28px;
		height: 4px;
		margin-bottom: 14px;
		border-radius: 2px;
		background: var(--kcs-accent, var(--kcs-lime));
	}

	.kcs-label {
		grid-column: 2;
		grid-row: 1;
		margin: 18px 0 0;
		font: 700 15px/1.3 var(--kcs-font);
		color: var(--kcs-ink);
		overflow-wrap: anywhere;
	}

	.kcs-caption {
		grid-column: 2;
		grid-row: 2;
		margin: 4px 0 0;
		font: 500 14px/1.45 var(--kcs-font);
		color: var(--kcs-soft);
		overflow-wrap: anywhere;
	}

	/* ---- tablet: two by two ---- */

	@container (min-width: 560px) {
		.kcs-inner {
			gap: 36px;
			padding: 80px 32px;
		}

		.kcs-board {
			grid-template-columns: repeat(2, minmax(0, 1fr));
		}

		.kcs-cell {
			display: flex;
			flex-direction: column;
			padding: 30px 30px 32px;
		}

		/* Two by two: only the second cell loses the phone layout's top rule. (Sibling selectors outrank :nth-child, so both sides use them.) */
		.kcs-cell + .kcs-cell:nth-child(2) {
			border-top: 0;
		}

		.kcs-cell:nth-child(even) {
			border-left: 1px solid var(--kcs-rule);
		}

		/* An odd last metric takes the full row. */
		.kcs-board--odd .kcs-cell:last-child {
			grid-column: 1 / -1;
		}

		.kcs-value {
			order: -1;
		}

		.kcs-label {
			margin-top: 20px;
		}
	}

	/* ---- desktop: all across ---- */

	@container (min-width: 1000px) {
		.kcs-inner {
			padding: 104px 40px;
		}

		.kcs-head {
			flex-direction: row;
			flex-wrap: wrap;
			align-items: flex-end;
			justify-content: space-between;
			column-gap: 48px;
		}

		.kcs-eyebrow {
			order: 2;
		}

		.kcs-board {
			grid-template-columns: repeat(var(--kcs-n), minmax(0, 1fr));
		}

		.kcs-cell {
			padding: 36px 32px 38px;
		}

		.kcs-cell + .kcs-cell {
			border-top: 0;
			border-left: 1px solid var(--kcs-rule);
		}

		/* Five or more: the board wraps into a second row of four. */
		.kcs-cell + .kcs-cell:nth-child(n + 5) {
			border-top: 1px solid var(--kcs-rule);
		}

		.kcs-cell + .kcs-cell:nth-child(4n + 1) {
			border-left: 0;
		}

		.kcs-board--odd .kcs-cell:last-child {
			grid-column: auto;
		}
	}

	@media (forced-colors: active) {
		.kcs-board {
			border-color: CanvasText;
		}

		.kcs-bar,
		.kcs-dot {
			background: CanvasText;
		}
	}
</style>

===== END src/components/KpiCardStrip.astro =====

===== FILE (required): src/components/ColorServiceGrid.astro =====
---
/**
 * ColorServiceGrid
 *
 * A light services section: lime-dot eyebrow, a big centred heading, a short
 * paragraph, then a grid of tall rounded cards, each in its own colour. Every
 * card has an icon tile, a huge faded index number in the top-right corner, a
 * title, a line of text and an "Explore service" link along the bottom. Any
 * card can carry a photo, which sits in its lower-right area behind a
 * gradient so the copy stays legible.
 *
 * The whole card is one link target (a stretched link). Hover lifts the card
 * and gives it a soft shadow in its own hue.
 *
 * Three across from 1200px, two from 810px, one below. Static: no script.
 *
 * Reads --nv-* design tokens from any ancestor, with fallbacks.
 */

export type ServiceTone = "ink" | "purple" | "orange" | "aqua" | "lime" | "green";
export type ServiceIcon = "truck" | "ship" | "plane" | "warehouse" | "bike" | "shield";

export interface ServiceCard {
	title: string;
	text?: string;
	href: string;
	/** The card colour. Picks background, text, number, tile and shadow hue together. */
	tone?: ServiceTone;
	icon?: ServiceIcon;
	/** Link label. Defaults to the section's `ctaLabel`. */
	ctaLabel?: string;
	/**
	 * Photo in the card's lower-right area. `src: ""` renders the labelled
	 * placeholder instead. Omit for a plain colour card.
	 */
	image?: { src?: string; alt?: string };
}

export interface ColorServiceGridProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	intro?: string;
	cards?: ServiceCard[];
	/** Default link label on every card. */
	ctaLabel?: string;
	/** Show the 01–06 index numbers. */
	numbered?: boolean;
	background?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends ColorServiceGridProps {}

const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	intro = "",
	cards = [],
	ctaLabel = "Explore service",
	numbered = true,
	background = "#eef2eb",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	id,
} = Astro.props;

const Heading = headingLevel;
const CardHeading = headingLevel === "h2" ? "h3" : "h4";
const TONES = new Set(["ink", "purple", "orange", "aqua", "lime", "green"]);
const list = (Array.isArray(cards) ? cards : []).filter((c) => c && c.title && c.href);

/* 24x24, 1.8 stroke, round caps. Drawn for this library. */
const ICONS: Record<string, string> = {
	truck:
		'<path d="M2.5 6.5h11v9h-11z"/><path d="M13.5 9.5h4l3 3v3h-7"/><circle cx="7" cy="17.5" r="1.8"/><circle cx="17" cy="17.5" r="1.8"/>',
	ship:
		'<path d="M3 14.5l1.8 4.2c2.4 1 4.8 1 7.2 0 2.4 1 4.8 1 7.2 0l1.8-4.2-9-3.5z"/><path d="M6 12.3V8h12v4.3"/><path d="M10 8V4.5h4V8"/>',
	plane:
		'<path d="M20.5 3.5c-1-1-2.6-.9-3.6.1l-3.4 3.6-8-2.4-1.9 1.9 6.5 3.8-3.3 3.5-2.6-.5-1.5 1.5 3.4 1.6 1.6 3.4 1.5-1.5-.5-2.6 3.5-3.3 3.8 6.5 1.9-1.9-2.4-8 3.6-3.4c1-1 1.1-2.6.1-3.6z"/>',
	warehouse:
		'<path d="M3 20V9l9-5 9 5v11"/><path d="M7 20v-8h10v8"/><path d="M7 15h10M7 18h10"/>',
	bike:
		'<circle cx="6" cy="16.5" r="3.5"/><circle cx="18" cy="16.5" r="3.5"/><path d="M6 16.5l4-7h5l3 7M10 9.5l2.5 7h-6.5"/><circle cx="14" cy="5" r="1.4"/>',
	shield:
		'<path d="M12 3l7.5 3v5.5c0 4.6-3.2 8.2-7.5 9.5-4.3-1.3-7.5-4.9-7.5-9.5V6z"/><path d="M8.8 12.2l2.3 2.3 4.3-4.6"/>',
};

const uid = id ?? `csg-${Math.random().toString(36).slice(2, 9)}`;
---

{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{list.length > 0 && (
<section class="csg" id={id} style={`--csg-bg: ${background};`} aria-labelledby={heading ? `${uid}-h` : undefined}>
	<div class="csg-inner">
		{
			(eyebrow || heading || intro) && (
				<header class="csg-intro">
					{eyebrow && (
						<p class="csg-eyebrow">
							<span class="csg-dot" aria-hidden="true" />
							{eyebrow}
						</p>
					)}
					{heading && <Heading class="csg-heading" id={`${uid}-h`}>{heading}</Heading>}
					{intro && <p class="csg-lede">{intro}</p>}
				</header>
			)
		}

		{
			list.length > 0 && (
				<ul class="csg-grid" role="list">
					{list.map((card, i) => {
						const tone = card.tone && TONES.has(card.tone) ? card.tone : "ink";
						const icon = card.icon && ICONS[card.icon] ? ICONS[card.icon] : null;
						const img = card.image;
						return (
							<li class:list={["csg-card", `csg-card--${tone}`, { "csg-card--photo": !!img }]}>
								{img && (
									<div class="csg-photo">
										{img.src ? (
											<img src={img.src} alt={img.alt ?? ""} width="720" height="960" loading="lazy" decoding="async" />
										) : (
											<div class="csg-ph" role="img" aria-label={`Image placeholder: ${img.alt || card.title} photo`}>
												<span class="csg-ph-text" aria-hidden="true">
													<span class="csg-ph-label">Image</span>
													<span>{img.alt || `${card.title} photo`}</span>
													<span class="csg-ph-spec">3:4 · 720 × 960 px</span>
												</span>
											</div>
										)}
									</div>
								)}
								{img && <span class="csg-scrim" aria-hidden="true" />}
								{numbered && (
									<span class="csg-num" aria-hidden="true">
										{String(i + 1).padStart(2, "0")}
									</span>
								)}
								<div class="csg-copy">
									{icon && (
										<span class="csg-tile" aria-hidden="true">
											<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" focusable="false" set:html={icon} />
										</span>
									)}
									<CardHeading class="csg-title">{card.title}</CardHeading>
									{card.text && <p class="csg-text">{card.text}</p>}
								</div>
								<a class="csg-link" href={card.href}>
									<span>{card.ctaLabel ?? ctaLabel}</span>
									<span class="csg-sr">: {card.title}</span>
									<svg class="csg-arrow" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
										<path d="M5 11l6-6M6 5h5v5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
									</svg>
								</a>
							</li>
						);
					})}
				</ul>
			)
		}
	</div>
</section>
)}

<style>
	.csg {
		--csg-ink: var(--nv-ink, #08110f);
		--csg-font: var(--nv-font, Manrope, Inter, ui-sans-serif, system-ui, sans-serif);
		--csg-lime: var(--nv-lime, #c8ff38);
		--csg-ease: cubic-bezier(0.22, 1, 0.36, 1);
		--csg-dur: 0.3s;

		box-sizing: border-box;
		padding: 112px 20px 120px;
		background: var(--csg-bg);
		font-family: var(--csg-font);
		color: var(--csg-ink);
		overflow: clip;
	}

	.csg *,
	.csg *::before,
	.csg *::after {
		box-sizing: border-box;
	}

	.csg-inner {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 54px;
		max-width: 1160px;
		margin: 0 auto;
	}

	.csg-intro {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 18px;
		max-width: 780px;
		text-align: center;
	}

	.csg-eyebrow {
		display: flex;
		align-items: center;
		gap: 9px;
		margin: 0;
		font: 700 12px/14.4px var(--csg-font);
		letter-spacing: 1.44px;
		text-transform: uppercase;
		color: var(--csg-ink);
	}

	.csg-dot {
		flex: none;
		width: 8px;
		height: 8px;
		border-radius: 50%;
		background: var(--csg-lime);
		box-shadow: 0 0 0 6px rgb(200 255 56 / 0.18);
	}

	.csg-heading {
		max-width: 760px;
		margin: 0;
		font-weight: 700;
		font-size: clamp(44px, 16vw, 62px);
		line-height: 0.95;
		letter-spacing: -0.055em;
		color: var(--csg-ink);
		text-wrap: balance;
	}

	.csg-lede {
		max-width: 610px;
		margin: 0;
		font: 400 17px/27.2px var(--csg-font);
		color: var(--nv-muted, #5b6c65);
		text-wrap: pretty;
	}

	/* The phone reference holds the lede to ~310px, narrower than the cards. */
	@media (max-width: 809px) {
		.csg-lede {
			padding-inline: 20px;
		}
	}

	.csg-grid {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: 18px;
		width: 100%;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	/* ---- the card ---- */

	.csg-card {
		/* Tone defaults: ink card. Each tone below overrides these. */
		--csg-card-bg: #08110f;
		--csg-card-fg: #fff;
		--csg-card-muted: rgb(255 255 255 / 0.72);
		--csg-card-num: rgb(255 255 255 / 0.13);
		--csg-card-tile: rgb(200 255 56 / 0.18);
		--csg-card-icon: #c8ff38;
		--csg-card-link: #fff;
		--csg-card-arrow: #c8ff38;
		--csg-card-hue: 8 17 15;

		position: relative;
		display: flex;
		flex-direction: column;
		justify-content: space-between;
		align-items: flex-start;
		gap: 32px;
		min-height: 350px;
		min-width: 0;
		padding: 30px;
		border-radius: 28px;
		background: var(--csg-card-bg);
		color: var(--csg-card-fg);
		overflow: clip;
		isolation: isolate;
		box-shadow: 0 20px 46px 0 rgb(var(--csg-card-hue) / 0);
		transition:
			transform var(--csg-dur) var(--csg-ease),
			box-shadow var(--csg-dur) var(--csg-ease);
	}

	.csg-card--purple {
		--csg-card-bg: #9d8cff;
		--csg-card-hue: 157 140 255;
		--csg-card-tile: rgb(255 255 255 / 0.24);
	}

	.csg-card--orange {
		--csg-card-bg: #ff9a62;
		--csg-card-hue: 255 154 98;
		--csg-card-tile: rgb(255 255 255 / 0.27);
	}

	.csg-card--aqua {
		--csg-card-bg: #73f5df;
		--csg-card-hue: 115 245 223;
		--csg-card-tile: rgb(255 255 255 / 0.3);
	}

	.csg-card--lime {
		--csg-card-bg: #c8ff38;
		--csg-card-hue: 200 255 56;
		--csg-card-tile: rgb(8 17 15 / 0.1);
	}

	.csg-card--purple,
	.csg-card--orange,
	.csg-card--aqua,
	.csg-card--lime {
		--csg-card-fg: #08110f;
		--csg-card-muted: rgb(8 17 15 / 0.74);
		--csg-card-num: rgb(8 17 15 / 0.1);
		--csg-card-icon: #08110f;
		--csg-card-link: #000;
		--csg-card-arrow: #000;
	}

	.csg-card--green {
		--csg-card-bg: #163c33;
		--csg-card-hue: 22 60 51;
		--csg-card-num: rgb(255 255 255 / 0.11);
		--csg-card-tile: rgb(255 154 98 / 0.18);
		--csg-card-icon: #ffb487;
		--csg-card-link: #ffb487;
		--csg-card-arrow: #ffb487;
	}

	.csg-card:hover {
		transform: translateY(-7px) scale(1.01);
		box-shadow: 0 20px 46px 0 rgb(var(--csg-card-hue) / 0.26);
	}

	.csg-photo {
		position: absolute;
		/* Measured on a 375x350 card: 186px in, 89px down, 217x287,
		   running off the right and bottom edges. */
		inset: 89px -28px -26px 49.6%;
		z-index: -2;
		border-top-left-radius: 180px;
		overflow: hidden;
		opacity: 0.78;
	}

	.csg-photo img {
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	.csg-scrim {
		position: absolute;
		inset: 0;
		z-index: -1;
		background: linear-gradient(
			90deg,
			rgb(8 17 15 / 0.96) 0%,
			rgb(8 17 15 / 0.62) 55%,
			rgb(8 17 15 / 0.04) 100%
		);
		pointer-events: none;
	}

	.csg-ph {
		display: grid;
		place-items: center;
		width: 100%;
		height: 100%;
		padding: 40px 36px 36px 44px;
		text-align: center;
		background: repeating-linear-gradient(135deg, #1d2a27 0 12px, #243330 12px 24px);
		color: rgb(255 255 255 / 0.7);
	}

	.csg-ph-text {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 6px;
		font: 500 12px/1.35 var(--csg-font);
	}

	.csg-ph-label {
		letter-spacing: 0.08em;
		text-transform: uppercase;
	}

	.csg-ph-spec {
		font-weight: 400;
		opacity: 0.8;
	}

	.csg-num {
		position: absolute;
		top: 16px;
		right: 25px;
		z-index: -1;
		font: 700 72px/72px var(--csg-font);
		letter-spacing: -5.04px;
		color: var(--csg-card-num);
		pointer-events: none;
		user-select: none;
	}

	.csg-copy {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 16px;
		max-width: 315px;
	}

	.csg-tile {
		display: grid;
		place-items: center;
		width: 56px;
		height: 56px;
		border-radius: 18px;
		background: var(--csg-card-tile);
		color: var(--csg-card-icon);
	}

	.csg-tile svg {
		width: 25px;
		height: 25px;
	}

	.csg-title {
		margin: 0;
		font: 700 31px/32.55px var(--csg-font);
		letter-spacing: -1.395px;
		color: var(--csg-card-fg);
	}

	.csg-text {
		margin: 0;
		font: 400 14px/22.12px var(--csg-font);
		color: var(--csg-card-muted);
	}

	/* Stretched link: the ::after covers the whole card, so the card is one
	   target while the accessible name stays on the link itself. */
	.csg-link {
		display: inline-flex;
		align-items: center;
		gap: 8px;
		min-height: 24px;
		font: 700 13px/15.6px var(--csg-font);
		color: var(--csg-card-link);
		text-decoration: none;
		outline: none;
	}

	.csg-link::after {
		content: "";
		position: absolute;
		inset: 0;
		z-index: 1;
		border-radius: 28px;
	}

	/* The ring goes on the card, not the pseudo-element: the card clips its
	   overflow, which would cut an outline drawn by anything inside it. */
	.csg-card:has(.csg-link:focus-visible) {
		outline: 3px solid var(--csg-ink);
		outline-offset: 4px;
	}

	/* Fallback for a browser without :has(). */
	.csg-link:focus-visible > span:first-child {
		text-decoration: underline;
		text-decoration-thickness: 2px;
		text-underline-offset: 3px;
	}

	.csg-arrow {
		flex: none;
		width: 16px;
		height: 16px;
		color: var(--csg-card-arrow);
	}

	.csg-sr {
		position: absolute;
		width: 1px;
		height: 1px;
		margin: -1px;
		padding: 0;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
		border: 0;
	}

	/* ---- tablet: two across ---- */
	@media (min-width: 810px) {
		.csg {
			padding-inline: 40px;
		}

		.csg-grid {
			grid-template-columns: repeat(2, minmax(0, 1fr));
		}
	}

	/* ---- desktop: three across ---- */
	@media (min-width: 1200px) {
		.csg-grid {
			grid-template-columns: repeat(3, minmax(0, 1fr));
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.csg-card {
			transition: box-shadow var(--csg-dur) var(--csg-ease);
		}

		.csg-card:hover {
			transform: none;
		}
	}

	@media (forced-colors: active) {
		.csg-card {
			border: 1px solid CanvasText;
		}

		.csg-card:has(.csg-link:focus-visible) {
			outline-color: Highlight;
		}
	}
</style>

===== END src/components/ColorServiceGrid.astro =====

===== FILE (required): public/color-service-grid/road-freight.webp =====
[binary asset — download https://astro.baysixmedia.com/color-service-grid/road-freight.webp and save it to public/color-service-grid/road-freight.webp]

===== FILE (required): src/components/ControlTowerShowcase.astro =====
---
/**
 * ControlTowerShowcase
 *
 * A dark product section. Left: lime-dot eyebrow, a huge white heading, a
 * paragraph, a list of features with lime check discs, and a lime pill link.
 * Right: a light, rounded dashboard mock — a header with a timestamp and a
 * status pill, a photo tile, and a stack of three white KPI tiles (a small
 * bar chart, a figure with a week-on-week change, and a plain figure).
 *
 * Appear on scroll: the feature rows slide in from the left and the KPI tiles
 * rise and scale in, both staggered, once. The hidden starting state is only
 * applied after the script has marked the root ready, so with no script or
 * under reduced motion everything renders at rest.
 *
 * Two columns from 1200px, stacked below. Reads --nv-* tokens, with fallbacks.
 */

export interface ControlTowerShowcaseProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	body?: string;
	features?: string[];
	cta?: { label: string; href: string } | null;
	dashboard?: {
		title?: string;
		/** Static text. Not a live clock. */
		timestamp?: string;
		/** Status pill text. `""` drops the pill. */
		status?: string;
		/** Photo tile. `src: ""` renders the labelled placeholder. `null` drops the tile. */
		image?: { src?: string; alt?: string } | null;
		onTime?: { label: string; value: string; bars?: number[]; highlight?: number[] };
		exceptions?: { label: string; value: string; delta?: string };
		carbon?: { label: string; value: string };
	};
	/** The scroll-in motion. `false` renders everything at rest. */
	animate?: boolean;
	background?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends ControlTowerShowcaseProps {}

/*
 * No demo dashboard and nothing merged into one: the title, timestamp,
 * status, photo and every figure are drawn only when the caller passes them.
 * A KPI needs both a label and a value; a partial one is not drawn — never an
 * invented 98.7% beside real figures, and never one on a page that passed
 * none. (The catalog demo passes its own dashboard through the registry.)
 */
const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	body = "",
	features = [],
	cta = null,
	dashboard = {},
	animate = true,
	background = "#08110f",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	id,
} = Astro.props;

const filled = (v: unknown): v is string => typeof v === "string" && v.trim() !== "";
const stat = <T extends { label?: string; value?: string }>(s: T | undefined | null) =>
	s && filled(s.label) && filled(s.value) ? s : undefined;

const raw = dashboard ?? {};
const d = {
	title: raw.title,
	timestamp: raw.timestamp,
	status: raw.status,
	onTime: stat(raw.onTime),
	exceptions: stat(raw.exceptions),
	carbon: stat(raw.carbon),
};
const image = raw.image ? { src: raw.image.src ?? "", alt: raw.image.alt ?? "" } : null;
const hasKpis = Boolean(d.onTime || d.exceptions || d.carbon);
/* The dashboard needs something in its body — a photo or a figure. A title,
   timestamp or status pill alone does not draw an empty panel. */
const showDash = Boolean(image) || hasKpis;
const bars = (d.onTime?.bars ?? []).filter((n) => Number.isFinite(n) && n > 0);
const barMax = Math.max(1, ...bars);
const highlight = new Set(d.onTime?.highlight ?? []);
const Heading = headingLevel;
const featureList = Array.isArray(features) ? features.filter(filled) : [];
const link = cta && filled(cta.label) && filled(cta.href) ? cta : null;
/* The eyebrow alone never renders the section. */
const hasContent = filled(heading) || filled(body) || featureList.length > 0 || Boolean(link) || showDash;
---

{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<section class="cts" id={id} style={`--cts-bg: ${background};`} data-cts-root data-cts-animate={animate ? "1" : "0"}>
	<div class="cts-grid">
		<div class="cts-content">
			{
				eyebrow && (
					<p class="cts-eyebrow">
						<span class="cts-dot" aria-hidden="true" />
						{eyebrow}
					</p>
				)
			}
			{heading && <Heading class="cts-heading">{heading}</Heading>}
			{body && <p class="cts-body">{body}</p>}
			{
				featureList.length > 0 && (
					<ul class="cts-features" role="list" data-cts-group>
						{featureList.map((f, i) => (
							<li class="cts-feature" style={`--cts-i:${i}`}>
								<span class="cts-check" aria-hidden="true">
									<svg viewBox="0 0 16 16" focusable="false">
										<path d="M3.5 8.4l2.9 2.9 6-6.3" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
									</svg>
								</span>
								<span>{f}</span>
							</li>
						))}
					</ul>
				)
			}
			{
				link && (
					<a class="cts-cta" href={link.href}>
						<span>{link.label}</span>
						<svg viewBox="0 0 18 18" aria-hidden="true" focusable="false">
							<path d="M3.5 9h11M10 4.5L14.5 9 10 13.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
						</svg>
					</a>
				)
			}
		</div>

		{showDash && (
		<div class="cts-visual">
			<div class="cts-dash" role="group" aria-label={d.title ? `${d.title} dashboard (illustration)` : "Dashboard illustration"}>
				<div class="cts-dash-head">
					<div class="cts-dash-id">
						{d.title && <p class="cts-dash-title">{d.title}</p>}
						{d.timestamp && <p class="cts-dash-time">{d.timestamp}</p>}
					</div>
					{
						d.status && (
							<p class="cts-status">
								<span class="cts-dot cts-dot--sm" aria-hidden="true" />
								{d.status}
							</p>
						)
					}
				</div>
				<div class:list={["cts-dash-main", { "cts-dash-main--noimg": !image }]}>
					{
						image && (
							<div class="cts-photo">
								{image.src ? (
									<img src={image.src} alt={image.alt ?? ""} width="900" height="1080" loading="lazy" decoding="async" />
								) : (
									<div class="cts-ph" role="img" aria-label={image.alt ? `Image placeholder: ${image.alt}` : "Image placeholder"}>
										<span class="cts-ph-text" aria-hidden="true">
											<span class="cts-ph-label">Image</span>
											{image.alt && <span>{image.alt}</span>}
											<span class="cts-ph-spec">5:6 · 900 × 1080 px</span>
										</span>
									</div>
								)}
							</div>
						)
					}
					{hasKpis && (
						<ul class="cts-kpis" role="list" data-cts-group>
							{
								d.onTime && (
									<li class="cts-kpi" style="--cts-i:0">
										<p class="cts-kpi-label">{d.onTime.label}</p>
										<p class="cts-kpi-value">{d.onTime.value}</p>
										{bars.length > 0 && (
											<div class="cts-bars" aria-hidden="true">
												{bars.map((b, i) => (
													<span
														class:list={["cts-bar", { "cts-bar--on": highlight.has(i) }]}
														style={`height:${Math.round((b / barMax) * 100)}%`}
													/>
												))}
											</div>
										)}
									</li>
								)
							}
							{
								d.exceptions && (
									<li class="cts-kpi" style="--cts-i:1">
										<p class="cts-kpi-label">{d.exceptions.label}</p>
										<p class="cts-kpi-value">{d.exceptions.value}</p>
										{d.exceptions.delta && <p class="cts-kpi-delta">{d.exceptions.delta}</p>}
									</li>
								)
							}
							{
								d.carbon && (
									<li class="cts-kpi" style="--cts-i:2">
										<p class="cts-kpi-label">{d.carbon.label}</p>
										<p class="cts-kpi-value">{d.carbon.value}</p>
									</li>
								)
							}
						</ul>
					)}
				</div>
			</div>
		</div>
		)}
	</div>
</section>
)}

<style>
	.cts {
		--cts-ink: var(--nv-ink, #08110f);
		--cts-lime: var(--nv-lime, #b7ff39);
		--cts-font: var(--nv-font-ui, Inter, ui-sans-serif, system-ui, sans-serif);
		--cts-ease: cubic-bezier(0.22, 1, 0.36, 1);

		box-sizing: border-box;
		padding: 82px 18px;
		background: var(--cts-bg);
		color: #fff;
		font-family: var(--cts-font);
		overflow: clip;
	}

	.cts *,
	.cts *::before,
	.cts *::after {
		box-sizing: border-box;
	}

	.cts p {
		margin: 0;
	}

	.cts-grid {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: 52px;
		max-width: 1240px;
		margin: 0 auto;
	}

	.cts-content {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 24px;
		min-width: 0;
	}

	.cts-eyebrow {
		display: flex;
		align-items: center;
		gap: 9px;
		font: 700 12px/14.4px var(--cts-font);
		letter-spacing: 1.56px;
		text-transform: uppercase;
		color: #fff;
	}

	.cts-dot {
		flex: none;
		width: 7px;
		height: 7px;
		border-radius: 100px;
		background: var(--cts-lime);
	}

	.cts-heading {
		max-width: 6.4em;
		margin: 0;
		font-weight: 700;
		font-size: clamp(42px, 13.4vw, 52px);
		line-height: 0.98;
		letter-spacing: -0.05em;
		color: #fff;
		text-wrap: balance;
	}

	.cts-body {
		max-width: 550px;
		font: 400 17px/28.9px var(--cts-font);
		color: rgb(255 255 255 / 0.65);
	}

	.cts-features {
		display: flex;
		flex-direction: column;
		gap: 14px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.cts-feature {
		display: flex;
		align-items: center;
		gap: 13px;
		font: 600 14px/16.8px var(--cts-font);
		color: #fff;
	}

	.cts-check {
		display: grid;
		place-items: center;
		flex: none;
		width: 28px;
		height: 28px;
		border-radius: 100px;
		background: var(--cts-lime);
		color: var(--cts-ink);
	}

	.cts-check svg {
		width: 15px;
		height: 15px;
	}

	.cts-cta {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: 10px;
		height: 52px;
		padding: 0 21px;
		border-radius: 100px;
		background: var(--cts-lime);
		color: var(--cts-ink);
		font: 700 13px/15.6px var(--cts-font);
		text-decoration: none;
		transition: transform 0.3s var(--cts-ease);
	}

	.cts-cta svg {
		width: 17px;
		height: 17px;
	}

	.cts-cta:hover {
		transform: translateY(-2px) scale(1.1);
	}

	.cts-cta:focus-visible {
		outline: 2px solid #fff;
		outline-offset: 3px;
	}

	/* ---- dashboard mock ---- */

	.cts-visual {
		min-width: 0;
	}

	.cts-dash {
		display: flex;
		flex-direction: column;
		gap: 14px;
		padding: 20px;
		border-radius: 32px;
		background: #f4f6f2;
		color: var(--cts-ink);
		box-shadow: 0 24px 60px 0 rgb(0 0 0 / 0.32);
		overflow: clip;
	}

	.cts-dash-head {
		display: flex;
		align-items: center;
		justify-content: space-between;
		flex-wrap: wrap;
		gap: 10px;
		padding: 8px 5px 19px;
	}

	.cts-dash-id {
		display: flex;
		flex-direction: column;
		gap: 3px;
	}

	.cts-dash-title {
		font: 400 12px/14.4px var(--cts-font);
	}

	.cts-dash-time {
		font: 700 16px/19.2px var(--cts-font);
	}

	.cts-status {
		display: inline-flex;
		align-items: center;
		gap: 7px;
		height: 30px;
		padding: 0 11px;
		border-radius: 100px;
		background: rgb(177 255 57 / 0.28);
		font: 700 10px/12px var(--cts-font);
		letter-spacing: 0.5px;
		text-transform: uppercase;
		white-space: nowrap;
	}

	.cts-dash-main {
		display: flex;
		flex-direction: column;
		gap: 14px;
	}

	.cts-photo {
		position: relative;
		height: 250px;
		border-radius: 22px;
		overflow: hidden;
		background: #0d1614;
	}

	.cts-photo img {
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	.cts-ph {
		display: grid;
		place-items: center;
		width: 100%;
		height: 100%;
		padding: 16px;
		text-align: center;
		background: repeating-linear-gradient(135deg, #e3e7e1 0 12px, #ebeee9 12px 24px);
		color: rgb(8 17 15 / 0.62);
	}

	.cts-ph-text {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 6px;
		font: 500 12px/1.35 var(--cts-font);
	}

	.cts-ph-label {
		letter-spacing: 0.08em;
		text-transform: uppercase;
	}

	.cts-ph-spec {
		font-weight: 400;
	}

	.cts-kpis {
		display: flex;
		flex-direction: column;
		gap: 14px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.cts-kpi {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 8px;
		padding: 18px;
		border-radius: 20px;
		background: #fff;
	}

	.cts-kpi-label {
		font: 500 12px/14.4px var(--cts-font);
		color: rgb(8 17 15 / 0.58);
	}

	.cts-kpi-value {
		font: 700 28px/33.6px var(--cts-font);
		color: var(--cts-ink);
	}

	.cts-kpi-delta {
		margin-top: -1px;
		font: 600 12px/14.4px var(--cts-font);
		color: #288047;
	}

	.cts-bars {
		display: flex;
		align-items: flex-end;
		gap: 7px;
		width: 100%;
		height: 58px;
		margin-top: 4px;
	}

	.cts-bar {
		flex: 1 1 0;
		min-width: 0;
		border-radius: 5px 5px 0 0;
		background: rgb(125 145 133 / 0.28);
	}

	.cts-bar--on {
		background: var(--cts-lime);
	}

	/* ---- tablet: still stacked, dashboard side by side inside ---- */
	@media (min-width: 810px) {
		.cts {
			padding: 100px 40px;
		}

		.cts-heading {
			font-size: 62px;
		}

		.cts-visual {
			max-width: 640px;
		}

		.cts-dash-main {
			flex-direction: row;
			align-items: stretch;
		}

		.cts-photo {
			flex: 1 1 auto;
			height: auto;
			min-height: 420px;
		}

		.cts-kpis {
			flex: 0 0 220px;
		}

		/* Tiles share out the photo's height, as the reference's stack does. */
		.cts-kpi {
			flex: 1 1 auto;
		}

		.cts-dash-main--noimg .cts-kpis {
			flex: 1 1 auto;
		}
	}

	/* ---- desktop: two columns ---- */
	@media (min-width: 1200px) {
		.cts {
			padding: 120px 48px;
		}

		.cts-grid {
			grid-template-columns: repeat(2, minmax(0, 584px));
			justify-content: center;
			align-items: start;
			gap: 72px;
		}

		.cts-heading {
			font-size: 70px;
			line-height: 68.6px;
			letter-spacing: -3.5px;
		}

		.cts-visual {
			max-width: none;
		}
	}

	/* ---- appear ----
	   Only once the script has marked the root ready: no script, no hidden
	   state. The script never marks it under reduced motion, and this media
	   guard covers a preference that changes afterwards. */
	@media (prefers-reduced-motion: no-preference) {
		/* Transition only on the way in, so arming the hidden state is an
		   instant cut rather than a fade-out. */
		.cts[data-cts-ready] [data-in] .cts-feature,
		.cts[data-cts-ready] [data-in] .cts-kpi {
			transition:
				opacity 0.6s var(--cts-ease),
				transform 0.6s var(--cts-ease);
			transition-delay: calc(var(--cts-i, 0) * 90ms);
		}

		.cts[data-cts-ready] [data-cts-group]:not([data-in]) .cts-feature {
			opacity: 0;
			transform: translateX(-34px);
		}

		.cts[data-cts-ready] [data-cts-group]:not([data-in]) .cts-kpi {
			opacity: 0;
			transform: translateY(18px) scale(0.96);
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.cts-cta {
			transition: none;
		}

		.cts-cta:hover {
			transform: none;
		}
	}

	@media (forced-colors: active) {
		.cts-dash,
		.cts-kpi {
			border: 1px solid CanvasText;
		}

		.cts-bar {
			background: GrayText;
		}

		.cts-bar--on,
		.cts-check,
		.cts-dot {
			background: CanvasText;
		}
	}
</style>

<script>
	/**
	 * ControlTowerShowcase appear controller.
	 *
	 * One IntersectionObserver per instance, watching the feature list and
	 * the KPI stack. Each gets `data-in` the first time it enters the
	 * viewport and is then unobserved. Never scrolls, never moves focus.
	 */
	function initControlTower(root: HTMLElement) {
		if (root.dataset.ctsReady !== undefined) return;
		if (root.dataset.ctsAnimate !== "1") return;
		if (!("IntersectionObserver" in window)) return;
		if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

		const groups = [...root.querySelectorAll<HTMLElement>("[data-cts-group]")];
		if (groups.length === 0) return;

		const controller = new AbortController();
		const { signal } = controller;

		const observer = new IntersectionObserver(
			(entries) => {
				for (const entry of entries) {
					if (!entry.isIntersecting) continue;
					(entry.target as HTMLElement).dataset.in = "";
					observer.unobserve(entry.target);
				}
			},
			{ threshold: 0.2, rootMargin: "0px 0px -8% 0px" },
		);

		root.dataset.ctsReady = "";
		groups.forEach((group) => observer.observe(group));
		signal.addEventListener("abort", () => observer.disconnect());

		document.addEventListener(
			"astro:before-swap",
			() => {
				controller.abort();
				delete root.dataset.ctsReady;
				groups.forEach((g) => delete g.dataset.in);
			},
			{ once: true, signal },
		);
	}

	const boot = () =>
		document.querySelectorAll<HTMLElement>("[data-cts-root]").forEach(initControlTower);

	if (document.readyState === "loading") {
		document.addEventListener("DOMContentLoaded", boot, { once: true });
	} else {
		boot();
	}

	document.addEventListener("astro:page-load", boot);
</script>

===== END src/components/ControlTowerShowcase.astro =====

===== FILE (required): public/control-tower-showcase/analytics-screen.webp =====
[binary asset — download https://astro.baysixmedia.com/control-tower-showcase/analytics-screen.webp and save it to public/control-tower-showcase/analytics-screen.webp]

===== FILE (required): src/components/IndustryTabsPanel.astro =====
---
/**
 * IndustryTabsPanel
 *
 * A light section: a lime-dot eyebrow on the left and a big heading on the
 * right, a row of pill tabs, and under them one rounded panel split into a
 * photo and an ink content side (eyebrow, heading, paragraph, two stat
 * tiles, a lime "View solution" pill). Each tab swaps the whole panel.
 *
 * WAI-ARIA tabs with AUTOMATIC activation: arrow keys move focus and select
 * in one step, which is fine here because every panel is already in the
 * document and a swap costs nothing.
 *
 * No script: the tab row is hidden and all panels are shown stacked (see
 * the <noscript> block), so nothing is stranded behind a control that cannot
 * work.
 *
 * Reads --nv-* design tokens from any ancestor, with fallbacks. Never sets
 * them.
 */

export interface IndustryTabStat {
	value: string;
	label: string;
}

export interface IndustryTab {
	/** The pill label, and the panel's accessible name. */
	label: string;
	/** Small uppercase line above the panel heading. `""` hides it. */
	eyebrow?: string;
	heading: string;
	text?: string;
	/** Up to two read best; more wrap onto a new row. */
	stats?: IndustryTabStat[];
	/**
	 * The photo. `src: ""` (or no `src`) renders the labelled placeholder
	 * instead, naming `hint` (or `alt`, or the label) and the size to supply.
	 */
	image?: { src?: string; alt?: string; hint?: string };
	/** The lime pill. `null` drops it. */
	cta?: { label: string; href: string } | null;
}

export interface IndustryTabsPanelProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	/** Accessible name of the tab row. */
	tabsLabel?: string;
	tabs?: IndustryTab[];
	/** Which tab starts selected, counting from 1. Clamped to the list. */
	initialTab?: number;
	/** The section surface. */
	background?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends IndustryTabsPanelProps {}

const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	tabsLabel = "Industries",
	tabs = [],
	initialTab = 1,
	background = "var(--nv-surface, #f8f9f6)",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	id,
} = Astro.props;

const Heading = headingLevel;
const PanelHeading = headingLevel === "h2" ? "h3" : "h4";

/*
 * A tab needs a label to click and a heading for its panel; one missing either
 * is dropped rather than drawn half-empty, as is a stat without both a value
 * and a label, or a CTA without a label. No tabs, no section: the heading alone
 * never renders.
 */
const list = tabs
	.filter((tab) => tab?.label && tab?.heading)
	.map((tab) => ({
		...tab,
		stats: tab.stats?.filter((stat) => stat?.value && stat?.label),
		cta: tab.cta?.label ? tab.cta : null,
	}));

const uid = id ?? `itp-${Math.random().toString(36).slice(2, 9)}`;
const active = Math.min(Math.max(Math.round(initialTab) - 1, 0), Math.max(list.length - 1, 0));

/* Only schemes that are links, so `javascript:` never reaches an href. */
const SAFE_SCHEME = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE_SCHEME.test(href.trim()) ? href.trim() : undefined);

/* The supplied photos are 1240 × 1040 and fill a 620 × 520 box at desktop. */
const IMAGE_SPEC = "31:26 · 1240 × 1040 px";
---

{list.length > 0 && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{list.length > 0 && (
<section
	class="itp"
	id={id}
	style={`--itp-bg: ${background};`}
	data-itp-root
	aria-labelledby={heading ? `${uid}-heading` : undefined}
	aria-label={heading ? undefined : tabsLabel}
>
	<div class="itp-inner">
		{
			(eyebrow || heading) && (
				<div class="itp-head">
					{eyebrow ? (
						<p class="itp-eyebrow">
							<span class="itp-dot" aria-hidden="true" />
							{eyebrow}
						</p>
					) : (
						<span class="itp-eyebrow-spacer" aria-hidden="true" />
					)}
					{heading && (
						<Heading class="itp-heading" id={`${uid}-heading`}>
							{heading}
						</Heading>
					)}
				</div>
			)
		}

		<div class="itp-tabs" role="tablist" aria-label={tabsLabel} data-itp-tablist>
			{
				list.map((tab, i) => (
					<button
						type="button"
						class="itp-tab"
						role="tab"
						id={`${uid}-tab-${i}`}
						aria-controls={`${uid}-panel-${i}`}
						aria-selected={i === active ? "true" : "false"}
						tabindex={i === active ? 0 : -1}
						data-itp-tab
					>
						{tab.label}
					</button>
				))
			}
		</div>

		<div class="itp-stage" data-itp-stage>
			{
				list.map((tab, i) => {
					const href = tab.cta ? safeHref(tab.cta.href) : undefined;
					const src = tab.image?.src;
					const hint = tab.image?.hint ?? tab.image?.alt ?? `${tab.label} photo`;
					return (
						<div
							class="itp-panel"
							role="tabpanel"
							id={`${uid}-panel-${i}`}
							aria-labelledby={`${uid}-tab-${i}`}
							hidden={i !== active}
							data-itp-panel
						>
							<div class="itp-media">
								{src ? (
									<img
										src={src}
										alt={tab.image?.alt ?? ""}
										width="620"
										height="520"
										loading="lazy"
										decoding="async"
									/>
								) : (
									<div class="itp-ph" role="img" aria-label={`Image placeholder: ${hint}`}>
										<div class="itp-ph-text" aria-hidden="true">
											<span class="itp-ph-label">Image</span>
											<span class="itp-ph-hint">{hint}</span>
											<span class="itp-ph-spec">{IMAGE_SPEC}</span>
										</div>
									</div>
								)}
							</div>
							<div class="itp-content">
								{tab.eyebrow && (
									<p class="itp-kicker">
										<span class="itp-dot" aria-hidden="true" />
										{tab.eyebrow}
									</p>
								)}
								<PanelHeading class="itp-title">{tab.heading}</PanelHeading>
								{tab.text && <p class="itp-text">{tab.text}</p>}
								{tab.stats && tab.stats.length > 0 && (
									<ul class="itp-stats">
										{tab.stats.map((stat) => (
											<li class="itp-stat">
												<strong class="itp-stat-value">{stat.value}</strong>
												<span class="itp-stat-label">{stat.label}</span>
											</li>
										))}
									</ul>
								)}
								{tab.cta && href && (
									<a class="itp-cta" href={href}>
										<span>{tab.cta.label}</span>
										<svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true" focusable="false">
											<path
												d="M4 10 10 4M5 4h5v5"
												fill="none"
												stroke="currentColor"
												stroke-width="1.6"
												stroke-linecap="round"
												stroke-linejoin="round"
											/>
										</svg>
									</a>
								)}
							</div>
						</div>
					);
				})
			}
		</div>
	</div>

	<noscript>
		<style>
			/* Without the script the tabs cannot switch anything, so the row is
			   hidden and every panel is shown, stacked. Written against data-*
			   attributes because a style block nested in the markup is not
			   scoped by Astro. */
			[data-itp-root] [data-itp-tablist] {
				display: none !important;
			}
			[data-itp-root] [data-itp-panel][hidden] {
				display: flex !important;
			}
		</style>
	</noscript>
</section>
)}

<style>
	.itp {
		--itp-ink: var(--nv-ink, #08110f);
		--itp-lime: var(--nv-lime, #b7ff39);
		--itp-chip: #e8ece5;
		--itp-chip-hover: #dfe7dd;
		--itp-font: var(--nv-font-ui, Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
		--itp-ease: cubic-bezier(0.22, 1, 0.36, 1);

		box-sizing: border-box;
		padding: 82px 18px;
		background: var(--itp-bg);
		color: var(--itp-ink);
		font-family: var(--itp-font);
	}

	.itp *,
	.itp *::before,
	.itp *::after {
		box-sizing: border-box;
	}

	.itp-inner {
		max-width: 1240px;
		margin: 0 auto;
	}

	/* ---- heading row ---- */
	.itp-head {
		display: flex;
		flex-direction: column;
		gap: 20px;
		/* Measured as two spacers, 50px + 22px. */
		margin-bottom: 72px;
	}

	.itp-eyebrow,
	.itp-kicker {
		display: flex;
		align-items: center;
		margin: 0;
		font-weight: 800;
		text-transform: uppercase;
	}

	.itp-eyebrow {
		gap: 9px;
		font-size: 12px;
		line-height: 14.4px;
		letter-spacing: 1.2px;
	}

	.itp-dot {
		flex: none;
		width: 8px;
		height: 8px;
		border-radius: 100px;
		background: var(--itp-lime);
	}

	.itp-heading {
		margin: 0;
		max-width: 700px;
		font-size: 40px;
		font-weight: 700;
		line-height: 1.02;
		letter-spacing: -0.045em;
		text-wrap: balance;
	}

	.itp-eyebrow-spacer {
		display: none;
	}

	/* ---- tabs ---- */
	.itp-tabs {
		display: flex;
		flex-wrap: wrap;
		gap: 10px;
		margin-bottom: 22px;
	}

	.itp-tab {
		position: relative;
		display: inline-flex;
		align-items: center;
		justify-content: center;
		height: 34px;
		padding: 0 16px;
		border: 0;
		border-radius: 100px;
		background: var(--itp-chip);
		color: var(--itp-ink);
		font: 800 11px/13.2px var(--itp-font);
		white-space: nowrap;
		cursor: pointer;
		transition:
			transform 0.3s var(--itp-ease),
			background-color 0.3s var(--itp-ease),
			box-shadow 0.3s var(--itp-ease),
			color 0.3s var(--itp-ease);
	}

	/* The pill is 34px tall as measured; the hit area is 44px. */
	.itp-tab::before {
		content: "";
		position: absolute;
		inset: -5px 0;
	}

	.itp-tab[aria-selected="true"] {
		background: var(--itp-ink);
		color: #fff;
	}

	.itp-tab:focus-visible {
		outline: 2px solid var(--itp-ink);
		outline-offset: 3px;
	}

	@media (hover: hover) {
		.itp-tab:hover {
			transform: scale(1.02) translateX(4px);
		}

		.itp-tab[aria-selected="false"]:hover {
			background: var(--itp-chip-hover);
		}

		.itp-tab[aria-selected="true"]:hover {
			box-shadow: 0 8px 16px rgb(8 17 15 / 0.12);
		}
	}

	/* ---- panel ---- */
	.itp-stage {
		display: grid;
		gap: 16px;
	}

	.itp-panel {
		display: flex;
		flex-direction: column;
		border-radius: 28px;
		background: var(--itp-ink);
		box-shadow: 0 20px 48px rgb(8 17 15 / 0.12);
		overflow: hidden;
		color: #fff;
	}

	.itp-panel[hidden] {
		display: none;
	}

	.itp-media {
		position: relative;
		flex: none;
		aspect-ratio: 339 / 250;
		overflow: hidden;
	}

	.itp-media img {
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	.itp-ph {
		display: grid;
		place-items: center;
		width: 100%;
		height: 100%;
		padding: 16px;
		text-align: center;
		background: repeating-linear-gradient(135deg, #1a2522 0 12px, #202c29 12px 24px);
		color: rgb(247 250 247 / 0.7);
	}

	.itp-ph-text {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 8px;
	}

	.itp-ph-label {
		font: 700 11px/1 var(--itp-font);
		letter-spacing: 0.1em;
		text-transform: uppercase;
	}

	.itp-ph-hint {
		max-width: 30ch;
		font: 500 14px/1.4 var(--itp-font);
	}

	.itp-ph-spec {
		font: 400 12px/1.2 var(--itp-font);
	}

	.itp-content {
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: flex-start;
		min-width: 0;
		padding: 36px clamp(22px, 8vw, 32px) 32px;
	}

	.itp-kicker {
		gap: 8px;
		margin-bottom: 20px;
		font-size: 11px;
		line-height: 13.2px;
		letter-spacing: 1.1px;
		color: #fff;
	}

	.itp-title {
		margin: 0;
		max-width: 420px;
		font-size: clamp(34px, 10.5vw, 40px);
		font-weight: 700;
		line-height: 1.05;
		letter-spacing: -0.035em;
		color: #fff;
	}

	.itp-text {
		margin: 26px 0 0;
		max-width: 440px;
		font-size: 15px;
		font-weight: 400;
		line-height: 25.2px;
		color: rgb(255 255 255 / 0.65);
	}

	.itp-stats {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: 12px;
		width: 100%;
		max-width: 524px;
		margin: 32px 0 0;
		padding: 0;
		list-style: none;
	}

	.itp-stat {
		display: flex;
		flex-direction: column;
		justify-content: center;
		gap: 3px;
		min-height: 76px;
		padding: 16px;
		border-radius: 18px;
		background: rgb(255 255 255 / 0.08);
	}

	.itp-stat-value {
		font-size: 23px;
		font-weight: 800;
		line-height: 23px;
		color: var(--itp-lime);
	}

	.itp-stat-label {
		font-size: 12px;
		font-weight: 500;
		line-height: 14.4px;
		color: #fff;
	}

	.itp-cta {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: 8px;
		height: 44px;
		margin-top: 24px;
		padding: 0 18px;
		border-radius: 100px;
		background: var(--itp-lime);
		color: var(--itp-ink);
		font: 800 12px/14.4px var(--itp-font);
		text-decoration: none;
		white-space: nowrap;
	}

	.itp-cta:focus-visible {
		outline: 2px solid var(--itp-lime);
		outline-offset: 3px;
	}

	/* ---- appear (only once the script has marked the root ready) ---- */
	.itp[data-itp-ready] .itp-stage {
		transition:
			opacity 0.7s var(--itp-ease),
			transform 0.7s var(--itp-ease);
	}

	.itp[data-itp-ready]:not([data-in]) .itp-stage {
		opacity: 0;
		transform: translateY(22px);
	}

	/* A softer re-appear on each tab change. */
	.itp-panel[data-itp-swap] {
		animation: itp-swap 0.45s var(--itp-ease) both;
	}

	@keyframes itp-swap {
		from {
			opacity: 0;
			transform: translateY(10px);
		}
		to {
			opacity: 1;
			transform: none;
		}
	}

	/* ---- tablet ---- */
	@media (min-width: 810px) {
		.itp {
			padding: 100px 40px;
		}

		.itp-heading {
			font-size: 52px;
			letter-spacing: -0.045em;
		}

		.itp-panel {
			flex-direction: row;
			min-height: 480px;
			border-radius: 34px;
		}

		.itp-media {
			flex: 0 0 50%;
			aspect-ratio: auto;
		}

		.itp-content {
			flex: 1 1 50%;
			padding: 44px 40px;
		}

		.itp-title {
			font-size: 38px;
		}
	}

	/* ---- desktop: the measured layout ---- */
	@media (min-width: 1200px) {
		.itp {
			padding: 120px 64px;
		}

		.itp-head {
			flex-direction: row;
			align-items: flex-start;
			gap: 70px;
		}

		.itp-eyebrow,
		.itp-eyebrow-spacer {
			display: flex;
			flex: 0 0 422px;
		}

		.itp-heading {
			flex: 0 1 700px;
			font-size: 62px;
			line-height: 60.76px;
			letter-spacing: -2.79px;
		}

		.itp-panel {
			height: 520px;
			min-height: 0;
		}

		.itp-media {
			flex: 0 0 620px;
		}

		.itp-content {
			padding: 52px 48px;
		}

		.itp-title {
			font-size: 44px;
			line-height: 44px;
			letter-spacing: -1.54px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.itp-tab {
			transition:
				background-color 0.3s ease,
				box-shadow 0.3s ease,
				color 0.3s ease;
		}

		.itp-tab:hover {
			transform: none !important;
		}

		.itp[data-itp-ready] .itp-stage {
			transition: none;
		}

		.itp[data-itp-ready]:not([data-in]) .itp-stage {
			opacity: 1;
			transform: none;
		}

		.itp-panel[data-itp-swap] {
			animation: none;
		}
	}

	@media (forced-colors: active) {
		.itp-tab {
			border: 1px solid ButtonText;
		}

		.itp-tab[aria-selected="true"] {
			background: Highlight;
			color: HighlightText;
			forced-color-adjust: none;
		}

		.itp-panel,
		.itp-stat {
			border: 1px solid CanvasText;
		}

		.itp-cta {
			border: 1px solid LinkText;
		}

		.itp-dot {
			forced-color-adjust: none;
		}
	}
</style>

<script>
	/*
	 * IndustryTabsPanel controller.
	 *
	 * One closure per instance, every lookup scoped to its root, every
	 * listener and the observer on one AbortController, torn down before a
	 * client-side navigation swaps the document.
	 *
	 * Automatic activation: Left/Right/Home/End move focus AND select. The
	 * roving tabindex keeps exactly one tab in the Tab order.
	 */
	interface Instance {
		destroy(): void;
	}

	function initIndustryTabs(root: HTMLElement): Instance | undefined {
		/* Idempotent: booting twice is a no-op, not a second set of listeners. */
		if (root.dataset.itpReady === "") return undefined;

		const tabs = [...root.querySelectorAll<HTMLButtonElement>("[data-itp-tab]")];
		const panels = [...root.querySelectorAll<HTMLElement>("[data-itp-panel]")];
		const stage = root.querySelector<HTMLElement>("[data-itp-stage]");
		if (tabs.length === 0 || tabs.length !== panels.length || !stage) return undefined;

		const controller = new AbortController();
		const { signal } = controller;
		const reduced = window.matchMedia("(prefers-reduced-motion: reduce)");

		let index = Math.max(
			0,
			tabs.findIndex((tab) => tab.getAttribute("aria-selected") === "true"),
		);
		const servedIndex = index;

		function select(next: number, focus: boolean) {
			const n = tabs.length;
			next = ((next % n) + n) % n;
			const changed = next !== index;

			tabs.forEach((tab, i) => {
				const on = i === next;
				tab.setAttribute("aria-selected", on ? "true" : "false");
				tab.tabIndex = on ? 0 : -1;
			});
			panels.forEach((panel, i) => {
				panel.hidden = i !== next;
				if (i !== next) panel.removeAttribute("data-itp-swap");
			});

			if (changed && !reduced.matches) {
				const panel = panels[next];
				panel.removeAttribute("data-itp-swap");
				void panel.offsetWidth;
				panel.setAttribute("data-itp-swap", "");
			}

			index = next;
			/* Focus moves only in response to a key press on the tab row. */
			if (focus) tabs[next].focus();
		}

		tabs.forEach((tab, i) => {
			tab.addEventListener("click", () => select(i, false), { signal });
			tab.addEventListener(
				"keydown",
				(event) => {
					let next: number;
					switch (event.key) {
						case "ArrowRight":
							next = i + 1;
							break;
						case "ArrowLeft":
							next = i - 1;
							break;
						case "Home":
							next = 0;
							break;
						case "End":
							next = tabs.length - 1;
							break;
						default:
							return;
					}
					event.preventDefault();
					select(next, true);
				},
				{ signal },
			);
		});

		panels.forEach((panel) =>
			panel.addEventListener("animationend", () => panel.removeAttribute("data-itp-swap"), { signal }),
		);

		/* Warm the other tabs' photos once the section is on screen, so a
		   switch does not wait on a lazy image. */
		function warmImages() {
			root.querySelectorAll<HTMLImageElement>("[data-itp-panel] img").forEach((img) => {
				img.loading = "eager";
			});
		}

		/* Appear: the hidden start state applies only under data-itp-ready,
		   so no-JS and a failed boot both show the panel at rest. */
		let observer: IntersectionObserver | undefined;
		if (reduced.matches || !("IntersectionObserver" in window)) {
			root.setAttribute("data-in", "");
			root.dataset.itpReady = "";
			warmImages();
		} else {
			root.dataset.itpReady = "";
			observer = new IntersectionObserver(
				(entries) => {
					if (entries.some((entry) => entry.isIntersecting)) {
						root.setAttribute("data-in", "");
						warmImages();
						observer?.disconnect();
					}
				},
				{ threshold: 0.15 },
			);
			observer.observe(stage);
		}

		function destroy() {
			observer?.disconnect();
			controller.abort();
			select(servedIndex, false);
			panels.forEach((panel) => panel.removeAttribute("data-itp-swap"));
			root.removeAttribute("data-in");
			delete root.dataset.itpReady;
		}

		document.addEventListener("astro:before-swap", destroy, { once: true, signal });

		return { destroy };
	}

	const boot = () =>
		document.querySelectorAll<HTMLElement>("[data-itp-root]").forEach(initIndustryTabs);

	if (document.readyState === "loading") {
		document.addEventListener("DOMContentLoaded", boot, { once: true });
	} else {
		boot();
	}

	/* Fires on every view transition in a project using <ClientRouter />. */
	document.addEventListener("astro:page-load", boot);
</script>

===== END src/components/IndustryTabsPanel.astro =====

===== FILE (required): public/industry-tabs-panel/automotive.webp =====
[binary asset — download https://astro.baysixmedia.com/industry-tabs-panel/automotive.webp and save it to public/industry-tabs-panel/automotive.webp]

===== FILE (required): public/industry-tabs-panel/retail.webp =====
[binary asset — download https://astro.baysixmedia.com/industry-tabs-panel/retail.webp and save it to public/industry-tabs-panel/retail.webp]

===== FILE (required): public/industry-tabs-panel/healthcare.webp =====
[binary asset — download https://astro.baysixmedia.com/industry-tabs-panel/healthcare.webp and save it to public/industry-tabs-panel/healthcare.webp]

===== FILE (required): public/industry-tabs-panel/industrial.webp =====
[binary asset — download https://astro.baysixmedia.com/industry-tabs-panel/industrial.webp and save it to public/industry-tabs-panel/industrial.webp]

===== FILE (required): public/industry-tabs-panel/food.webp =====
[binary asset — download https://astro.baysixmedia.com/industry-tabs-panel/food.webp and save it to public/industry-tabs-panel/food.webp]

===== FILE (required): src/components/RouteQuoteEstimator.astro =====
---
/**
 * RouteQuoteEstimator
 *
 * A rounded ink card on a pale section: a lime-dot eyebrow, a big heading,
 * a paragraph and two small status badges on the left; on the right a white
 * panel with a live route estimator — origin, destination, mode, weight and
 * shipment type — whose result box recomputes an indicative price range and
 * transit time on every input, and a full-width lime link to request a
 * verified quote.
 *
 * It is an estimate UI, not a form: nothing is submitted. The rate table
 * (lanes, modes, shipment types, currency) is required props, never a
 * default: with none it renders nothing.
 *
 * No script: the default estimate is rendered on the server and every
 * control is still labelled; a <noscript> note says the figure will not
 * recalculate.
 *
 * Reads --nv-* design tokens from any ancestor, with fallbacks. Never sets
 * them.
 */

/** One origin → destination pair. Price = (base + perKg × kg) × mode × type. */
export interface EstimatorLane {
	origin: string;
	destination: string;
	/** Fixed cost of the lane at the mode multiplier 1, in `currency`. */
	base: number;
	/** Cost per kilogram at the mode multiplier 1. */
	perKg: number;
	/** Road transit in whole days, [min, max]. Modes scale this. */
	transitDays: [number, number];
}

export interface EstimatorMode {
	label: string;
	/** Multiplies the lane price. 1 for the reference mode. */
	multiplier: number;
	/** Multiplies the lane's transit days (rounded up, minimum 1). */
	transitScale: number;
}

export interface EstimatorShipmentType {
	label: string;
	/** Multiplies the price after the mode. */
	multiplier: number;
}

export interface RouteQuoteEstimatorProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	text?: string;
	/** Small pills under the paragraph. `[]` drops the row. */
	badges?: string[];
	lanes?: EstimatorLane[];
	modes?: EstimatorMode[];
	shipmentTypes?: EstimatorShipmentType[];
	/** Half-width of the range as a fraction of the midpoint. 0.04 = ±4%. */
	band?: number;
	/** Range ends are rounded to the nearest multiple of this. */
	roundTo?: number;
	/** ISO 4217 code for Intl.NumberFormat. */
	currency?: string;
	locale?: string;
	minWeight?: number;
	maxWeight?: number;
	initialOrigin?: string;
	initialDestination?: string;
	initialMode?: string;
	initialType?: string;
	initialWeight?: number;
	/** Field labels, in reading order. */
	labels?: {
		origin?: string;
		destination?: string;
		mode?: string;
		weight?: string;
		type?: string;
		estimate?: string;
		note?: string;
	};
	quoteLabel?: string;
	quoteHref?: string;
	/** The section surface around the card. */
	background?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends RouteQuoteEstimatorProps {}

/*
 * No default rate table. Lanes, modes, shipment types and the currency are
 * the caller's own rates: a built-in table would be inventing prices. With
 * any of them missing the component renders nothing. (The catalog demo's
 * table, solved from the reference's three measured points, lives in the
 * registry's demo props.)
 */
const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	text = "",
	badges = [],
	lanes: lanesProp = [],
	modes: modesProp = [],
	shipmentTypes: typesProp = [],
	band = 0.04,
	roundTo = 10,
	currency = "",
	locale = "en-US",
	minWeight = 1,
	maxWeight = 40000,
	initialOrigin,
	initialDestination,
	initialMode,
	initialType,
	initialWeight = 1250,
	labels = {},
	quoteLabel = "",
	quoteHref = "",
	background = "#eef3f8",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	id,
} = Astro.props;

const L = {
	origin: "Origin",
	destination: "Destination",
	mode: "Mode",
	weight: "Weight (kg)",
	type: "Shipment type",
	estimate: "Indicative estimate",
	note: "Rate table estimate",
	...labels,
};

const filled = (v: unknown): v is string => typeof v === "string" && v.trim() !== "";
const finite = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);

/* Only complete rows count: a lane needs both ends, both rates and a
   [min, max] transit; a mode or type needs a label and its numbers. */
const lanes = (lanesProp ?? []).filter(
	(l) =>
		l &&
		filled(l.origin) &&
		filled(l.destination) &&
		finite(l.base) &&
		finite(l.perKg) &&
		Array.isArray(l.transitDays) &&
		l.transitDays.length === 2 &&
		l.transitDays.every(finite),
);
const modes = (modesProp ?? []).filter((m) => m && filled(m.label) && finite(m.multiplier) && finite(m.transitScale));
const shipmentTypes = (typesProp ?? []).filter((t) => t && filled(t.label) && finite(t.multiplier));

/* No currency (or one Intl does not know) → no prices to show. */
let money: Intl.NumberFormat | null = null;
try {
	if (filled(currency)) money = new Intl.NumberFormat(locale, { style: "currency", currency, maximumFractionDigits: 0 });
} catch {
	money = null;
}

const hasRates = lanes.length > 0 && modes.length > 0 && shipmentTypes.length > 0 && money !== null;
const badgeList = (badges ?? []).filter(filled);
const showCta = filled(quoteLabel) && filled(quoteHref);

const Heading = headingLevel;
const uid = id ?? `rqe-${Math.random().toString(36).slice(2, 9)}`;

const unique = (list: string[]) => [...new Set(list)];
const origins = unique(lanes.map((l) => l.origin));
const destinations = unique(lanes.map((l) => l.destination));

const pick = (list: string[], wanted?: string) => (wanted && list.includes(wanted) ? wanted : list[0] ?? "");
const origin = pick(origins, initialOrigin);
/* Prefer a destination that has a lane from the chosen origin. */
const destination = pick(
	destinations,
	initialDestination ?? lanes.find((l) => l.origin === origin)?.destination,
);
const mode = pick(
	modes.map((m) => m.label),
	initialMode,
);
const type = pick(
	shipmentTypes.map((t) => t.label),
	initialType,
);
const weight = Math.min(Math.max(initialWeight, minWeight), maxWeight);

/*
 * The estimate. Written once here for the server render and once, the same,
 * in the script — the table travels to it as JSON on the root.
 */
function estimate(o: string, d: string, m: string, t: string, kg: number) {
	const lane = lanes.find((l) => l.origin === o && l.destination === d);
	const modeRow = modes.find((x) => x.label === m);
	const typeRow = shipmentTypes.find((x) => x.label === t);
	if (!lane || !modeRow || !typeRow) return null;
	const mid = (lane.base + lane.perKg * kg) * modeRow.multiplier * typeRow.multiplier;
	const step = roundTo > 0 ? roundTo : 1;
	const low = Math.round((mid * (1 - band)) / step) * step;
	const high = Math.round((mid * (1 + band)) / step) * step;
	const days = lane.transitDays.map((n) => Math.max(1, Math.ceil(n * modeRow.transitScale)));
	return { low, high, days };
}

const transitText = (days: number[]) =>
	days[0] === days[1] ? `Transit ${days[0]} ${days[0] === 1 ? "day" : "days"}` : `Transit ${days[0]}–${days[1]} days`;

const first = estimate(origin, destination, mode, type, weight);
const rangeText = first && money ? `${money.format(first.low)} – ${money.format(first.high)}` : "No rate on this lane";
const firstTransit = first ? transitText(first.days) : "Ask a planner";

const table = JSON.stringify({
	lanes,
	modes,
	types: shipmentTypes,
	band,
	roundTo,
	currency,
	locale,
	min: minWeight,
	max: maxWeight,
});

const SAFE_SCHEME = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const href = showCta && SAFE_SCHEME.test(quoteHref.trim()) ? quoteHref.trim() : "#";
const numberFmt = new Intl.NumberFormat(locale);
---

{hasRates && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasRates && (
<section
	class="rqe"
	id={id}
	style={`--rqe-bg: ${background};`}
	data-rqe-root
	data-rqe-table={table}
	aria-labelledby={heading ? `${uid}-heading` : undefined}
	aria-label={heading ? undefined : "Route estimator"}
>
	<div class="rqe-card">
		<div class="rqe-copy">
			{
				eyebrow && (
					<p class="rqe-eyebrow">
						<span class="rqe-dot" aria-hidden="true" />
						{eyebrow}
					</p>
				)
			}
			{
				heading && (
					<Heading class="rqe-heading" id={`${uid}-heading`}>
						{heading}
					</Heading>
				)
			}
			{text && <p class="rqe-text">{text}</p>}
			{
				badgeList.length > 0 && (
					<ul class="rqe-badges">
						{badgeList.map((badge) => (
							<li class="rqe-badge">
								<svg viewBox="0 0 8 8" width="8" height="8" aria-hidden="true" focusable="false">
									<circle cx="4" cy="4" r="3.25" fill="none" stroke="currentColor" stroke-width="1.1" />
								</svg>
								{badge}
							</li>
						))}
					</ul>
				)
			}
		</div>

		<div class="rqe-panel">
			<div class="rqe-grid" data-rqe-form>
				<div class="rqe-field">
					<label class="rqe-label" for={`${uid}-origin`}>{L.origin}</label>
					<select class="rqe-control" id={`${uid}-origin`} data-rqe-origin>
						{origins.map((o) => <option selected={o === origin}>{o}</option>)}
					</select>
				</div>
				<div class="rqe-field">
					<label class="rqe-label" for={`${uid}-destination`}>{L.destination}</label>
					<select class="rqe-control" id={`${uid}-destination`} data-rqe-destination>
						{destinations.map((d) => <option selected={d === destination}>{d}</option>)}
					</select>
				</div>
				<div class="rqe-field">
					<label class="rqe-label" for={`${uid}-mode`}>{L.mode}</label>
					<select class="rqe-control" id={`${uid}-mode`} data-rqe-mode>
						{modes.map((m) => <option selected={m.label === mode}>{m.label}</option>)}
					</select>
				</div>
				<div class="rqe-field">
					<label class="rqe-label" for={`${uid}-weight`}>{L.weight}</label>
					<input
						class="rqe-control"
						id={`${uid}-weight`}
						type="number"
						inputmode="decimal"
						min={minWeight}
						max={maxWeight}
						step="any"
						value={weight}
						aria-describedby={`${uid}-hint`}
						data-rqe-weight
					/>
				</div>
				<div class="rqe-field rqe-field--wide">
					<label class="rqe-label" for={`${uid}-type`}>{L.type}</label>
					<select class="rqe-control" id={`${uid}-type`} data-rqe-type>
						{shipmentTypes.map((t) => <option selected={t.label === type}>{t.label}</option>)}
					</select>
				</div>
				<p class="rqe-hint" id={`${uid}-hint`} aria-live="polite" data-rqe-hint></p>
			</div>

			<div class="rqe-result" aria-live="polite" aria-atomic="true" data-rqe-result>
				<span class="rqe-result-label">{L.estimate}</span>
				<strong class="rqe-range" data-rqe-range>{rangeText}</strong>
				<span class="rqe-transit" data-rqe-transit>{firstTransit}</span>
				<span class="rqe-note">
					{L.note}
					<noscript>{` · shown for ${origin} → ${destination}, ${numberFmt.format(weight)} kg; it recalculates with JavaScript on`}</noscript>
				</span>
			</div>

			{showCta && (
			<a class="rqe-cta" href={href}>
				<span>{quoteLabel}</span>
				<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">
					<path
						d="M3 8h9.5M8.5 4l4 4-4 4"
						fill="none"
						stroke="currentColor"
						stroke-width="1.8"
						stroke-linecap="round"
						stroke-linejoin="round"
					/>
				</svg>
			</a>
			)}
		</div>
	</div>
</section>
)}

<style>
	.rqe {
		--rqe-ink: var(--nv-ink, #08110f);
		--rqe-lime: var(--nv-lime, #b7ff39);
		--rqe-muted: var(--nv-muted, #6b7b78);
		--rqe-field: #f6f8f5;
		--rqe-line: #d9deda;
		--rqe-font: var(--nv-font, Manrope, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
		--rqe-font-ui: var(--nv-font-ui, Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);

		box-sizing: border-box;
		padding: 48px 18px;
		background: var(--rqe-bg);
		color: var(--rqe-ink);
		font-family: var(--rqe-font);
	}

	.rqe *,
	.rqe *::before,
	.rqe *::after {
		box-sizing: border-box;
	}

	.rqe-card {
		display: flex;
		flex-direction: column;
		gap: 36px;
		max-width: 1240px;
		margin: 0 auto;
		padding: 36px 22px 22px;
		border-radius: 32px;
		background: var(--rqe-ink);
		overflow: hidden;
	}

	/* ---- copy ---- */
	.rqe-copy {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		min-width: 0;
		font-family: var(--rqe-font-ui);
		color: #fff;
	}

	.rqe-eyebrow {
		display: flex;
		align-items: center;
		gap: 8px;
		margin: 0 0 24px;
		font-size: 12px;
		font-weight: 800;
		line-height: 14.4px;
		letter-spacing: 1.2px;
		text-transform: uppercase;
	}

	.rqe-dot {
		flex: none;
		width: 9px;
		height: 9px;
		border-radius: 100px;
		background: var(--rqe-lime);
	}

	.rqe-heading {
		margin: 0;
		max-width: 470px;
		font-size: 42px;
		font-weight: 700;
		line-height: 1.04;
		letter-spacing: -0.045em;
		color: #fff;
		text-wrap: balance;
	}

	.rqe-text {
		margin: 24px 0 0;
		max-width: 440px;
		font-size: 16px;
		font-weight: 400;
		line-height: 26.4px;
		color: rgb(255 255 255 / 0.65);
	}

	.rqe-badges {
		display: flex;
		flex-wrap: wrap;
		gap: 12px;
		margin: 32px 0 0;
		padding: 0;
		list-style: none;
	}

	.rqe-badge {
		display: inline-flex;
		align-items: center;
		gap: 6px;
		height: 32px;
		padding: 0 13px;
		border-radius: 100px;
		background: #e9ffd0;
		color: #547021;
		font-size: 10px;
		font-weight: 800;
		line-height: 12px;
		text-transform: uppercase;
		white-space: nowrap;
	}

	.rqe-badge svg {
		flex: none;
		color: #7fb62a;
	}

	/* ---- estimator panel ---- */
	.rqe-panel {
		display: grid;
		gap: 16px;
		min-width: 0;
		padding: 18px;
		border-radius: 24px;
		background: #fff;
	}

	.rqe-grid {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: 12px;
	}

	.rqe-field {
		display: grid;
		gap: 7px;
		min-width: 0;
	}

	.rqe-label {
		font-size: 11px;
		font-weight: 800;
		letter-spacing: 0.88px;
		text-transform: uppercase;
		color: var(--rqe-muted);
	}

	/* 16px text so iOS Safari does not zoom the page on focus. */
	.rqe-control {
		width: 100%;
		min-width: 0;
		height: 52px;
		margin: 0;
		padding: 0 14px;
		border: 1px solid var(--rqe-line);
		border-radius: 14px;
		background-color: var(--rqe-field);
		color: var(--rqe-ink);
		font: 500 16px/1.2 var(--rqe-font);
	}

	.rqe-control:hover {
		border-color: #bfc8c2;
	}

	.rqe-control:focus-visible {
		outline: 2px solid var(--rqe-ink);
		outline-offset: 2px;
		border-color: var(--rqe-ink);
	}

	.rqe-control[aria-invalid="true"] {
		border-color: #b3261e;
	}

	.rqe-hint {
		margin: 0;
		font-size: 12px;
		font-weight: 600;
		line-height: 1.4;
		color: #9a2019;
	}

	.rqe-hint:empty {
		display: none;
	}

	.rqe-result {
		display: flex;
		flex-direction: column;
		gap: 8px;
		padding: 14px 18px;
		border-radius: 18px;
		background: #eef8d6;
		color: var(--rqe-ink);
	}

	.rqe-result-label {
		font-size: 13px;
		font-weight: 400;
	}

	.rqe-range {
		font-size: 26px;
		font-weight: 700;
		line-height: 1.35;
		text-align: center;
		font-variant-numeric: tabular-nums;
	}

	.rqe-transit {
		align-self: flex-end;
		font-size: 12px;
		font-weight: 700;
	}

	.rqe-note {
		font-size: 11px;
		font-weight: 400;
		color: #64736d;
	}

	.rqe-cta {
		display: flex;
		align-items: center;
		justify-content: center;
		gap: 8px;
		min-height: 52px;
		padding: 0 20px;
		border-radius: 999px;
		background: var(--rqe-lime);
		color: var(--rqe-ink);
		font: 800 12px/1.2 var(--rqe-font);
		text-align: center;
		text-decoration: none;
	}

	.rqe-cta:hover {
		background: #c8ff38;
	}

	.rqe-cta:focus-visible {
		outline: 2px solid var(--rqe-ink);
		outline-offset: 3px;
	}

	/* ---- tablet: two-column fields ---- */
	@media (min-width: 810px) {
		.rqe {
			padding: 90px 40px;
		}

		.rqe-card {
			gap: 44px;
			padding: 52px 48px 48px;
			border-radius: 36px;
		}

		.rqe-heading {
			font-size: 50px;
		}

		.rqe-panel {
			padding: 26px;
			border-radius: 28px;
		}

		.rqe-grid {
			grid-template-columns: repeat(2, minmax(0, 1fr));
		}

		.rqe-field--wide,
		.rqe-hint {
			grid-column: 1 / -1;
		}

		.rqe-result {
			display: grid;
			grid-template-columns: 1fr auto 1fr;
			align-items: center;
		}

		.rqe-range {
			text-align: center;
		}

		.rqe-transit {
			align-self: center;
			justify-self: end;
			text-align: right;
		}

		.rqe-note {
			grid-column: 1 / -1;
		}

		.rqe-range {
			white-space: nowrap;
		}
	}

	/* ---- desktop: the measured layout ---- */
	@media (min-width: 1200px) {
		.rqe {
			padding: 120px 64px;
		}

		/* Measured: the 638px panel runs past the card's 60px padding and
		   stops 8px from its right edge; reproduced as an 8px right pad. */
		.rqe-card {
			flex-direction: row;
			align-items: center;
			gap: 64px;
			padding: 60px 8px 60px 60px;
		}

		.rqe-copy {
			flex: 0 1 470px;
		}

		.rqe-heading {
			font-size: 54px;
			line-height: 56.16px;
			letter-spacing: -2.43px;
		}

		.rqe-panel {
			flex: 0 1 638px;
		}

		.rqe-grid {
			grid-template-columns: repeat(3, minmax(0, 1fr));
		}
	}

	@media (forced-colors: active) {
		.rqe-card,
		.rqe-panel,
		.rqe-result {
			border: 1px solid CanvasText;
		}

		.rqe-badge {
			border: 1px solid CanvasText;
		}

		.rqe-cta {
			border: 2px solid LinkText;
		}

		.rqe-control:focus-visible,
		.rqe-cta:focus-visible {
			outline-color: Highlight;
		}

		.rqe-dot {
			forced-color-adjust: none;
		}
	}
</style>

<script>
	/*
	 * RouteQuoteEstimator controller.
	 *
	 * Reads the rate table from the root's data attribute, recomputes on every
	 * input and writes the range and transit into an aria-live region. A weight
	 * that is empty, non-numeric or out of range keeps the last valid estimate
	 * and shows a hint instead. Nothing is submitted.
	 *
	 * One closure per instance, every listener on one AbortController, torn
	 * down before a client-side navigation swaps the document.
	 */
	interface Lane {
		origin: string;
		destination: string;
		base: number;
		perKg: number;
		transitDays: [number, number];
	}
	interface Table {
		lanes: Lane[];
		modes: { label: string; multiplier: number; transitScale: number }[];
		types: { label: string; multiplier: number }[];
		band: number;
		roundTo: number;
		currency: string;
		locale: string;
		min: number;
		max: number;
	}
	interface Instance {
		destroy(): void;
	}

	function initRouteEstimator(root: HTMLElement): Instance | undefined {
		/* Idempotent: booting twice is a no-op, not a second set of listeners. */
		if (root.dataset.rqeReady === "") return undefined;

		let table: Table;
		try {
			table = JSON.parse(root.dataset.rqeTable ?? "");
		} catch {
			return undefined;
		}

		const q = <T extends Element>(sel: string) => root.querySelector<T>(sel);
		const origin = q<HTMLSelectElement>("[data-rqe-origin]");
		const destination = q<HTMLSelectElement>("[data-rqe-destination]");
		const mode = q<HTMLSelectElement>("[data-rqe-mode]");
		const type = q<HTMLSelectElement>("[data-rqe-type]");
		const weight = q<HTMLInputElement>("[data-rqe-weight]");
		const range = q<HTMLElement>("[data-rqe-range]");
		const transit = q<HTMLElement>("[data-rqe-transit]");
		const hint = q<HTMLElement>("[data-rqe-hint]");
		const form = q<HTMLElement>("[data-rqe-form]");
		if (!origin || !destination || !mode || !type || !weight || !range || !transit || !hint || !form) {
			return undefined;
		}

		root.dataset.rqeReady = "";
		const controller = new AbortController();
		const { signal } = controller;

		const money = new Intl.NumberFormat(table.locale, {
			style: "currency",
			currency: table.currency,
			maximumFractionDigits: 0,
		});
		const num = new Intl.NumberFormat(table.locale);
		const served = { range: range.textContent, transit: transit.textContent };

		let lastKg = Number(weight.value) || table.min;

		function readWeight(): number | null {
			const raw = weight!.value.trim();
			if (raw === "") return null;
			const kg = Number(raw);
			if (!Number.isFinite(kg) || kg < table.min || kg > table.max) return null;
			return kg;
		}

		function update() {
			const kg = readWeight();
			if (kg === null) {
				weight!.setAttribute("aria-invalid", "true");
				hint!.textContent = `Enter a weight from ${num.format(table.min)} to ${num.format(table.max)} kg. Showing the last valid estimate.`;
			} else {
				weight!.removeAttribute("aria-invalid");
				hint!.textContent = "";
				lastKg = kg;
			}

			const lane = table.lanes.find((l) => l.origin === origin!.value && l.destination === destination!.value);
			const m = table.modes.find((x) => x.label === mode!.value);
			const t = table.types.find((x) => x.label === type!.value);
			if (!lane || !m || !t) {
				range!.textContent = "No rate on this lane";
				transit!.textContent = "Ask a planner";
				return;
			}

			const mid = (lane.base + lane.perKg * lastKg) * m.multiplier * t.multiplier;
			const step = table.roundTo > 0 ? table.roundTo : 1;
			const low = Math.round((mid * (1 - table.band)) / step) * step;
			const high = Math.round((mid * (1 + table.band)) / step) * step;
			const [a, b] = lane.transitDays.map((n) => Math.max(1, Math.ceil(n * m.transitScale)));

			const nextRange = `${money.format(low)} – ${money.format(high)}`;
			const nextTransit = a === b ? `Transit ${a} ${a === 1 ? "day" : "days"}` : `Transit ${a}–${b} days`;
			/* Write only on change, so the live region does not repeat itself. */
			if (range!.textContent !== nextRange) range!.textContent = nextRange;
			if (transit!.textContent !== nextTransit) transit!.textContent = nextTransit;
		}

		form.addEventListener("input", update, { signal });
		form.addEventListener("change", update, { signal });

		/* The browser may have restored different values on back/forward. */
		update();

		function destroy() {
			controller.abort();
			range!.textContent = served.range;
			transit!.textContent = served.transit;
			hint!.textContent = "";
			weight!.removeAttribute("aria-invalid");
			delete root.dataset.rqeReady;
		}

		document.addEventListener("astro:before-swap", destroy, { once: true, signal });

		return { destroy };
	}

	const boot = () =>
		document.querySelectorAll<HTMLElement>("[data-rqe-root]").forEach(initRouteEstimator);

	if (document.readyState === "loading") {
		document.addEventListener("DOMContentLoaded", boot, { once: true });
	} else {
		boot();
	}

	/* Fires on every view transition in a project using <ClientRouter />. */
	document.addEventListener("astro:page-load", boot);
</script>

===== END src/components/RouteQuoteEstimator.astro =====

===== FILE (required): src/components/SplitIntroFaq.astro =====
---
/**
 * SplitIntroFaq
 *
 * A text-only FAQ in two parts: an intro column — optional label, a big
 * heading and a short paragraph — and a list of full-width question rows
 * separated by hairlines. From 1024px of its own width the intro sits beside
 * the list and stays in view while the list scrolls; from 700px the heading
 * and paragraph share a line above it; narrower, everything stacks.
 *
 * Each row carries a ring on the right with a plus that turns into a minus
 * when it is open; the open row is marked by a heavier rule and the answer
 * sits on the question's own left edge.
 *
 * Built on `<details>` / `<summary>`: the toggle is the browser's own and it
 * works with no script. There is no script at all. `exclusive` hands the
 * one-at-a-time rule to the browser through `<details name>`.
 *
 * Reads --nv-* design tokens from any ancestor, with fallbacks.
 */

export interface SplitIntroFaqItem {
	question: string;
	answer: string;
}

export interface SplitIntroFaqProps {
	heading?: string;
	headingLevel?: "h2" | "h3";
	/** Paragraph under the heading. `""` drops it. */
	intro?: string;
	/** Small label above the heading, with the lime dot. `""` drops it. */
	eyebrow?: string;
	items?: SplitIntroFaqItem[];
	/** Which row starts open, counting from 1. `0` (default) opens none. */
	initialOpen?: number;
	/** Only one row open at a time, through the native `<details name>` group. */
	exclusive?: boolean;
	/** The section surface. */
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends SplitIntroFaqProps {}

const {
	heading = "",
	headingLevel = "h2",
	intro = "",
	eyebrow = "",
	items = [],
	initialOpen = 0,
	exclusive = false,
	background = "#ffffff",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;

/* `name` is document-wide, so each instance gets its own group. */
const uid = id ?? `sif-${Math.random().toString(36).slice(2, 9)}`;

/* A row needs a question and an answer; anything short of that is dropped.
   No rows, no section — the heading and eyebrow alone say nothing. */
const list = items.filter(
	(item) =>
		typeof item?.question === "string" &&
		item.question.trim() !== "" &&
		typeof item.answer === "string" &&
		item.answer.trim() !== "",
);

const openAt = Math.trunc(Number(initialOpen));
const openIndex = Number.isFinite(openAt) && openAt >= 1 && openAt <= list.length ? openAt - 1 : -1;
const hasIntro = Boolean(heading || intro || eyebrow);
---

{list.length > 0 && (<>{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section
	class:list={["sif", className]}
	id={id}
	style={`--sif-bg: ${background};`}
	aria-labelledby={heading ? `${uid}-heading` : undefined}
	aria-label={heading ? undefined : "Frequently asked questions"}
>
	<div class:list={["sif-inner", { "sif-inner--split": hasIntro }]}>
		{
			hasIntro && (
				<div class="sif-intro-col">
					<div class="sif-intro-stick">
						{eyebrow && (
							<p class="sif-eyebrow">
								<span class="sif-dot" aria-hidden="true" />
								<span>{eyebrow}</span>
							</p>
						)}
						{heading && <Heading class="sif-heading" id={`${uid}-heading`}>{heading}</Heading>}
						{intro && <p class="sif-intro">{intro}</p>}
					</div>
				</div>
			)
		}
		<div class="sif-list">
			{
				list.map((item, index) => (
					<details class="sif-row" name={exclusive ? uid : undefined} open={index === openIndex}>
						<summary class="sif-q">
							<span class="sif-q-text">{item.question}</span>
							<span class="sif-icon" aria-hidden="true" />
						</summary>
						<div class="sif-a">
							<p>{item.answer}</p>
						</div>
					</details>
				))
			}
		</div>
	</div>
</section></>)}

<style>
	.sif {
		--sif-ink: var(--nv-ink, #08110f);
		--sif-lime: var(--nv-lime, #c8ff38);
		--sif-font: var(--nv-font, Manrope, ui-sans-serif, system-ui, sans-serif);
		--sif-rule: rgb(8 17 15 / 0.12);
		--sif-soft: rgb(8 17 15 / 0.74);
		--sif-ease: cubic-bezier(0.22, 1, 0.36, 1);

		container-type: inline-size;
		box-sizing: border-box;
		background: var(--sif-bg);
		color: var(--sif-ink);
		font-family: var(--sif-font);
	}

	.sif *,
	.sif *::before,
	.sif *::after {
		box-sizing: border-box;
	}

	.sif-inner {
		display: grid;
		gap: 36px;
		max-width: 1240px;
		margin: 0 auto;
		padding: 64px 20px;
	}

	.sif-intro-col,
	.sif-list {
		min-width: 0;
	}

	/* ---- intro ---- */

	.sif-eyebrow {
		display: flex;
		align-items: center;
		gap: 10px;
		margin: 0 0 18px;
		font: 700 14px/1.3 var(--sif-font);
		color: var(--sif-ink);
	}

	.sif-dot {
		flex: none;
		width: 10px;
		height: 10px;
		border-radius: 50%;
		background: var(--sif-lime);
		box-shadow: 0 0 0 4px rgb(200 255 56 / 0.28);
	}

	.sif-heading {
		max-width: 14ch;
		margin: 0;
		font: 700 clamp(34px, 8.6cqi, 60px) / 1 var(--sif-font);
		letter-spacing: -0.045em;
		text-wrap: balance;
		overflow-wrap: anywhere;
	}

	.sif-intro {
		max-width: 40ch;
		margin: 18px 0 0;
		font: 500 17px/1.6 var(--sif-font);
		letter-spacing: -0.01em;
		color: var(--sif-soft);
		overflow-wrap: anywhere;
	}

	/* ---- questions ---- */

	.sif-list {
		border-bottom: 1px solid var(--sif-rule);
	}

	.sif-row {
		border-top: 1px solid var(--sif-rule);
		transition: border-color 0.2s ease;
	}

	/* The open row is marked by a heavier rule, not a tinted box. */
	.sif-row[open] {
		border-top-color: var(--sif-ink);
		box-shadow: inset 0 1px 0 var(--sif-ink);
	}

	.sif-q {
		display: flex;
		align-items: center;
		gap: 20px;
		min-height: 76px;
		padding: 18px 0;
		cursor: pointer;
		list-style: none;
	}

	.sif-q::-webkit-details-marker {
		display: none;
	}

	.sif-q:focus-visible {
		outline: 2px solid var(--sif-ink);
		outline-offset: 4px;
		border-radius: 4px;
	}

	.sif-q-text {
		flex: 1 1 auto;
		min-width: 0;
		font: 700 clamp(17px, 3.2cqi, 20px) / 1.35 var(--sif-font);
		letter-spacing: -0.015em;
		overflow-wrap: anywhere;
	}

	/* A ring with a plus drawn from two bars; the vertical bar folds away when open. */
	.sif-icon {
		position: relative;
		flex: none;
		width: 34px;
		height: 34px;
		border: 1.5px solid var(--sif-ink);
		border-radius: 50%;
		transition: background-color 0.25s var(--sif-ease), border-color 0.25s var(--sif-ease);
	}

	.sif-icon::before,
	.sif-icon::after {
		content: "";
		position: absolute;
		top: 50%;
		left: 50%;
		width: 12px;
		height: 2px;
		margin: -1px 0 0 -6px;
		border-radius: 1px;
		background: var(--sif-ink);
		transition: transform 0.3s var(--sif-ease);
	}

	.sif-icon::after {
		transform: rotate(90deg);
	}

	.sif-row[open] .sif-icon {
		border-color: var(--sif-ink);
		background: var(--sif-ink);
	}

	.sif-row[open] .sif-icon::before,
	.sif-row[open] .sif-icon::after {
		background: var(--sif-lime);
	}

	.sif-row[open] .sif-icon::after {
		transform: rotate(0deg);
	}

	@media (hover: hover) {
		.sif-row:not([open]) .sif-q:hover .sif-icon {
			background: var(--sif-lime);
		}
	}

	.sif-a {
		padding: 0 54px 26px 0;
	}

	.sif-a p {
		max-width: 62ch;
		margin: 0;
		font: 500 16px/1.65 var(--sif-font);
		color: var(--sif-soft);
		overflow-wrap: anywhere;
	}

	/* ---- tablet: heading and paragraph share a line ---- */

	@container (min-width: 700px) {
		.sif-inner {
			padding: 88px 36px;
			gap: 48px;
		}

		.sif-intro-stick {
			display: grid;
			grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
			column-gap: 40px;
			align-items: end;
		}

		.sif-eyebrow {
			grid-column: 1 / -1;
		}

		.sif-heading {
			max-width: 16ch;
			font-size: clamp(36px, 6.2cqi, 52px);
		}

		.sif-intro {
			margin: 0;
		}
	}

	/* ---- desktop: the intro stands beside the list ---- */

	@container (min-width: 1024px) {
		.sif-inner {
			padding: 112px 40px;
		}

		.sif-inner--split {
			grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
			column-gap: 72px;
			align-items: start;
		}

		.sif-intro-stick {
			position: sticky;
			top: 96px;
			display: block;
		}

		.sif-heading {
			max-width: 14ch;
			font-size: clamp(40px, 4.4cqi, 60px);
		}

		.sif-intro {
			margin-top: 22px;
		}

		.sif-q {
			min-height: 84px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.sif-row,
		.sif-icon,
		.sif-icon::before,
		.sif-icon::after {
			transition: none;
		}
	}

	@media (forced-colors: active) {
		.sif-dot,
		.sif-icon::before,
		.sif-icon::after,
		.sif-row[open] .sif-icon::before,
		.sif-row[open] .sif-icon::after {
			background: CanvasText;
		}

		.sif-row[open] .sif-icon {
			background: Canvas;
		}

		.sif-icon {
			border-color: CanvasText;
		}
	}
</style>

===== END src/components/SplitIntroFaq.astro =====

===== FILE (required): src/components/DarkImageCtaCard.astro =====
---
/**
 * DarkImageCtaCard
 *
 * A rounded ink card centred in a white band: a lime uppercase eyebrow, a big
 * white heading, a paragraph and two pill links on the left; a photograph
 * edge to edge on the right, clipped by the card's radius. The photo slides
 * in from 18px to the right and fades up the first time the card enters the
 * viewport.
 *
 * Side by side from 810px. On a phone the reference drops the photo and keeps
 * the ink card; `phoneImage` can put it back above or below the copy.
 *
 * Reads --nv-* design tokens from any ancestor, with fallbacks.
 */

export interface DarkImageCtaLink {
	label: string;
	href: string;
}

export interface DarkImageCtaCardProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	text?: string;
	/** The lime pill. `null` drops it. */
	primaryCta?: DarkImageCtaLink | null;
	/** The outline pill. `null` drops it. */
	secondaryCta?: DarkImageCtaLink | null;
	/** The photo. `src: ""` renders the labelled placeholder instead. */
	image?: { src?: string; alt?: string };
	/** Below 810px: `"hidden"` (as measured), or stack it `"top"` / `"bottom"`. */
	phoneImage?: "hidden" | "top" | "bottom";
	/** The band around the card. */
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends DarkImageCtaCardProps {}

const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	text = "",
	primaryCta: primaryProp = null,
	secondaryCta: secondaryProp = null,
	image: imageProp,
	phoneImage = "hidden",
	background = "#ffffff",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	class: className,
	id,
} = Astro.props;

/* No default photo: with no `src` the labelled placeholder shows. A photo
   given no alt is decorative. */
const image = { src: imageProp?.src ?? "", alt: imageProp?.alt ?? "" };

/* A pill needs a label and a link; the card needs a heading to render at all. */
const primaryCta = primaryProp?.label && primaryProp.href ? primaryProp : null;
const secondaryCta = secondaryProp?.label && secondaryProp.href ? secondaryProp : null;

const Heading = headingLevel;

const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");

/* Measured slot: 627 × 300 at desktop (~2.1:1); the file is 1300 × 620. */
const IMAGE_SPEC = "~2.1:1 · 1300 × 620 px";
---

{heading && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{heading && (
<section
	class:list={["dicc", className]}
	id={id}
	style={`--dicc-band: ${background};`}
	data-dicc-root
	data-phone-image={phoneImage}
>
	<div class="dicc-card">
		<div class="dicc-copy">
			{eyebrow && <p class="dicc-eyebrow">{eyebrow}</p>}
			<Heading class="dicc-heading">{heading}</Heading>
			{text && <p class="dicc-text">{text}</p>}
			{
				(primaryCta || secondaryCta) && (
					<div class="dicc-actions">
						{primaryCta && (
							<a class="dicc-btn dicc-btn--primary" href={safeHref(primaryCta.href)}>
								{primaryCta.label}
								<span class="dicc-arrow" aria-hidden="true">↗</span>
							</a>
						)}
						{secondaryCta && (
							<a class="dicc-btn dicc-btn--outline" href={safeHref(secondaryCta.href)}>
								{secondaryCta.label}
								<span class="dicc-arrow" aria-hidden="true">↗</span>
							</a>
						)}
					</div>
				)
			}
		</div>
		<div class="dicc-media" data-dicc-media>
			{
				image.src ? (
					<img src={image.src} alt={image.alt} width="1300" height="620" loading="lazy" decoding="async" />
				) : (
					<div class="dicc-ph" role="img" aria-label="Image placeholder: wide landscape photo">
						<div class="dicc-ph-text" aria-hidden="true">
							<span class="dicc-ph-label">Image</span>
							<span class="dicc-ph-hint">Wide landscape photo</span>
							<span class="dicc-ph-spec">{IMAGE_SPEC}</span>
						</div>
					</div>
				)
			}
		</div>
	</div>
</section>
)}

<style>
	.dicc {
		--dicc-ink: #07130e;
		--dicc-lime: var(--nv-lime, #c8ff38);
		--dicc-font: var(--nv-font-ui, Inter, ui-sans-serif, system-ui, sans-serif);
		--dicc-ease: cubic-bezier(0.22, 1, 0.36, 1);

		box-sizing: border-box;
		padding: 0 16px 24px;
		background: var(--dicc-band);
		font-family: var(--dicc-font);
	}

	.dicc *,
	.dicc *::before,
	.dicc *::after {
		box-sizing: border-box;
	}

	.dicc-card {
		display: flex;
		flex-direction: column;
		max-width: 1120px;
		margin: 0 auto;
		border-radius: 24px;
		background: var(--dicc-ink);
		overflow: clip;
	}

	.dicc-copy {
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: flex-start;
		gap: 14px;
		padding: 34px 22px;
		min-width: 0;
	}

	.dicc-eyebrow {
		margin: 0;
		font: 800 11px/13.2px var(--dicc-font);
		letter-spacing: 1px;
		text-transform: uppercase;
		color: var(--dicc-lime);
	}

	.dicc-heading {
		margin: 0;
		max-width: 420px;
		font: 800 42px/1 var(--dicc-font);
		letter-spacing: normal;
		color: #fff;
	}

	.dicc-text {
		margin: 0;
		max-width: 360px;
		font: 400 15px/1.45 var(--dicc-font);
		color: rgb(255 255 255 / 0.75);
	}

	.dicc-actions {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: 8px;
	}

	.dicc-btn {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: 6px;
		min-height: 44px;
		padding: 0 14px;
		border-radius: 999px;
		font: 800 13px/15.6px var(--dicc-font);
		text-decoration: none;
		white-space: nowrap;
		transition:
			transform 0.3s var(--dicc-ease),
			background-color 0.3s var(--dicc-ease);
	}

	.dicc-btn--primary {
		background: var(--dicc-lime);
		color: var(--dicc-ink);
	}

	.dicc-btn--outline {
		background: transparent;
		color: #fff;
		font-weight: 700;
		box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.3);
	}

	.dicc-btn--primary:hover {
		transform: scale(1.04) translateY(-2px);
	}

	.dicc-btn--outline:hover {
		background: rgb(255 255 255 / 0.1);
		transform: scale(1.1) translateY(-2px);
	}

	.dicc-btn:focus-visible {
		outline: 2px solid var(--dicc-lime);
		outline-offset: 3px;
	}

	.dicc-arrow {
		font-size: 12px;
	}

	.dicc-media {
		position: relative;
		display: none;
		min-width: 0;
		aspect-ratio: 16 / 9;
	}

	.dicc[data-phone-image="top"] .dicc-media,
	.dicc[data-phone-image="bottom"] .dicc-media {
		display: block;
	}

	.dicc[data-phone-image="top"] .dicc-media {
		order: -1;
	}

	.dicc-media img {
		position: absolute;
		inset: 0;
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	/* Appear: the hidden start pose exists only once the script has marked
	   the root ready, so no-JS and reduced motion show the photo at rest. */
	.dicc-media {
		transition:
			opacity 0.7s var(--dicc-ease),
			transform 0.7s var(--dicc-ease);
	}

	.dicc[data-dicc-ready]:not([data-in]) .dicc-media {
		opacity: 0;
		transform: translateX(18px);
	}

	/* Placeholder for `src: ""`: fills the same box as the photo. */
	.dicc-ph {
		position: absolute;
		inset: 0;
		display: grid;
		place-items: center;
		padding: 16px;
		text-align: center;
		background: repeating-linear-gradient(135deg, #1a2621 0 12px, #1f2c27 12px 24px);
		color: rgb(247 250 247 / 0.7);
	}

	.dicc-ph-text {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 8px;
	}

	.dicc-ph-label {
		font: 600 12px/1 var(--dicc-font);
		letter-spacing: 0.08em;
		text-transform: uppercase;
	}

	.dicc-ph-hint {
		max-width: 28ch;
		font: 500 14px/1.4 var(--dicc-font);
	}

	.dicc-ph-spec {
		font: 400 12px/1.2 var(--dicc-font);
	}

	/* ---- tablet and up: side by side ---- */
	@media (min-width: 810px) {
		.dicc {
			padding: 0 30px 30px;
		}

		.dicc-card {
			display: grid;
			grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
			min-height: 300px;
		}

		.dicc-copy {
			padding: 42px 32px 42px 40px;
		}

		.dicc-heading {
			font-size: 36px;
		}

		.dicc-actions {
			gap: 12px;
		}

		.dicc-btn {
			padding: 0 20px;
		}

		.dicc .dicc-media,
		.dicc[data-phone-image] .dicc-media {
			display: block;
			order: 0;
			aspect-ratio: auto;
		}
	}

	/* ---- desktop: the measured 515 / 605 split of a 1120 card ---- */
	@media (min-width: 1200px) {
		.dicc-card {
			grid-template-columns: minmax(0, 46%) minmax(0, 1fr);
		}

		.dicc-copy {
			padding: 42px 0 42px 44px;
		}

		.dicc-heading {
			font-size: 42px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.dicc-btn--primary:hover,
		.dicc-btn--outline:hover {
			transform: none;
		}

		.dicc-media {
			transition: none;
		}

		.dicc[data-dicc-ready]:not([data-in]) .dicc-media {
			opacity: 1;
			transform: none;
		}
	}

	@media (forced-colors: active) {
		.dicc-card {
			border: 1px solid CanvasText;
		}

		.dicc-btn {
			border: 1px solid ButtonText;
		}
	}
</style>

<script>
	/*
	 * Appear-on-scroll for the photo. One IntersectionObserver per instance,
	 * scoped to its root, torn down on astro:before-swap. It sets `data-in`
	 * once and stops watching. Under reduced motion it marks the root in
	 * straight away, so nothing ever starts hidden.
	 */
	function initDarkImageCtaCard(root: HTMLElement) {
		if (root.dataset.diccReady === "") return;
		const media = root.querySelector<HTMLElement>("[data-dicc-media]");
		if (!media) return;

		const controller = new AbortController();
		const { signal } = controller;
		const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

		if (reduced || !("IntersectionObserver" in window)) {
			root.dataset.in = "";
			return;
		}

		root.dataset.diccReady = "";

		const observer = new IntersectionObserver(
			(entries) => {
				if (entries.some((entry) => entry.isIntersecting)) {
					root.dataset.in = "";
					observer.disconnect();
				}
			},
			{ threshold: 0.2 },
		);
		observer.observe(root);
		signal.addEventListener("abort", () => observer.disconnect());

		document.addEventListener(
			"astro:before-swap",
			() => {
				controller.abort();
				delete root.dataset.diccReady;
			},
			{ once: true, signal },
		);
	}

	const boot = () =>
		document.querySelectorAll<HTMLElement>("[data-dicc-root]").forEach(initDarkImageCtaCard);

	if (document.readyState === "loading") {
		document.addEventListener("DOMContentLoaded", boot, { once: true });
	} else {
		boot();
	}

	document.addEventListener("astro:page-load", boot);
</script>

===== END src/components/DarkImageCtaCard.astro =====

===== FILE (required): public/dark-image-cta-card/winter-highway.webp =====
[binary asset — download https://astro.baysixmedia.com/dark-image-cta-card/winter-highway.webp and save it to public/dark-image-cta-card/winter-highway.webp]

===== FILE (required): src/components/LimeContactFooter.astro =====
---
/**
 * LimeContactFooter
 *
 * A dark green-black footer: logo tile, two-line wordmark, tagline and icon
 * links on the left; Explore and Company link columns and a Contact column
 * (phone, email, address, a lime pill) on the right; a hairline-boxed bottom
 * bar with legal links and the copyright. Static markup, no script.
 *
 * Nothing about the business is assumed: no name, tagline, link, contact
 * detail, pill or legal link is shown unless it is passed, a block with
 * nothing in it is not drawn, and with no content at all nothing renders.
 *
 * Reads --nv-* tokens from any ancestor, with fallbacks.
 */

export interface LimeFooterLink {
	label: string;
	href: string;
}

export interface LimeFooterColumn {
	heading: string;
	links: LimeFooterLink[];
}

export type LimeFooterGlyph = "globe" | "play" | "chat" | "mail" | "phone" | "pin" | "rss" | "link";

export interface LimeFooterSocial {
	/** Accessible name, e.g. "Halcyon on video". */
	label: string;
	href: string;
	glyph: LimeFooterGlyph;
	/** Icon colour. Each defaults to its own hue, as in the reference. */
	color?: string;
}

export interface LimeFooterContact {
	heading?: string;
	phone?: string;
	email?: string;
	/** Lines of the postal address. */
	address?: string[];
}

export interface LimeContactFooterProps {
	/** First wordmark line. No name and no logo drops the logo link. */
	brandName?: string;
	/** Second, lime line of the wordmark. `""` drops it. */
	brandSub?: string;
	brandHref?: string;
	/** Replaces the drawn logo tile and wordmark with an image. */
	logo?: { src: string; alt: string; width?: number; height?: number };
	tagline?: string;
	social?: LimeFooterSocial[];
	columns?: LimeFooterColumn[];
	/** `null` drops the whole contact column. */
	contact?: LimeFooterContact | null;
	/** The lime pill under the contact details. `null` drops it. */
	cta?: { label: string; href: string } | null;
	legalLinks?: LimeFooterLink[];
	/** Name in the copyright line. Defaults to the brand; with neither, no line. */
	copyrightName?: string;
	/** Defaults to the year the site was built. */
	year?: number;
	navLabel?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends LimeContactFooterProps {}

const {
	brandName = "",
	brandSub = "",
	brandHref = "/",
	logo,
	tagline = "",
	social = [],
	columns = [],
	contact = null,
	cta = null,
	legalLinks = [],
	copyrightName,
	year = new Date().getFullYear(),
	navLabel = "Footer",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
	id,
} = Astro.props;

/* A dialable href from a displayed number: keep a leading + and the digits. */
const telHref = (number: string) => {
	const trimmed = number.trim();
	const digits = trimmed.replace(/\D/g, "");
	return digits ? `tel:${trimmed.startsWith("+") ? "+" : ""}${digits}` : "";
};

/* No name, no copyright line: never "Copyright 2026 ." */
const owner = copyrightName ?? (brandName ? (brandSub ? `${brandName} ${brandSub}` : brandName) : "");
/* A call link only with somewhere to dial: words in `phone` are not a number. */
const callHref = contact?.phone ? telHref(contact.phone) : "";
const hasDetails = Boolean(callHref || contact?.email || (contact?.address && contact.address.length > 0));
const showContact = Boolean(contact && (hasDetails || cta));
const hasMark = Boolean(logo || brandName);
const hasBrand = Boolean(hasMark || tagline || social.length > 0);
const hasCols = columns.length > 0 || showContact || Boolean(cta);
const hasBottom = legalLinks.length > 0 || Boolean(owner);
const hasContent = hasBrand || hasCols || hasBottom;

/* Generic 24px line glyphs, no third-party logos. */
const GLYPHS: Record<LimeFooterGlyph, string> = {
	globe: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-9 9h18M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3Z",
	play: "M4.5 6.5A2.5 2.5 0 0 1 7 4h10a2.5 2.5 0 0 1 2.5 2.5v11A2.5 2.5 0 0 1 17 20H7a2.5 2.5 0 0 1-2.5-2.5v-11ZM10 9v6l5-3-5-3Z",
	chat: "M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v9a1.5 1.5 0 0 1-1.5 1.5H10l-4.5 4v-4h0A1.5 1.5 0 0 1 4 14.5v-9ZM8 9h8M8 12h5",
	mail: "M3.5 6.5A1.5 1.5 0 0 1 5 5h14a1.5 1.5 0 0 1 1.5 1.5v11A1.5 1.5 0 0 1 19 19H5a1.5 1.5 0 0 1-1.5-1.5v-11Zm.5.5 8 6 8-6",
	phone: "M6.5 3.5h3l1.5 4-2 1.5a11 11 0 0 0 6 6l1.5-2 4 1.5v3a2 2 0 0 1-2 2A16.5 16.5 0 0 1 4.5 5.5a2 2 0 0 1 2-2Z",
	pin: "M12 21s-6.5-5.7-6.5-11a6.5 6.5 0 0 1 13 0c0 5.3-6.5 11-6.5 11Zm0-8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z",
	rss: "M5 5a14 14 0 0 1 14 14M5 11a8 8 0 0 1 8 8M6 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z",
	link: "M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1",
};
---

{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<footer class="lcf" id={id} data-lcf-root>
	{(hasBrand || hasCols) && (
	<div class="lcf-top">
		{hasBrand && (
		<div class="lcf-brand">
			{hasMark && (
			<a class="lcf-logo" href={brandHref} aria-label={logo ? undefined : `${brandName} home`}>
				{
					logo ? (
						<img
							class="lcf-logo-img"
							src={logo.src}
							alt={logo.alt}
							width={logo.width ?? 190}
							height={logo.height ?? 50}
						/>
					) : (
						<>
							<span class="lcf-tile" aria-hidden="true">
								<svg viewBox="0 0 32 32" focusable="false">
									<rect x="4" y="4" width="7" height="24" rx="2" fill="var(--nv-lime, #c8ff38)" />
									<rect x="21" y="4" width="7" height="24" rx="2" fill="#fff" />
									<path d="M11 18.5 21 10v6.2L11 24.7z" fill="var(--nv-lime, #c8ff38)" />
								</svg>
							</span>
							<span class="lcf-wordmark" aria-hidden="true">
								<span class="lcf-word">{brandName}</span>
								{brandSub && <span class="lcf-sub">{brandSub}</span>}
							</span>
						</>
					)
				}
			</a>
			)}
			{tagline && <p class="lcf-tagline">{tagline}</p>}
			{
				social.length > 0 && (
					<ul class="lcf-social" role="list">
						{social.map((s) => (
							<li>
								<a class="lcf-social-link" href={s.href} aria-label={s.label} style={s.color ? `--lcf-glyph:${s.color}` : undefined}>
									<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
										<path d={GLYPHS[s.glyph]} fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
									</svg>
								</a>
							</li>
						))}
					</ul>
				)
			}
		</div>
		)}

		{hasCols && (
		<nav class="lcf-cols" aria-label={navLabel}>
			{
				columns.map((col) => (
					<div class="lcf-col">
						<h2 class="lcf-heading">{col.heading}</h2>
						<ul class="lcf-links" role="list">
							{col.links.map((link) => (
								<li>
									<a class="lcf-link" href={link.href}>{link.label}</a>
								</li>
							))}
						</ul>
					</div>
				))
			}
			{
				contact && showContact && (
					<div class="lcf-col lcf-col--contact">
						{contact.heading && <h2 class="lcf-heading lcf-heading--lime">{contact.heading}</h2>}
						{hasDetails && (
						<address class="lcf-address">
							{callHref && <a class="lcf-detail" href={callHref}>{contact.phone}</a>}
							{contact.email && <a class="lcf-detail" href={`mailto:${contact.email}`}>{contact.email}</a>}
							{contact.address && contact.address.length > 0 && (
								<span class="lcf-postal">
									{contact.address.map((line, i) => (
										<>
											{line}
											{i < contact.address!.length - 1 && <br />}
										</>
									))}
								</span>
							)}
						</address>
						)}
						{cta && <a class="lcf-cta" href={cta.href}>{cta.label}</a>}
					</div>
				)
			}
			{!showContact && cta && <div class="lcf-col lcf-col--contact"><a class="lcf-cta" href={cta.href}>{cta.label}</a></div>}
		</nav>
		)}
	</div>
	)}

	{hasBottom && (
	<div class="lcf-bottom">
		{
			legalLinks.length > 0 && (
				<nav class="lcf-legal" aria-label="Legal">
					<ul role="list">
						{legalLinks.map((link) => (
							<li>
								<a class="lcf-legal-link" href={link.href}>{link.label}</a>
							</li>
						))}
					</ul>
				</nav>
			)
		}
		{owner && <p class="lcf-copy">Copyright {year} {owner}.</p>}
	</div>
	)}
</footer>
)}

<style>
	.lcf {
		--lcf-ink: var(--nv-ink, #08110f);
		--lcf-lime: var(--nv-lime, #c8ff38);
		--lcf-font: var(--nv-font, Manrope, ui-sans-serif, system-ui, sans-serif);
		--lcf-font-ui: var(--nv-font-ui, Inter, ui-sans-serif, system-ui, sans-serif);

		box-sizing: border-box;
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 40px;
		width: 100%;
		padding: 64px 20px 28px;
		overflow: hidden;
		background: linear-gradient(135deg, #07100d 0%, #08150f 62%, #10241a 100%);
		color: #fff;
		font-family: var(--lcf-font);
	}

	.lcf *,
	.lcf *::before,
	.lcf *::after {
		box-sizing: border-box;
	}

	.lcf ul {
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.lcf p,
	.lcf h2 {
		margin: 0;
	}

	/* Listed by class rather than `.lcf a`: Astro's scoping attribute would
	   give a descendant `a` rule more weight than each link's own colour. */
	.lcf-logo,
	.lcf-social-link,
	.lcf-link,
	.lcf-detail,
	.lcf-legal-link,
	.lcf-cta {
		text-decoration: none;
	}

	.lcf-logo {
		color: #fff;
	}

	/* ---- top: brand + columns ---- */
	.lcf-top {
		display: flex;
		flex-direction: column;
		gap: 28px;
		width: 100%;
		max-width: 1180px;
		padding: 36px 0 28px;
	}

	.lcf-brand {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 14px;
		max-width: 350px;
	}

	.lcf-logo {
		display: inline-flex;
		align-items: center;
		gap: 10px;
		border-radius: 8px;
	}

	.lcf-tile {
		display: grid;
		place-items: center;
		width: 50px;
		height: 50px;
		flex: none;
		border-radius: 8px;
		background: rgb(255 255 255 / 0.06);
	}

	.lcf-tile svg {
		width: 38px;
		height: 38px;
	}

	.lcf-logo-img {
		display: block;
		height: 50px;
		width: auto;
	}

	.lcf-wordmark {
		display: flex;
		flex-direction: column;
		font-family: var(--lcf-font-ui);
		font-weight: 700;
		text-transform: uppercase;
	}

	.lcf-word {
		font-size: 25px;
		line-height: 30px;
		letter-spacing: 1.75px;
		color: #fff;
	}

	.lcf-sub {
		font-size: 17px;
		line-height: 20.4px;
		letter-spacing: 4.08px;
		color: #b7ff39;
	}

	.lcf-tagline {
		font-size: 19px;
		font-weight: 500;
		line-height: 27.55px;
		color: rgb(255 255 255 / 0.66);
	}

	/* 24px glyphs in 44px targets. The row pulls itself back by the target
	   padding so the first glyph still lines up with the tagline. */
	.lcf .lcf-social {
		display: flex;
		align-items: center;
		margin: -6px 0 -6px -10px;
	}

	.lcf-social-link {
		display: grid;
		place-items: center;
		width: 44px;
		height: 44px;
		border-radius: 10px;
		color: var(--lcf-glyph, rgb(255 255 255 / 0.78));
	}

	.lcf-social-link svg {
		width: 24px;
		height: 24px;
	}

	/* ---- columns ---- */
	.lcf-cols {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 155px));
		gap: 26px 20px;
		padding: 10px;
	}

	.lcf-col {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 14px;
		min-width: 0;
	}

	.lcf-heading {
		font-size: 12px;
		font-weight: 800;
		line-height: 14.4px;
		letter-spacing: 0.08em;
		text-transform: uppercase;
		color: rgb(255 255 255 / 0.48);
	}

	.lcf-heading--lime {
		letter-spacing: 0.075em;
		color: rgb(200 255 56 / 0.78);
	}

	/* 16px type on the measured 35px row pitch. Each row's whole pitch is
	   its target, with no dead space between rows; the list pulls back by
	   half the gap so the first label sits the measured 14px under the
	   heading. */
	.lcf .lcf-links {
		display: flex;
		flex-direction: column;
		margin: -7px 0;
	}

	.lcf-link {
		display: inline-flex;
		align-items: center;
		min-height: 35px;
		border-radius: 6px;
		font-size: 16px;
		font-weight: 500;
		line-height: 20.8px;
		color: rgb(255 255 255 / 0.84);
		transition: color 0.25s ease;
	}

	.lcf-col--contact {
		gap: 9px;
		grid-column: 1;
	}

	.lcf-address {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 9px;
		font-style: normal;
		font-size: 12px;
		font-weight: 600;
		line-height: 14.4px;
		color: rgb(255 255 255 / 0.78);
	}

	.lcf-detail {
		display: inline-flex;
		align-items: center;
		min-height: 24px;
		margin: -5px 0;
		color: inherit;
		border-radius: 4px;
		transition: color 0.25s ease;
	}

	.lcf-postal {
		line-height: 17.4px;
	}

	.lcf-cta {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		min-width: 165px;
		height: 40px;
		margin-top: 9px;
		padding: 0 18px;
		border-radius: 999px;
		background: var(--lcf-lime);
		color: var(--lcf-ink);
		font-size: 12px;
		font-weight: 800;
		line-height: 14.4px;
		white-space: nowrap;
	}

	/* ---- bottom bar ---- */
	.lcf-bottom {
		display: flex;
		flex-direction: column;
		gap: 14px;
		width: calc(100% + 40px);
		max-width: none;
		margin: 0 -20px;
		padding: 18px 20px 28px;
		border-top: 1px solid rgb(255 255 255 / 0.08);
		border-bottom: 1px solid rgb(255 255 255 / 0.08);
	}

	.lcf .lcf-legal ul {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 160px));
		gap: 10px;
		padding: 10px;
	}

	.lcf-legal-link {
		display: inline-flex;
		align-items: center;
		min-height: 24px;
		margin: -4px 0;
		border-radius: 4px;
		font-size: 12px;
		font-weight: 500;
		line-height: 15.6px;
		color: rgb(255 255 255 / 0.62);
		transition: color 0.25s ease;
	}

	.lcf-copy {
		font-size: 12px;
		font-weight: 500;
		line-height: 15.6px;
		color: rgb(255 255 255 / 0.5);
	}

	/* ---- links: no hover was measured. A quiet brighten on hover (an
	   approximation) and a clear lime ring plus full white on focus. ---- */
	@media (hover: hover) {
		.lcf-link:hover,
		.lcf-detail:hover,
		.lcf-legal-link:hover {
			color: #fff;
		}
	}

	.lcf a:focus-visible {
		outline: 2px solid var(--lcf-lime);
		outline-offset: 3px;
		color: #fff;
	}

	.lcf .lcf-cta:focus-visible {
		outline-color: #fff;
		color: var(--lcf-ink);
	}

	/* ---- tablet: brand above, three columns in one row ---- */
	@media (min-width: 810px) {
		.lcf-cols {
			display: flex;
			align-items: flex-start;
			gap: 56px;
		}

		.lcf-col {
			width: 132px;
		}

		.lcf-col--contact {
			width: 190px;
		}

		.lcf-bottom {
			flex-direction: row;
			align-items: center;
			justify-content: space-between;
			width: 100%;
			max-width: 1180px;
			margin: 0;
			/* 12px, not the 18px Framer reports: its 24px legal row carries
			   10px of padding the box does not grow by, and the measured box
			   is 60px tall. */
			padding: 12px 10px 12px 0;
			border: 1px solid rgb(255 255 255 / 0.08);
		}

		.lcf .lcf-legal ul {
			display: flex;
			align-items: center;
			gap: 18px;
		}
	}

	/* ---- desktop: brand left, columns right, as measured ---- */
	@media (min-width: 960px) {
		.lcf-top {
			flex-direction: row;
			justify-content: space-between;
			align-items: flex-start;
			gap: 40px;
			padding: 38px 0 30px;
		}

		.lcf-brand {
			width: 220px;
			flex: none;
		}

		.lcf-cols {
			gap: 68px;
			flex: none;
		}
	}

	@media (min-width: 960px) and (max-width: 1099.98px) {
		.lcf-cols {
			gap: 44px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.lcf a {
			transition: none;
		}
	}

	@media (forced-colors: active) {
		.lcf-cta,
		.lcf-tile {
			border: 1px solid CanvasText;
		}
	}
</style>

===== END src/components/LimeContactFooter.astro =====

--- SECTIONS ---
- Glass nav header (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/glass-nav-header/
- Signal ticker bar (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/signal-ticker-bar/
- Dashboard glass hero (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/dashboard-glass-hero/
- Partner chip band (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/partner-chip-band/
- KPI card strip (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/kpi-card-strip/
- Color service grid (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/color-service-grid/
- Control tower showcase (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/control-tower-showcase/
- Industry tabs panel (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/industry-tabs-panel/
- Route quote estimator (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/route-quote-estimator/
- Split intro FAQ (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/split-intro-faq/
- Dark image CTA card (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/dark-image-cta-card/
- Lime contact footer (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/lime-contact-footer/

Each component's README — its full prop reference — is on its library page above, and is not repeated here. The library verified each component on its own. Nobody has verified this page in your project: test the assembled page yourself.

What's on the page

12 sections, top to bottom. Each is an ordinary library component — open one for its props, its own preview and its verification record.

  1. HeaderGlass nav headerin verification
  2. SectionSignal ticker barin verification
  3. SectionDashboard glass heroin verification
  4. SectionPartner chip bandin verification
  5. SectionKPI card stripin verification
  6. SectionColor service gridin verification
  7. SectionControl tower showcasein verification
  8. SectionIndustry tabs panelin verification
  9. SectionRoute quote estimatorin verification
  10. SectionSplit intro FAQin verification
  11. SectionDark image CTA cardin verification
  12. FooterLime contact footerin verification

25 required files · page file src/pages/freight-platform-homepage.astro · no npm packages