Component library
Back to templates

Page template

Transport site — Home

Homepage of a four-page freight site: white header, big-type hero with a rotating badge, stat band, about split, service photo cards, process steps, success accordion, team, logo marquee, trust split, numbered testimonials, pricing, blog and newsletter footer. Stock photos included.

homepagelogisticstruckingorangenavymulti-pagestock photos

Open preview in a new tab

Use this template

Two ways to get Transport site — Home 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 38 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 "Transport site — Home" page template in my Astro Component Library.

Library page, for reference only:
https://astro.baysixmedia.com/templates/transport-home/

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/transport-home.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/transport-home.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 38 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: "Transport site — Home"
Library page: https://astro.baysixmedia.com/templates/transport-home/
Page file: src/pages/transport-home.astro
Files: 38 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/transport-home.astro =====
---
/**
 * Transport site — Home — a page template from the Astro Component Library.
 * https://astro.baysixmedia.com/templates/transport-home/
 *
 * 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 SolidBarHeader from "../components/SolidBarHeader.astro";
import CircleBadgeHero from "../components/CircleBadgeHero.astro";
import IconStatBand from "../components/IconStatBand.astro";
import CommittedAboutSplit from "../components/CommittedAboutSplit.astro";
import ServicePhotoCards from "../components/ServicePhotoCards.astro";
import DottedProcessSteps from "../components/DottedProcessSteps.astro";
import AccordionSuccessShowcase from "../components/AccordionSuccessShowcase.astro";
import TeamHoverCards from "../components/TeamHoverCards.astro";
import CargoLogoMarquee from "../components/CargoLogoMarquee.astro";
import TrustReasonsSplit from "../components/TrustReasonsSplit.astro";
import NumberedTestimonialSwitcher from "../components/NumberedTestimonialSwitcher.astro";
import StackedPricingPlans from "../components/StackedPricingPlans.astro";
import LogisticsBlogCards from "../components/LogisticsBlogCards.astro";
import NewsletterPanelFooter from "../components/NewsletterPanelFooter.astro";

const solidBarHeaderProps = {
	"items": [
		{
			"label": "Home",
			"children": [
				{
					"label": "Freight home",
					"href": "/"
				},
				{
					"label": "Trucking home",
					"href": "/trucking"
				},
				{
					"label": "Ocean home",
					"href": "/ocean"
				}
			]
		},
		{
			"label": "About Us",
			"href": "/about"
		},
		{
			"label": "Services",
			"children": [
				{
					"label": "Ocean freight",
					"href": "/services/ocean-freight"
				},
				{
					"label": "Road haulage",
					"href": "/services/road-haulage"
				},
				{
					"label": "Rail freight",
					"href": "/services/rail-freight"
				},
				{
					"label": "Warehousing",
					"href": "/services/warehousing"
				}
			]
		},
		{
			"label": "Pages",
			"children": [
				{
					"label": "Our team",
					"href": "/team"
				},
				{
					"label": "Pricing",
					"href": "/pricing"
				},
				{
					"label": "Request a quote",
					"href": "/request-quote"
				},
				{
					"label": "Shipping FAQ",
					"href": "/faq"
				}
			]
		},
		{
			"label": "News",
			"children": [
				{
					"label": "Latest news",
					"href": "/news"
				},
				{
					"label": "Lane reports",
					"href": "/news/lane-reports"
				}
			]
		},
		{
			"label": "Contact",
			"href": "/contact"
		}
	],
	"brandName": "Northpass",
	"cta": {
		"label": "Free Quote!",
		"href": "/request-quote"
	},
	"currentPath": "/"
};

const circleBadgeHeroProps = {
	"eyebrow": "Coast to coast freight",
	"headingTop": "Fast freight",
	"headingMain": "Logistics",
	"text": "Northpass has moved freight for manufacturers, growers and retailers since 1998. One team plans the lane, books the carrier and tracks every load door to door.",
	"image": {
		"src": "/circle-badge-hero/port-trucks.webp",
		"alt": "A container truck between stacked shipping containers and gantry cranes under a pink sunset sky"
	},
	"badge": {
		"label": "Get A Free Quote",
		"href": "/request-quote"
	},
	"nextLink": {
		"label": "See our services",
		"href": "/services"
	},
	"cta": {
		"label": "Explore Our Fleet",
		"href": "/services"
	},
	"clients": [
		{
			"src": "/circle-badge-hero/client-1.webp",
			"alt": ""
		},
		{
			"src": "/circle-badge-hero/client-2.webp",
			"alt": ""
		},
		{
			"src": "/circle-badge-hero/client-3.webp",
			"alt": ""
		}
	],
	"statValue": "42k+",
	"statLabel": "Shippers who trust our lanes"
};

const iconStatBandProps = {
	"items": [
		{
			"icon": "transit",
			"value": 68,
			"suffix": "k+",
			"label": "Loads Delivered"
		},
		{
			"icon": "team",
			"value": 52,
			"suffix": "+",
			"label": "Dispatch Staff"
		},
		{
			"icon": "experience",
			"value": 27,
			"suffix": "+",
			"label": "Years on the Road"
		},
		{
			"icon": "customers",
			"value": 42,
			"suffix": "k+",
			"label": "Repeat Shippers"
		}
	],
	"image": {
		"src": "/icon-stat-band/highway-dusk.webp"
	}
};

const committedAboutSplitProps = {
	"image": {
		"src": "/committed-about-split/port-dock.webp",
		"alt": "Stacked shipping containers on a quay beside a moored cargo ship"
	},
	"videoTitle": "Northpass Logistics: a day at the port",
	"heading": "Freight partners who keep your business moving",
	"text": "Since 1998 Northpass has planned, booked and tracked freight for companies that can't afford a late load. Our dispatchers know every lane we run, so your cargo arrives when we said it would.",
	"points": [
		"Dispatchers who answer the phone",
		"Inspected equipment, every trip",
		"On-time delivery you can plan around"
	],
	"cta": {
		"label": "About More",
		"href": "/about"
	}
};

const servicePhotoCardsProps = {
	"heading": "Freight & Logistics Services",
	"cta": {
		"label": "View All Services",
		"href": "/services"
	},
	"cards": [
		{
			"title": "Ocean Freight",
			"description": "Full and part container loads booked on the carriers that fit your date, with customs paperwork handled for you.",
			"href": "/services/ocean-freight",
			"icon": "ship",
			"image": {
				"src": "/service-photo-cards/ocean.webp",
				"alt": ""
			}
		},
		{
			"title": "Regional Truckload",
			"description": "Dry van and reefer capacity across the West, dispatched the same day and tracked until it is signed for.",
			"href": "/services/truckload",
			"icon": "truck",
			"image": {
				"src": "/service-photo-cards/truck.webp",
				"alt": ""
			}
		},
		{
			"title": "Rail Intermodal",
			"description": "Long-haul lanes moved by rail and finished by truck, for heavy freight that doesn't need to arrive tomorrow.",
			"href": "/services/rail",
			"icon": "train",
			"image": {
				"src": "/service-photo-cards/rail.webp",
				"alt": ""
			}
		}
	]
};

const dottedProcessStepsProps = {
	"heading": "Three Steps From Quote to Delivery",
	"steps": [
		{
			"title": "Request a Rate",
			"text": "Tell us what is moving, from where and by when. You get a clear, itemised rate within the hour.",
			"icon": "globe-parcel"
		},
		{
			"title": "Lock In Capacity",
			"text": "Confirm the booking and we reserve the truck, box or rail slot on the lane you picked.",
			"icon": "truck-plane"
		},
		{
			"title": "Track to the Dock",
			"text": "Follow every leg live, and a named coordinator stays on it until the freight is signed for.",
			"icon": "globe-truck"
		}
	]
};

const accordionSuccessShowcaseProps = {
	"eyebrow": "Proven Results",
	"heading": "Freight Programs That Paid for Themselves",
	"items": [
		{
			"title": "Ocean Freight",
			"text": "Consolidated container bookings cut one retailer's landed cost per unit by 18% in a single season.",
			"card": {
				"title": "Vessel Schedules",
				"text": "Weekly sailings on 40 lanes, with space held for contract shippers."
			}
		},
		{
			"title": "Air Cargo",
			"text": "Priority uplift and pre-cleared customs brought a medical supplier's lead time down to four days.",
			"card": {
				"title": "Priority Uplift",
				"text": "Next-flight-out capacity for parcels that cannot wait for the ship."
			}
		},
		{
			"title": "Rail Intermodal",
			"text": "Moving heavy volume from road to rail saved a building-materials client 22% on inland haulage.",
			"card": {
				"title": "Terminal Tracking",
				"text": "Live ramp and railcar status from origin yard to final dray."
			}
		}
	],
	"image": {
		"src": "/accordion-success-showcase/cranes-dusk.webp",
		"alt": "Container cranes over a lit port terminal at dusk"
	}
};

const teamHoverCardsProps = {
	"heading": "Meet the Team Behind Your Freight",
	"members": [
		{
			"name": "Karim Haddad",
			"role": "Chief Executive",
			"href": "#",
			"image": {
				"src": "/team-hover-cards/member-1.webp",
				"alt": "Smiling bearded man in a red hard hat and a navy jacket"
			},
			"socials": [
				{
					"network": "instagram",
					"href": "https://instagram.com"
				},
				{
					"network": "x",
					"href": "https://x.com"
				},
				{
					"network": "facebook",
					"href": "https://facebook.com"
				}
			]
		},
		{
			"name": "Luca Moretti",
			"role": "Head of Operations",
			"href": "#",
			"image": {
				"src": "/team-hover-cards/member-2.webp",
				"alt": "Man in a white hard hat and a work jacket, tipping the brim"
			},
			"socials": [
				{
					"network": "instagram",
					"href": "https://instagram.com"
				},
				{
					"network": "x",
					"href": "https://x.com"
				},
				{
					"network": "facebook",
					"href": "https://facebook.com"
				}
			]
		},
		{
			"name": "Kwame Mensah",
			"role": "Fleet Director",
			"href": "#",
			"image": {
				"src": "/team-hover-cards/member-3.webp",
				"alt": "Smiling man in a white hard hat, sunglasses and a white shirt"
			},
			"socials": [
				{
					"network": "instagram",
					"href": "https://instagram.com"
				},
				{
					"network": "x",
					"href": "https://x.com"
				},
				{
					"network": "facebook",
					"href": "https://facebook.com"
				}
			]
		}
	]
};

const cargoLogoMarqueeProps = {
	"logos": [
		{
			"name": "Harbor",
			"accent": "Line",
			"tagline": "Ocean Carriers",
			"glyph": "ship"
		},
		{
			"name": "Tide",
			"accent": "well",
			"tagline": "Port Services",
			"glyph": "crane"
		},
		{
			"name": "Sky",
			"accent": "run",
			"tagline": "Air Freight",
			"glyph": "plane"
		},
		{
			"name": "Iron",
			"accent": "vale",
			"tagline": "Rail Logistics",
			"glyph": "rail"
		},
		{
			"name": "Long",
			"accent": "haul",
			"tagline": "Motor Freight",
			"glyph": "truck"
		},
		{
			"name": "Blue",
			"accent": "keel",
			"tagline": "Global Shipping",
			"glyph": "globe"
		}
	],
	"image": {
		"src": "/cargo-logo-marquee/port-aerial.webp",
		"alt": ""
	},
	"heading": "Over 300 Shippers Trust Northpass With Their Freight"
};

const trustReasonsSplitProps = {
	"eyebrow": "Why Northpass",
	"heading": "Why Shippers Keep Their Freight With Us",
	"text": "For more than 25 years we have moved freight for manufacturers, retailers and growers across four continents, with one coordinator on every account from first quote to final signature.",
	"reasons": [
		"On-Time, Every Lane",
		"Door-to-Door Handling",
		"Live Shipment Tracking",
		"Honest, Fixed Pricing"
	],
	"cta": {
		"label": "About Us",
		"href": "/about"
	},
	"contact": {
		"label": "Call Us Any Time",
		"phone": "+1 (555) 014-2290"
	},
	"image": {
		"src": "/trust-reasons-split/ship-loading.webp",
		"alt": "Container ship moored under orange gantry cranes while cargo is loaded"
	}
};

const numberedTestimonialSwitcherProps = {
	"testimonials": [
		{
			"quote": "“Northpass took over our inbound ocean freight in the middle of peak season and nothing slipped. Containers cleared, trucks were waiting at the port, and our shelves stayed full through the holidays.”",
			"name": "Maya Castillo",
			"role": "Procurement Director",
			"image": {
				"src": "/numbered-testimonial-switcher/client-1.webp",
				"alt": "Portrait of Maya Castillo, smiling, in a black top"
			}
		},
		{
			"quote": "“We run three shifts and can't afford a late trailer. Their dispatch team texts us before we even think to ask, and our dock schedule finally matches reality.”",
			"name": "Darnell Okafor",
			"role": "Warehouse Operations Lead",
			"image": {
				"src": "/numbered-testimonial-switcher/client-2.webp",
				"alt": "Portrait of Darnell Okafor wearing a yellow hard hat"
			}
		},
		{
			"quote": "“Switching our rail lanes to Northpass cut transit variance in half. The weekly lane report is the first thing I open on Monday.”",
			"name": "Ryan Mercer",
			"role": "Supply Chain Manager",
			"image": {
				"src": "/numbered-testimonial-switcher/client-3.webp",
				"alt": "Portrait of Ryan Mercer with his arms crossed on a city street"
			}
		},
		{
			"quote": "“Customs paperwork used to eat my week. Now one coordinator handles it end to end, and I get a clean landed-cost sheet for every shipment.”",
			"name": "Lauren Pike",
			"role": "Import Coordinator",
			"image": {
				"src": "/numbered-testimonial-switcher/client-4.webp",
				"alt": "Portrait of Lauren Pike in a light blazer, outdoors"
			}
		}
	]
};

const stackedPricingPlansProps = {
	"heading": "Freight plans that scale with your lanes",
	"text": "From a few pallets a week to a national network, pick the plan that fits today and move up when volume does. No setup fees.",
	"plans": [
		{
			"name": "Starter",
			"price": "$249",
			"period": "/Month",
			"cta": {
				"label": "Get Started",
				"href": "/contact"
			},
			"features": [
				"Regional LTL and parcel",
				"Up to 80 shipments a month",
				"Online booking and labels",
				{
					"label": "Live GPS tracking",
					"included": false
				}
			]
		},
		{
			"name": "Business",
			"price": "$389",
			"period": "/Month",
			"featured": true,
			"cta": {
				"label": "Get Started",
				"href": "/contact"
			},
			"features": [
				"Full truckload and intermodal",
				"Up to 400 shipments a month",
				"Live GPS tracking",
				"Dedicated account coordinator"
			]
		},
		{
			"name": "Enterprise",
			"price": "$649",
			"period": "/Month",
			"cta": {
				"label": "Get Started",
				"href": "/contact"
			},
			"features": [
				"Ocean, air, rail and road",
				"Unlimited shipments",
				"Customs brokerage included",
				"Quarterly lane reviews"
			]
		}
	],
	"highlights": [
		"Every load insured from pickup to delivery",
		"One invoice across every mode you ship"
	]
};

const logisticsBlogCardsProps = {
	"heading": "Notes From the Loading Dock",
	"posts": [
		{
			"title": "How We Cut Port Dwell Time to Under Two Days",
			"href": "/blog/port-dwell-time",
			"tag": "Ocean Freight",
			"image": {
				"src": "/logistics-blog-cards/post-1.webp",
				"alt": "Aerial view of stacked shipping containers in a port terminal"
			},
			"author": {
				"name": "Priya Natarajan"
			},
			"date": "2026-08-14"
		},
		{
			"title": "Five Questions to Ask Before You Book Full Truckload",
			"href": "/blog/full-truckload-checklist",
			"tag": "Road Transport",
			"image": {
				"src": "/logistics-blog-cards/post-2.webp",
				"alt": "Semi truck on a divided highway beside a concrete wall"
			},
			"author": {
				"name": "Marcus Hale"
			},
			"date": "2026-07-29"
		},
		{
			"title": "Why Intermodal Rail Is Winning Long-Haul Lanes",
			"href": "/blog/intermodal-rail-long-haul",
			"tag": "Rail Transport",
			"image": {
				"src": "/logistics-blog-cards/post-3.webp",
				"alt": "Freight trains and tank cars lined up in a rail yard"
			},
			"author": {
				"name": "Elena Ruiz"
			},
			"date": "2026-07-10"
		}
	]
};

const newsletterPanelFooterProps = {
	"heading": "Subscribe Our Newsletter",
	"newsletter": {},
	"brandName": "Northpass",
	"blurb": "Freight moved on time, by people who answer the phone. Ocean, road, rail and storage, run from one dispatch desk since 2009.",
	"social": [
		{
			"network": "facebook",
			"href": "https://facebook.com",
			"label": "Northpass on Facebook"
		},
		{
			"network": "x",
			"href": "https://x.com",
			"label": "Northpass on X"
		},
		{
			"network": "instagram",
			"href": "https://instagram.com",
			"label": "Northpass on Instagram"
		},
		{
			"network": "linkedin",
			"href": "https://linkedin.com",
			"label": "Northpass on LinkedIn"
		}
	],
	"columns": [
		{
			"heading": "Quick Links",
			"links": [
				{
					"label": "Freight",
					"href": "/freight"
				},
				{
					"label": "Shipping",
					"href": "/shipping"
				},
				{
					"label": "About Northpass",
					"href": "/about"
				},
				{
					"label": "Our Team",
					"href": "/team"
				},
				{
					"label": "Lane Reports",
					"href": "/news"
				},
				{
					"label": "Pricing",
					"href": "/pricing"
				},
				{
					"label": "Contact Us",
					"href": "/contact"
				},
				{
					"label": "Client Stories",
					"href": "/stories"
				},
				{
					"label": "Case Studies",
					"href": "/case-studies"
				}
			]
		},
		{
			"heading": "Our Services",
			"links": [
				{
					"label": "Ocean Freight",
					"href": "/services/ocean-freight"
				},
				{
					"label": "Road Haulage",
					"href": "/services/road-haulage"
				},
				{
					"label": "Rail Freight",
					"href": "/services/rail-freight"
				},
				{
					"label": "Cross-Border Trucking",
					"href": "/services/cross-border"
				},
				{
					"label": "Less-Than-Truckload",
					"href": "/services/ltl"
				},
				{
					"label": "Warehousing",
					"href": "/services/warehousing"
				},
				{
					"label": "Cold-Chain Transport",
					"href": "/services/cold-chain"
				},
				{
					"label": "Project Cargo",
					"href": "/services/project-cargo"
				},
				{
					"label": "Customs Brokerage",
					"href": "/services/customs"
				}
			]
		}
	],
	"contacts": [
		{
			"icon": "email",
			"label": "dispatch@northpass.example",
			"href": "mailto:dispatch@northpass.example"
		},
		{
			"icon": "phone",
			"label": "+1 (503) 555-0148",
			"href": "tel:+15035550148"
		}
	],
	"copyright": "Copyright © {year} Northpass Logistics. All Rights Reserved.",
	"variant": "band"
};
---

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title>[Business name] — [Primary freight service] in [Region]</title>
		<meta name="description" content="[One-sentence description of the carrier, its services and its service area.]" />
		<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>

		<!-- Solid bar header -->
		<SolidBarHeader {...solidBarHeaderProps} />

		<main id="main">
			<!-- Circle badge hero -->
			<CircleBadgeHero {...circleBadgeHeroProps} />

			<!-- Icon stat band -->
			<IconStatBand {...iconStatBandProps} />

			<!-- Committed about split -->
			<CommittedAboutSplit {...committedAboutSplitProps} />

			<!-- Service photo cards -->
			<ServicePhotoCards {...servicePhotoCardsProps} />

			<!-- Dotted process steps -->
			<DottedProcessSteps {...dottedProcessStepsProps} />

			<!-- Accordion success showcase -->
			<AccordionSuccessShowcase {...accordionSuccessShowcaseProps} />

			<!-- Team hover cards -->
			<TeamHoverCards {...teamHoverCardsProps} />

			<!-- Cargo logo marquee -->
			<CargoLogoMarquee {...cargoLogoMarqueeProps} />

			<!-- Trust reasons split -->
			<TrustReasonsSplit {...trustReasonsSplitProps} />

			<!-- Numbered testimonial switcher -->
			<NumberedTestimonialSwitcher {...numberedTestimonialSwitcherProps} />

			<!-- Stacked pricing plans -->
			<StackedPricingPlans {...stackedPricingPlansProps} />

			<!-- Logistics blog cards -->
			<LogisticsBlogCards {...logisticsBlogCardsProps} />
		</main>

		<!-- Newsletter panel footer -->
		<NewsletterPanelFooter {...newsletterPanelFooterProps} />
	</body>
</html>

===== END src/pages/transport-home.astro =====

===== FILE (required): src/components/SolidBarHeader.astro =====
---
/**
 * SolidBarHeader
 *
 * A 104px white site header in the normal flow: truck mark and wordmark on
 * the left, a centred row of links (Home, Services, Pages and News open
 * dropdown panels, marked with a small "+"), and an orange "Free Quote!" pill
 * with an arrow on the right. Below 1200px the links and the pill fold into
 * a menu button and a white card that drops under the bar.
 *
 * Reads --tg-* design tokens from any ancestor, with fallbacks, so it works
 * alone and re-themes with a page.
 *
 * `sticky` pins the bar to the top of the viewport as the page scrolls
 * (position: sticky, so it still holds its own space and nothing is covered
 * at rest). Off by default: the reference scrolls away with the page.
 */

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

export interface SolidBarHeaderItem {
	label: string;
	/** Ignored when `children` is present: the item becomes a dropdown trigger. */
	href?: string;
	/** Renders the item as a real <button> that opens a single-column panel. */
	children?: SolidBarHeaderLink[];
}

export interface SolidBarHeaderProps {
	items?: SolidBarHeaderItem[];
	/** 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 };
	cta?: { label: string; href: string } | null;
	navLabel?: string;
	menuLabel?: string;
	fontStylesheet?: string;
	/** Pin the bar to the top of the viewport on scroll. Default false (in the flow). */
	sticky?: boolean;
	id?: string;
}

interface Props extends SolidBarHeaderProps {}

/* No invented site: links, brand and CTA are all the caller's. Each block
   renders only when its data is supplied; with none, nothing renders. */
const {
	items = [],
	currentPath,
	brandName,
	brandHref = "/",
	logo,
	cta = null,
	navLabel = "Primary",
	menuLabel = "Menu",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;600&display=swap",
	sticky = false,
	id,
} = Astro.props;

/* Ids for aria-controls. Seeded from `id` when given, random otherwise, so a
   second instance never collides with the first. */
const seed = id ? id : `sbh-${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 hasCurrentChild = (item: SolidBarHeaderItem) =>
	Boolean(item.children?.some((c) => isCurrent(c.href)));

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

{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<header class:list={["sbh", { "sbh--sticky": sticky }]} id={id} data-header-root data-sbh-root>
	<div class="sbh-bar">
		{hasBrand && (
		<a class="sbh-brand" href={brandHref} aria-label={logo ? undefined : `${brand} home`}>
			{
				logo ? (
					<img
						class="sbh-logo-img"
						src={logo.src}
						alt={logo.alt}
						width={logo.width ?? 157}
						height={logo.height ?? 30}
					/>
				) : (
					<>
						<svg class="sbh-mark" viewBox="0 0 46 30" aria-hidden="true" focusable="false">
							<path d="M2 9h11M5 14h9M1 19h13" fill="none" stroke="var(--sbh-orange)" stroke-width="2.6" stroke-linecap="round" />
							<rect x="14" y="4" width="17" height="15" rx="2" fill="currentColor" />
							<path d="M32 8h6.2l5.3 5.6V19H32z" fill="currentColor" />
							<path d="M34.4 10.4h3.2l3 3.2h-6.2z" fill="var(--sbh-orange)" />
							<circle cx="20" cy="21.5" r="3.4" fill="currentColor" stroke="var(--sbh-bg)" stroke-width="1.6" />
							<circle cx="37.5" cy="21.5" r="3.4" fill="currentColor" stroke="var(--sbh-bg)" stroke-width="1.6" />
							<path d="M4 27.5h40" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
						</svg>
						<span class="sbh-wordmark" aria-hidden="true">{brand}</span>
					</>
				)
			}
		</a>
		)}

		{hasItems && (
		<nav class="sbh-nav" aria-label={navLabel}>
			<ul class="sbh-list" role="list">
				{
					items.map((item, i) => {
						if (item.children && item.children.length > 0) {
							const panelId = `${seed}-panel-${i}`;
							return (
								<li class="sbh-item sbh-item--dd" data-sbh-dd>
									<button
										type="button"
										class="sbh-link sbh-trigger"
										aria-expanded="false"
										aria-controls={panelId}
										data-sbh-trigger
										data-current={hasCurrentChild(item) ? "" : undefined}
									>
										{item.label}
										<svg class="sbh-plus" viewBox="0 0 11 11" aria-hidden="true" focusable="false">
											<path d="M5.5 1v9M1 5.5h9" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
										</svg>
									</button>
									<div class="sbh-panel" id={panelId} data-sbh-panel>
										<ul role="list">
											{item.children.map((child) => (
												<li>
													<a
														class="sbh-panel-link"
														href={child.href}
														aria-current={isCurrent(child.href) ? "page" : undefined}
													>
														{child.label}
													</a>
												</li>
											))}
										</ul>
									</div>
								</li>
							);
						}
						return (
							<li class="sbh-item">
								<a
									class="sbh-link"
									href={item.href ?? "#"}
									aria-current={isCurrent(item.href) ? "page" : undefined}
								>
									{item.label}
								</a>
							</li>
						);
					})
				}
			</ul>
		</nav>
		)}

		{
			cta && (
				<a class="sbh-cta" href={cta.href}>
					<span>{cta.label}</span>
					<svg class="sbh-arrow" viewBox="0 0 12 12" aria-hidden="true" focusable="false">
						<path d="M2 10L10 2M3.5 2H10v6.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
					</svg>
				</a>
			)
		}

		{hasMenu && (
		<button
			type="button"
			class="sbh-burger"
			aria-expanded="false"
			aria-controls={menuId}
			data-sbh-burger
		>
			<span class="sbh-sr">{menuLabel}</span>
			<span class="sbh-burger-lines" aria-hidden="true"><span></span><span></span><span></span></span>
		</button>
		)}
	</div>

	{hasMenu && (
	<nav class="sbh-menu" id={menuId} aria-label={navLabel} data-sbh-menu>
		{hasItems && (
		<ul class="sbh-menu-list" role="list">
			{
				items.map((item, i) =>
					item.children && item.children.length > 0 ? (
						<li class="sbh-menu-group" data-sbh-sub>
							<button
								type="button"
								class="sbh-menu-link sbh-menu-toggle"
								aria-expanded="true"
								aria-controls={`${seed}-sub-${i}`}
								data-sbh-subtoggle
								data-current={hasCurrentChild(item) ? "" : undefined}
							>
								{item.label}
								<svg class="sbh-plus" viewBox="0 0 11 11" aria-hidden="true" focusable="false">
									<path d="M5.5 1v9M1 5.5h9" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
								</svg>
							</button>
							<ul class="sbh-sublist" id={`${seed}-sub-${i}`} role="list" data-sbh-sublist>
								{item.children.map((child) => (
									<li>
										<a
											class="sbh-menu-link sbh-menu-sublink"
											href={child.href}
											aria-current={isCurrent(child.href) ? "page" : undefined}
										>
											{child.label}
										</a>
									</li>
								))}
							</ul>
						</li>
					) : (
						<li>
							<a
								class="sbh-menu-link"
								href={item.href ?? "#"}
								aria-current={isCurrent(item.href) ? "page" : undefined}
							>
								{item.label}
							</a>
						</li>
					),
				)
			}
		</ul>
		)}
		{
			cta && (
				<a class="sbh-cta sbh-cta--menu" href={cta.href}>
					<span>{cta.label}</span>
					<svg class="sbh-arrow" viewBox="0 0 12 12" aria-hidden="true" focusable="false">
						<path d="M2 10L10 2M3.5 2H10v6.5" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
					</svg>
				</a>
			)
		}
	</nav>
	)}
</header>
)}

<style>
	.sbh {
		--sbh-navy: var(--tg-navy, #020e28);
		--sbh-orange: var(--tg-orange, #fd5523);
		--sbh-muted: var(--tg-muted, #61697d);
		--sbh-surface: var(--tg-surface, #ecf0f1);
		--sbh-display: var(--tg-font-display, Outfit, ui-sans-serif, system-ui, sans-serif);
		--sbh-body: var(--tg-font-body, "DM Sans", ui-sans-serif, system-ui, sans-serif);
		--sbh-bg: #fff;
		--sbh-dd-hover: #ff6060;
		--sbh-dur: 0.3s;
		--sbh-ease: cubic-bezier(0.44, 0, 0.56, 1);

		position: relative;
		z-index: 30;
		box-sizing: border-box;
		width: 100%;
		padding: 24px 20px;
		background: var(--sbh-bg);
		border-bottom: 1px solid #e7e9ee;
		font-family: var(--sbh-body);
		color: var(--sbh-navy);
	}

	/* Sticky: pinned once the script runs (the menu card then hangs out of the
	   flow), and always at desktop widths, where there is no card. Without
	   script the small-screen card sits open in the flow, so the bar is not
	   pinned there: it would hold the whole menu over the page. */
	.sbh--sticky[data-sbh-ready] {
		position: sticky;
		top: 0;
	}

	/* A pinned bar would cover whatever an in-page link or the skip link
	   scrolls to: the page's scroll padding keeps targets below it (the
	   105px bar with its hairline; 81px on a phone). */
	:global(html:has(.sbh--sticky)) {
		scroll-padding-top: 105px;
	}

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

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

	.sbh-bar {
		position: relative;
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 16px;
		min-height: 56px;
	}

	/* ---- brand ---- */
	.sbh-brand {
		display: inline-flex;
		align-items: center;
		gap: 9px;
		min-height: 44px;
		color: var(--sbh-navy);
		text-decoration: none;
		border-radius: 6px;
		flex: none;
	}

	.sbh-mark {
		width: 46px;
		height: 30px;
		flex: none;
	}

	.sbh-wordmark {
		font: 600 24px/1 var(--sbh-display);
		letter-spacing: -0.01em;
	}

	.sbh-logo-img {
		display: block;
		height: 30px;
		width: auto;
	}

	/* ---- desktop links ---- */
	.sbh-nav {
		display: none;
	}

	/* `.sbh .sbh-list`, not `.sbh-list`: the `.sbh ul` reset would otherwise
	   outrank it and zero the padding. */
	.sbh .sbh-list {
		display: flex;
		align-items: center;
		gap: clamp(26px, calc(6.5vw - 52px), 42px);
		padding: 9px 20px;
	}

	.sbh-item {
		position: relative;
	}

	.sbh-link {
		display: inline-flex;
		align-items: center;
		gap: 1px;
		/* 16px type on a 26px line inside a 44px target; the negative margin
		   gives the space back so the 42px rhythm is the reference's. */
		padding: 9px 6px;
		margin: -9px -6px;
		border: 0;
		border-radius: 6px;
		background: none;
		color: var(--sbh-muted);
		font: 500 16px/26px var(--sbh-display);
		text-decoration: none;
		white-space: nowrap;
		cursor: pointer;
		transition: color var(--sbh-dur) var(--sbh-ease);
	}

	.sbh-link:hover,
	.sbh-link[aria-current="page"] {
		color: var(--sbh-orange);
	}

	.sbh-trigger:hover,
	.sbh-trigger[aria-expanded="true"],
	.sbh-trigger[data-current] {
		color: var(--sbh-dd-hover);
	}

	.sbh-plus {
		width: 11px;
		height: 11px;
		flex: none;
		transition: transform var(--sbh-dur) var(--sbh-ease);
	}

	.sbh-trigger:hover .sbh-plus,
	.sbh-trigger[aria-expanded="true"] .sbh-plus {
		transform: rotate(180deg);
	}

	/* ---- dropdown panel ----
	   Without script the panel opens on hover and focus-within, so every link
	   stays reachable. Once booted the controller owns the state, and a closed
	   panel is visibility: hidden — out of the tab order. */
	.sbh-panel {
		position: absolute;
		top: calc(100% + 24px);
		left: -22px;
		min-width: 220px;
		padding: 14px 22px;
		border-radius: 10px;
		background: #fff;
		box-shadow: 0 18px 44px rgb(2 14 40 / 0.14);
		border-top: 3px solid var(--sbh-orange);
		opacity: 0;
		transform: translateY(10px);
		visibility: hidden;
		pointer-events: none;
		transition:
			opacity var(--sbh-dur) var(--sbh-ease),
			transform var(--sbh-dur) var(--sbh-ease),
			visibility 0s linear var(--sbh-dur);
		z-index: 2;
	}

	/* A bridge over the gap between trigger and panel, so pointer travel
	   from one to the other never crosses dead space. */
	.sbh-panel::before {
		content: "";
		position: absolute;
		inset: -27px 0 100% 0;
	}

	.sbh:not([data-sbh-ready]) .sbh-item--dd:hover .sbh-panel,
	.sbh:not([data-sbh-ready]) .sbh-item--dd:focus-within .sbh-panel,
	.sbh[data-sbh-ready] .sbh-item--dd[data-open] .sbh-panel {
		opacity: 1;
		transform: none;
		visibility: visible;
		pointer-events: auto;
		transition:
			opacity var(--sbh-dur) var(--sbh-ease),
			transform var(--sbh-dur) var(--sbh-ease),
			visibility 0s;
	}

	.sbh-panel-link {
		display: flex;
		align-items: center;
		min-height: 40px;
		color: var(--sbh-navy);
		font: 500 16px/1.3 var(--sbh-display);
		text-decoration: none;
		white-space: nowrap;
		border-radius: 4px;
		transition:
			color var(--sbh-dur) var(--sbh-ease),
			transform var(--sbh-dur) var(--sbh-ease);
	}

	.sbh-panel-link:hover,
	.sbh-panel-link[aria-current="page"] {
		color: var(--sbh-orange);
	}

	.sbh-panel-link:hover {
		transform: translateX(4px);
	}

	/* ---- Free Quote pill ---- */
	.sbh-cta {
		display: none;
		align-items: center;
		gap: 10px;
		min-height: 56px;
		padding: 0 30px;
		border-radius: 100px;
		background: var(--sbh-orange);
		color: #fff;
		font: 600 14px/1 var(--sbh-body);
		text-decoration: none;
		white-space: nowrap;
		flex: none;
		transition: background-color var(--sbh-dur) var(--sbh-ease);
	}

	.sbh-arrow {
		width: 12px;
		height: 12px;
		flex: none;
		transition: transform var(--sbh-dur) var(--sbh-ease);
	}

	.sbh-cta:hover {
		background: var(--sbh-navy);
	}

	.sbh-cta:hover .sbh-arrow {
		transform: rotate(45deg);
	}

	/* ---- burger ---- */
	.sbh-burger {
		display: grid;
		place-items: center;
		width: 44px;
		height: 44px;
		margin-right: -8px;
		padding: 0;
		border: 0;
		border-radius: 8px;
		background: none;
		color: var(--sbh-navy);
		cursor: pointer;
	}

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

	.sbh-burger-lines {
		position: relative;
		display: block;
		width: 24px;
		height: 18px;
	}

	.sbh-burger-lines span {
		position: absolute;
		left: 0;
		width: 24px;
		height: 2px;
		border-radius: 1px;
		background: currentColor;
		transition:
			transform var(--sbh-dur) var(--sbh-ease),
			opacity var(--sbh-dur) var(--sbh-ease);
	}

	.sbh-burger-lines span:nth-child(1) { top: 0; }
	.sbh-burger-lines span:nth-child(2) { top: 8px; }
	.sbh-burger-lines span:nth-child(3) { top: 16px; }

	.sbh[data-sbh-open] .sbh-burger-lines span:nth-child(1) { transform: translateY(8px) rotate(45deg); }
	.sbh[data-sbh-open] .sbh-burger-lines span:nth-child(2) { opacity: 0; }
	.sbh[data-sbh-open] .sbh-burger-lines span:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }

	/* ---- phone / tablet menu card ---- */
	.sbh-menu {
		margin: 16px 0 0;
		padding: 12px 20px 20px;
		border-radius: 12px;
		background: #fff;
		box-shadow: 0 18px 44px rgb(2 14 40 / 0.14);
		border: 1px solid #e7e9ee;
		transition:
			opacity var(--sbh-dur) var(--sbh-ease),
			transform var(--sbh-dur) var(--sbh-ease),
			visibility 0s linear var(--sbh-dur);
	}

	/* With script the card hangs under the bar, out of the flow, so opening
	   it covers the page rather than pushing it down. */
	.sbh[data-sbh-ready] .sbh-menu {
		position: absolute;
		top: calc(100% + 8px);
		left: 16px;
		right: 16px;
		margin: 0;
		max-height: calc(100vh - 120px);
		max-height: calc(100dvh - 120px);
		overflow-y: auto;
	}

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

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

	.sbh-menu-list > li + li {
		border-top: 1px solid #eceef2;
	}

	.sbh-menu-link {
		display: flex;
		align-items: center;
		justify-content: space-between;
		width: 100%;
		min-height: 48px;
		padding: 0;
		border: 0;
		background: none;
		color: var(--sbh-navy);
		font: 500 17px/1.2 var(--sbh-display);
		text-align: left;
		text-decoration: none;
		cursor: pointer;
		border-radius: 4px;
		transition: color var(--sbh-dur) var(--sbh-ease);
	}

	.sbh-menu-link:hover,
	.sbh-menu-link[aria-current="page"] {
		color: var(--sbh-orange);
	}

	.sbh-menu-toggle:hover,
	.sbh-menu-toggle[data-current] {
		color: var(--sbh-dd-hover);
	}

	.sbh-menu-toggle .sbh-plus {
		width: 13px;
		height: 13px;
	}

	.sbh-menu-toggle[aria-expanded="true"] .sbh-plus {
		transform: rotate(45deg);
	}

	.sbh .sbh-sublist {
		padding: 0 0 10px 14px;
		border-left: 2px solid var(--sbh-surface);
		margin-left: 2px;
	}

	.sbh-sublist[hidden] {
		display: none;
	}

	.sbh-menu-sublink {
		min-height: 44px;
		color: var(--sbh-muted);
		font: 400 16px/1.3 var(--sbh-body);
	}

	.sbh-cta--menu {
		display: flex;
		justify-content: center;
		margin-top: 14px;
	}

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

	.sbh-cta:focus-visible {
		outline-color: var(--sbh-navy);
	}

	.sbh-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: an 80px bar ---- */
	@media (max-width: 809.98px) {
		:global(html:has(.sbh--sticky)) {
			scroll-padding-top: 81px;
		}

		.sbh {
			padding: 12px 20px;
		}

		.sbh-mark {
			width: 40px;
			height: 26px;
		}

		.sbh-wordmark {
			font-size: 22px;
		}
	}

	/* ---- desktop: links and pill appear, the menu goes ---- */
	@media (min-width: 1200px) {
		.sbh--sticky {
			position: sticky;
			top: 0;
		}

		.sbh-nav {
			display: block;
			position: absolute;
			left: 50%;
			top: 50%;
			transform: translate(-50%, -50%);
		}

		.sbh-cta {
			display: inline-flex;
		}

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

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

		.sbh-trigger:hover .sbh-plus,
		.sbh-cta:hover .sbh-arrow,
		.sbh-panel-link:hover {
			transform: none;
		}

		.sbh-trigger[aria-expanded="true"] .sbh-plus {
			transform: none;
		}
	}

	@media (forced-colors: active) {
		.sbh-panel,
		.sbh-menu,
		.sbh-cta {
			border: 1px solid CanvasText;
		}
	}
</style>

<script>
	/**
	 * SolidBarHeader controller.
	 *
	 * One closure per instance, every lookup scoped to that instance's root,
	 * every listener on an AbortController aborted before a client-side
	 * navigation swaps the document. No body scroll lock: the small-screen
	 * menu is a card under the bar, not a sheet over the page.
	 */
	function initSolidBarHeader(root: HTMLElement) {
		/* Idempotent: a second boot over the same element is a no-op. */
		if (root.dataset.sbhReady === "") return;

		const burger = root.querySelector<HTMLButtonElement>("[data-sbh-burger]");
		const menu = root.querySelector<HTMLElement>("[data-sbh-menu]");
		const dropdowns = [...root.querySelectorAll<HTMLElement>("[data-sbh-dd]")];
		const subs = [...root.querySelectorAll<HTMLElement>("[data-sbh-sub]")];

		root.dataset.sbhReady = "";
		const controller = new AbortController();
		const { signal } = controller;
		const desktop = window.matchMedia("(min-width: 1200px)");
		const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)");

		/* ---- desktop dropdowns: one open at a time, per instance ---- */
		let openDd: HTMLElement | null = null;
		let openedByHover = false;
		let hoverTimer = 0;

		const triggerOf = (dd: HTMLElement) =>
			dd.querySelector<HTMLButtonElement>("[data-sbh-trigger]");

		function closeDropdown(returnFocus = false) {
			window.clearTimeout(hoverTimer);
			if (!openDd) return;
			const dd = openDd;
			openDd = null;
			openedByHover = false;
			dd.removeAttribute("data-open");
			const trigger = triggerOf(dd);
			trigger?.setAttribute("aria-expanded", "false");
			if (returnFocus) trigger?.focus({ preventScroll: true });
		}

		function openDropdown(dd: HTMLElement, byHover = false) {
			window.clearTimeout(hoverTimer);
			if (openDd === dd) return;
			closeDropdown();
			openDd = dd;
			openedByHover = byHover;
			dd.setAttribute("data-open", "");
			triggerOf(dd)?.setAttribute("aria-expanded", "true");
		}

		dropdowns.forEach((dd) => {
			const trigger = triggerOf(dd);
			trigger?.addEventListener(
				"click",
				() => {
					/* A click on a panel the pointer already opened pins it open
					   rather than snapping it shut under the cursor. */
					if (openDd === dd && openedByHover) {
						openedByHover = false;
						return;
					}
					if (openDd === dd) closeDropdown();
					else openDropdown(dd);
				},
				{ signal },
			);
			dd.addEventListener(
				"pointerenter",
				(event) => {
					if (event.pointerType !== "mouse" || !finePointer.matches) return;
					if (openDd && openDd !== dd && !openedByHover) return;
					openDropdown(dd, true);
				},
				{ signal },
			);
			dd.addEventListener(
				"pointerleave",
				(event) => {
					if (event.pointerType !== "mouse" || openDd !== dd || !openedByHover) return;
					window.clearTimeout(hoverTimer);
					hoverTimer = window.setTimeout(() => {
						if (openDd === dd && openedByHover && !dd.contains(document.activeElement)) closeDropdown();
					}, 160);
				},
				{ signal },
			);
			/* Focus leaving the item entirely closes it, so Tab past the last
			   link does not leave a panel hanging open behind the focus. */
			dd.addEventListener(
				"focusout",
				(event) => {
					const next = event.relatedTarget as Node | null;
					if (openDd === dd && next && !dd.contains(next)) closeDropdown();
				},
				{ signal },
			);
		});

		/* ---- small-screen menu ---- */
		let menuOpen = false;

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

		/* Sub-lists start collapsed once the script runs; without it they are
		   all shown, so every link stays reachable. */
		subs.forEach((sub) => {
			const toggle = sub.querySelector<HTMLButtonElement>("[data-sbh-subtoggle]");
			const list = sub.querySelector<HTMLElement>("[data-sbh-sublist]");
			if (!toggle || !list) return;
			const set = (open: boolean) => {
				toggle.setAttribute("aria-expanded", String(open));
				list.hidden = !open;
			};
			set(false);
			toggle.addEventListener(
				"click",
				() => set(toggle.getAttribute("aria-expanded") !== "true"),
				{ signal },
			);
			signal.addEventListener("abort", () => set(true));
		});

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

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

		/* Escape is bound to the document, not the root: after a client-side
		   navigation focus sits outside the header, and Escape must still work. */
		document.addEventListener(
			"keydown",
			(event) => {
				if (event.key !== "Escape") return;
				if (openDd) {
					event.preventDefault();
					const inside = openDd.contains(document.activeElement);
					closeDropdown(inside || document.activeElement === document.body);
				} else if (menuOpen) {
					event.preventDefault();
					setMenu(false, true);
				}
			},
			{ signal },
		);

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

		/* Tabbing out of the open menu closes it. */
		menu?.addEventListener(
			"focusout",
			(event) => {
				const next = event.relatedTarget as Node | null;
				if (menuOpen && next && !root.contains(next)) setMenu(false);
			},
			{ signal },
		);

		/* Crossing the breakpoint resets both. */
		desktop.addEventListener(
			"change",
			() => {
				setMenu(false);
				closeDropdown();
			},
			{ signal },
		);

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

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

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

	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/SolidBarHeader.astro =====

===== FILE (required): src/components/CircleBadgeHero.astro =====
---
/**
 * CircleBadgeHero
 *
 * A white top band with an orange eyebrow, a two-size uppercase heading
 * (a small line over one huge word), a paragraph and, on the right, three
 * overlapping client avatars with a big figure and a caption. Below it a
 * full-bleed photo under a navy wash, with a round navy badge overlapping its
 * top edge — a ring of text that turns once every 8 seconds around a static
 * arrow — and, at the bottom right, an orange block with two round arrow
 * buttons beside a white block holding one underlined link.
 *
 * No script. The ring turns with a CSS animation, off under reduced motion.
 *
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

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

export interface CircleBadgeHeroImage {
	src?: string;
	alt?: string;
}

export interface CircleBadgeHeroProps {
	eyebrow?: string;
	/** The smaller first line of the heading. `""` drops it. */
	headingTop?: string;
	/** The huge second line of the heading. */
	headingMain?: string;
	headingLevel?: "h1" | "h2";
	text?: string;
	/** Up to three round avatars. `[]` drops the row. */
	clients?: CircleBadgeHeroImage[];
	/** The big figure beside the avatars, e.g. "42k+". `""` drops it. */
	statValue?: string;
	statLabel?: string;
	/** The full-bleed photo. Omitted, or `src: ""`, renders the labelled placeholder. */
	image?: CircleBadgeHeroImage;
	/** The rotating badge. Its label is written round the ring. `null` drops it. */
	badge?: CircleBadgeHeroLink | null;
	/**
	 * The right-hand round arrow is a real link to this. The left arrow is
	 * always decorative. `null` makes both decorative.
	 */
	nextLink?: CircleBadgeHeroLink | null;
	/** The underlined link in the white block. `null` drops the whole corner card. */
	cta?: CircleBadgeHeroLink | null;
	/**
	 * Extra top padding in px, for a header that sits absolutely over the
	 * hero. The reference's is 104px. Default 0: the hero follows the header.
	 */
	headerOffset?: number;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends CircleBadgeHeroProps {}

/* No copy, 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. */
const {
	eyebrow = "",
	headingTop = "",
	headingMain = "",
	headingLevel = "h1",
	text = "",
	clients = [],
	statValue = "",
	statLabel = "",
	image: imageProp,
	badge = null,
	nextLink = null,
	cta = null,
	headerOffset = 0,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

/* Only the photo passed; with no alt it is decorative. */
const image = { src: imageProp?.src ?? "", alt: imageProp?.alt ?? "" };

const Heading = headingLevel;
const uid = id ?? `cbh-${Math.random().toString(36).slice(2, 8)}`;
const ringId = `${uid}-ring`;

const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");
/* A link needs both halves: a label with no destination, or the reverse, is not drawn. */
const link = (l: CircleBadgeHeroLink | null | undefined) => (l && l.label && l.href ? l : null);
const badgeLink = link(badge);
const nextHop = link(nextLink);
const ctaLink = link(cta);

const avatars = clients.slice(0, 3);
const ringText = badgeLink ? `${badgeLink.label} * ${badgeLink.label} * ` : "";

/* Measured slot: 1425 × 783 at desktop (~1.82:1); the file is 2000 × 1100. */
const IMAGE_SPEC = "~1.82:1 · 2000 × 1100 px";

/* A photo alone is not a hero: with no copy, figure or link, render nothing. */
const hasCopy = !!eyebrow || !!headingTop || !!headingMain || !!text;
const hasTop = hasCopy || avatars.length > 0 || !!statValue || !!statLabel;
const hasContent = hasTop || !!badgeLink || !!ctaLink;
---

{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<section
	class:list={["cbh", className]}
	id={id}
	style={`--cbh-offset: ${Math.max(0, Number(headerOffset) || 0)}px;`}
>
	{hasTop && (
	<div class="cbh-top">
		{hasCopy && (
		<div class="cbh-copy">
			{eyebrow && <p class="cbh-eyebrow">{eyebrow}</p>}
			{(headingTop || headingMain) && (
				<Heading class="cbh-heading">
					{headingTop && <span class="cbh-heading-top">{headingTop}</span>}
					<span class="cbh-heading-main">{headingMain}</span>
				</Heading>
			)}
			{text && <p class="cbh-text">{text}</p>}
		</div>
		)}
		{
			(avatars.length > 0 || statValue || statLabel) && (
				<div class="cbh-clients">
					{(avatars.length > 0 || statValue) && (
						<div class="cbh-clients-row">
							{avatars.length > 0 && (
								<ul class="cbh-avatars" role="list">
									{avatars.map((client) => (
										<li class="cbh-avatar">
											{client.src ? (
												<img src={client.src} alt={client.alt ?? ""} width="60" height="60" loading="lazy" decoding="async" />
											) : (
												<span class="cbh-avatar-ph" aria-hidden="true" />
											)}
										</li>
									))}
								</ul>
							)}
							{statValue && <p class="cbh-stat">{statValue}</p>}
						</div>
					)}
					{statLabel && <p class="cbh-stat-label">{statLabel}</p>}
				</div>
			)
		}
	</div>
	)}

	<div class="cbh-band">
		<div class="cbh-media">
			{
				image.src ? (
					<img src={image.src} alt={image.alt} width="2000" height="1100" decoding="async" fetchpriority="high" />
				) : (
					<div class="cbh-ph" role="img" aria-label="Image placeholder: wide landscape photo">
						<div class="cbh-ph-text" aria-hidden="true">
							<span class="cbh-ph-label">Image</span>
							<span class="cbh-ph-hint">Wide landscape photo</span>
							<span class="cbh-ph-spec">{IMAGE_SPEC}</span>
						</div>
					</div>
				)
			}
			<div class="cbh-wash" aria-hidden="true"></div>
		</div>

		{
			badgeLink && (
				<div class="cbh-badge-anchor">
					<a class="cbh-badge" href={safeHref(badgeLink.href)} aria-label={badgeLink.label}>
						<svg class="cbh-ring" viewBox="0 0 200 200" aria-hidden="true" focusable="false">
							<defs>
								<path id={ringId} d="M100,100 m-60,0 a60,60 0 1,1 120,0 a60,60 0 1,1 -120,0" />
							</defs>
							<text class="cbh-ring-text">
								<textPath href={`#${ringId}`} textLength="374" lengthAdjust="spacing">
									{ringText}
								</textPath>
							</text>
						</svg>
						<span class="cbh-badge-core" aria-hidden="true">
							<svg viewBox="0 0 24 24" width="24" height="24" focusable="false">
								<path d="M7 17 17 7M9 7h8v8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
							</svg>
						</span>
					</a>
				</div>
			)
		}

		{
			ctaLink && (
				<div class="cbh-corner">
					<div class="cbh-arrows">
						<span class="cbh-round cbh-round--off" aria-hidden="true">
							<svg viewBox="0 0 24 24" width="24" height="24" focusable="false">
								<path d="M19 12H5m6-6-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
							</svg>
						</span>
						{nextHop ? (
							<a class="cbh-round" href={safeHref(nextHop.href)} aria-label={nextHop.label}>
								<span class="cbh-round-core" aria-hidden="true">
									<svg viewBox="0 0 24 24" width="24" height="24" focusable="false">
										<path d="M5 12h14m-6-6 6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
									</svg>
								</span>
							</a>
						) : (
							<span class="cbh-round cbh-round--static" aria-hidden="true">
								<span class="cbh-round-core">
									<svg viewBox="0 0 24 24" width="24" height="24" focusable="false">
										<path d="M5 12h14m-6-6 6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
									</svg>
								</span>
							</span>
						)}
					</div>
					<div class="cbh-cta-wrap">
						<a class="cbh-cta" href={safeHref(ctaLink.href)}>{ctaLink.label}</a>
					</div>
				</div>
			)
		}
	</div>
</section>
)}

<style>
	.cbh {
		--cbh-navy: var(--tg-navy, #020e28);
		--cbh-teal: #0e2d3a;
		--cbh-orange: var(--tg-orange, #fd5523);
		--cbh-muted: var(--tg-muted, #61697d);
		--cbh-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
		--cbh-body: var(--tg-font-body, "DM Sans", "DM Sans Placeholder", ui-sans-serif, system-ui, sans-serif);
		--cbh-ease: cubic-bezier(0.25, 0.1, 0.25, 1);

		box-sizing: border-box;
		position: relative;
		display: flex;
		flex-direction: column;
		gap: 36px;
		padding-top: calc(40px + var(--cbh-offset, 0px));
		background: #fff;
		color: var(--cbh-navy);
		overflow-x: clip;
	}

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

	/* ---- top band ---- */
	.cbh-top {
		display: flex;
		flex-direction: column;
		gap: 30px;
		width: 100%;
		max-width: 1293px;
		margin: 0 auto;
		padding: 0 20px;
	}

	.cbh-copy {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		min-width: 0;
	}

	.cbh .cbh-eyebrow {
		margin: 0 0 20px;
		font: 600 16px/1 var(--cbh-display);
		text-transform: uppercase;
		color: var(--cbh-orange);
	}

	.cbh-heading {
		display: flex;
		flex-direction: column;
		gap: 16px;
		margin: 0;
		font-family: var(--cbh-display);
		text-transform: uppercase;
		letter-spacing: normal;
		color: var(--cbh-navy);
	}

	.cbh-heading-top {
		display: block;
		font-size: clamp(40px, 5.2vw, 74px);
		font-weight: 700;
		line-height: 0.95;
	}

	.cbh-heading-main {
		display: block;
		font-size: clamp(50px, 11.16vw, 159px);
		font-weight: 800;
		line-height: 0.9;
	}

	.cbh .cbh-text {
		max-width: 834px;
		margin: 20px 0 0;
		font: 400 16px/1.155 var(--cbh-body);
		color: var(--cbh-navy);
	}

	.cbh-clients {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 20px;
		flex: none;
	}

	.cbh-clients-row {
		display: flex;
		align-items: center;
		gap: 16px;
	}

	.cbh .cbh-avatars {
		display: flex;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.cbh-avatar {
		width: 40px;
		height: 40px;
		border-radius: 50%;
		overflow: hidden;
		background: #ecf0f1;
	}

	.cbh-avatar + .cbh-avatar {
		margin-left: -14px;
	}

	.cbh-avatar img,
	.cbh-avatar-ph {
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	.cbh-avatar-ph {
		background: repeating-linear-gradient(135deg, #e6e9ee 0 6px, #edf0f3 6px 12px);
	}

	.cbh .cbh-stat {
		margin: 0;
		font: 600 34px/normal var(--cbh-display);
		color: var(--cbh-navy);
	}

	.cbh .cbh-stat-label {
		max-width: 190px;
		margin: 0;
		font: 600 18px/1.5 var(--cbh-display);
		color: var(--cbh-muted);
	}

	/* ---- photo band ---- */
	.cbh-band {
		position: relative;
		display: flex;
		flex-direction: column;
		justify-content: flex-end;
		min-height: 332px;
	}

	.cbh-media {
		position: absolute;
		inset: 0;
		overflow: hidden;
		background: var(--cbh-navy);
	}

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

	.cbh-wash {
		position: absolute;
		inset: 0;
		background: rgb(2 14 40 / 0.5);
	}

	/* Placeholder for `src: ""`. */
	.cbh-ph {
		display: grid;
		place-items: center;
		width: 100%;
		height: 100%;
		padding: 16px;
		text-align: center;
		background: repeating-linear-gradient(135deg, #1b2740 0 12px, #213050 12px 24px);
		color: rgb(255 255 255 / 0.75);
	}

	.cbh-ph-text {
		position: relative;
		z-index: 1;
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 8px;
	}

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

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

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

	/* ---- rotating badge: hidden on a phone, as measured ---- */
	.cbh-badge-anchor {
		display: none;
	}

	.cbh-badge {
		position: absolute;
		display: grid;
		place-items: center;
		width: 182px;
		height: 182px;
		border-radius: 50%;
		background: var(--cbh-navy);
		color: #fff;
		text-decoration: none;
	}

	.cbh-badge:focus-visible {
		outline: 3px solid var(--cbh-orange);
		outline-offset: 4px;
	}

	.cbh-ring {
		position: absolute;
		inset: 0;
		width: 100%;
		height: 100%;
		animation: cbh-spin 8s linear infinite;
	}

	.cbh-ring-text {
		fill: #fff;
		font: 600 19px var(--cbh-display);
	}

	.cbh-badge-core {
		display: grid;
		place-items: center;
		width: 91px;
		height: 91px;
		border: 1px solid rgb(255 255 255 / 0.85);
		border-radius: 50%;
	}

	@keyframes cbh-spin {
		to {
			transform: rotate(360deg);
		}
	}

	/* ---- corner card: orange arrows + white link ---- */
	.cbh-corner {
		position: relative;
		z-index: 1;
		display: flex;
		align-self: flex-end;
		width: 100%;
		height: 82px;
	}

	.cbh-arrows {
		display: flex;
		align-items: center;
		justify-content: center;
		gap: 10px;
		flex: 0 0 47%;
		background: var(--cbh-orange);
	}

	.cbh-round {
		display: grid;
		place-items: center;
		width: 44px;
		height: 44px;
		border-radius: 50px;
		background: #fff;
		color: var(--cbh-navy);
		transition: background-color 0.3s var(--cbh-ease);
	}

	.cbh-round-core {
		display: grid;
		place-items: center;
		color: var(--cbh-navy);
		transition: color 0.3s var(--cbh-ease);
	}

	.cbh-round--off {
		background: rgb(255 255 255 / 0.6);
		color: rgb(14 45 58 / 0.6);
	}

	a.cbh-round:hover {
		background: var(--cbh-teal);
	}

	a.cbh-round:hover .cbh-round-core {
		color: #fff;
	}

	a.cbh-round:focus-visible {
		outline: 3px solid #fff;
		outline-offset: 3px;
	}

	.cbh-cta-wrap {
		display: flex;
		align-items: center;
		justify-content: center;
		flex: 1 1 auto;
		min-width: 0;
		padding: 0 12px;
		background: #fff;
	}

	.cbh-cta {
		display: inline-flex;
		align-items: center;
		min-height: 44px;
		font: 700 18px/1.1 var(--cbh-display);
		color: var(--cbh-navy);
		text-align: center;
		text-decoration: underline;
		text-decoration-thickness: 2px;
		text-underline-offset: 4px;
		transition: color 0.3s var(--cbh-ease);
	}

	.cbh-cta:hover {
		color: var(--cbh-orange);
	}

	.cbh-cta:focus-visible {
		outline: 2px solid var(--cbh-orange);
		outline-offset: 4px;
	}

	/* ---- tablet ---- */
	@media (min-width: 810px) {
		.cbh {
			gap: 60px;
			padding-top: calc(80px + var(--cbh-offset, 0px));
		}

		.cbh-top {
			flex-direction: row;
			justify-content: space-between;
			align-items: center;
			gap: 40px;
		}

		.cbh .cbh-eyebrow {
			margin-bottom: 30px;
			font-size: 18px;
			line-height: 0.7;
		}

		.cbh-heading {
			gap: 40px;
		}

		.cbh-heading-top {
			line-height: 0.705;
		}

		.cbh-heading-main {
			line-height: 0.708;
		}

		.cbh .cbh-text {
			max-width: min(834px, 62vw);
			margin-top: 40px;
		}

		.cbh-avatar {
			width: 60px;
			height: 60px;
		}

		.cbh-avatar + .cbh-avatar {
			margin-left: -22px;
		}

		.cbh .cbh-stat-label {
			max-width: 170px;
			font-size: 20px;
			line-height: 30px;
		}

		.cbh-band {
			min-height: 0;
			aspect-ratio: 16 / 10;
		}

		.cbh-badge-anchor {
			position: absolute;
			inset: 0 0 auto;
			z-index: 2;
			display: block;
			max-width: 1293px;
			margin: 0 auto;
			padding: 0 20px;
		}

		.cbh-badge {
			top: -78px;
			right: clamp(40px, 10vw, 143px);
			width: 150px;
			height: 150px;
		}

		.cbh-badge-core {
			width: 75px;
			height: 75px;
		}

		.cbh-corner {
			width: min(520px, 70%);
			height: 120px;
		}

		.cbh-arrows {
			flex-basis: 37%;
		}

		.cbh-round {
			width: 46px;
			height: 46px;
		}

		.cbh-cta {
			font-size: 22px;
		}
	}

	/* ---- desktop: the measured 1425 × 1376 frame ---- */
	@media (min-width: 1200px) {
		.cbh {
			padding-top: calc(105px + var(--cbh-offset, 0px));
		}

		.cbh .cbh-text {
			max-width: 834px;
		}

		.cbh-band {
			aspect-ratio: 1425 / 783;
		}

		.cbh-badge {
			right: 143px;
			width: 182px;
			height: 182px;
		}

		.cbh-badge-core {
			width: 91px;
			height: 91px;
		}

		.cbh-corner {
			width: 607px;
			height: 152px;
		}

		.cbh-arrows {
			flex: 0 0 226px;
		}

		.cbh-cta {
			font-size: 24px;
			line-height: 0.71;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.cbh-ring {
			animation: none;
		}
	}

	@media (forced-colors: active) {
		.cbh-badge,
		.cbh-round {
			border: 1px solid ButtonText;
		}

		.cbh-ring-text {
			fill: CanvasText;
		}
	}
</style>

===== END src/components/CircleBadgeHero.astro =====

===== FILE (required): public/circle-badge-hero/port-trucks.webp =====
[binary asset — download https://astro.baysixmedia.com/circle-badge-hero/port-trucks.webp and save it to public/circle-badge-hero/port-trucks.webp]

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

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

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

===== FILE (required): src/components/IconStatBand.astro =====
---
/**
 * IconStatBand
 *
 * A navy band over a dimmed road photograph with four stats in a row: an
 * orange line icon, a big white figure with its suffix ("k+", "+") and a
 * short label. The row rises 40px into place the first time it scrolls into
 * view, and each figure counts up from zero to its value.
 *
 * The final figures are in the HTML, so without JavaScript or under reduced
 * motion the band simply shows them. Stacks into one centred column on a
 * phone, two by two on a tablet.
 *
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

export type IconStatBandIcon = "transit" | "team" | "experience" | "customers" | "truck" | "warehouse";

export interface IconStatBandItem {
	/** One of the built-in orange line icons. */
	icon?: IconStatBandIcon;
	/** The figure, a whole number. Counted up from 0. */
	value: number;
	/** Written straight after the figure, e.g. "k+", "+", "%". */
	suffix?: string;
	label: string;
}

export interface IconStatBandProps {
	items?: IconStatBandItem[];
	/** Background photo under the navy wash. `src: ""` leaves plain navy. */
	image?: { src?: string; alt?: string };
	/** Count-up length in ms. `0` shows the figures without counting. */
	countDuration?: number;
	/** Gives the section an accessible name. Empty by default. */
	ariaLabel?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends IconStatBandProps {}

const {
	items = [],
	image: imageProp,
	countDuration = 2000,
	ariaLabel,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

/* Background only: always decorative. No photo unless one is passed. */
const image: { src?: string; alt?: string } = imageProp ?? {};
const duration = Math.max(0, Number(countDuration) || 0);
const fmt = (n: number) => Math.round(n).toLocaleString("en-US");
/* A stat only with a real figure and a label: a partial item renders nothing, never "undefined". */
const stats = (items ?? []).filter(
	(i) => i && typeof i.value === "number" && Number.isFinite(i.value) && typeof i.label === "string" && i.label.trim(),
);
---

{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{stats.length > 0 && (
<section
	class:list={["isb", className]}
	id={id}
	aria-label={ariaLabel || undefined}
	data-isb-root
	data-isb-duration={duration}
>
	<div class="isb-bg" aria-hidden="true">
		{image.src && <img src={image.src} alt="" width="2000" height="260" loading="lazy" decoding="async" />}
	</div>
	<ul class="isb-row" role="list" data-isb-row>
		{
			stats.map((item) => (
				<li class="isb-item">
					<span class="isb-icon" aria-hidden="true">
						<svg viewBox="0 0 66 60" width="66" height="60" focusable="false">
							{item.icon === "team" ? (
								<g>
									<path d="m33 3 1.9 3.9 4.3.6-3.1 3 .7 4.2-3.8-2-3.8 2 .7-4.2-3.1-3 4.3-.6Z" />
									<circle cx="33" cy="25" r="6" />
									<path d="M22 50V41a11 11 0 0 1 22 0v9Z" />
									<circle cx="15" cy="30" r="5" />
									<path d="M6 52v-7a9 9 0 0 1 15-6.7" />
									<circle cx="51" cy="30" r="5" />
									<path d="M60 52v-7a9 9 0 0 0-15-6.7" />
									<path d="M4 56h58" />
								</g>
							) : item.icon === "experience" ? (
								<g>
									<circle cx="20" cy="15" r="7" />
									<path d="M7 42v-6a13 13 0 0 1 26 0v6Z" />
									<path d="M16 29l4 5 4-5" />
									<rect x="38" y="4" width="20" height="26" rx="2" />
									<path d="M43 11h10M43 16h10M43 21h7" />
									<path d="M34 8h-4v26" />
									<path d="m9 49 1.2 2.4 2.6.4-1.9 1.8.5 2.6L9 55l-2.4 1.2.5-2.6-1.9-1.8 2.6-.4Z" />
									<path d="m21 49 1.2 2.4 2.6.4-1.9 1.8.5 2.6L21 55l-2.4 1.2.5-2.6-1.9-1.8 2.6-.4Z" />
									<path d="m33 49 1.2 2.4 2.6.4-1.9 1.8.5 2.6L33 55l-2.4 1.2.5-2.6-1.9-1.8 2.6-.4Z" />
									<path d="m45 49 1.2 2.4 2.6.4-1.9 1.8.5 2.6L45 55l-2.4 1.2.5-2.6-1.9-1.8 2.6-.4Z" />
									<path d="m57 49 1.2 2.4 2.6.4-1.9 1.8.5 2.6L57 55l-2.4 1.2.5-2.6-1.9-1.8 2.6-.4Z" />
								</g>
							) : item.icon === "customers" ? (
								<g>
									<path d="m13 4 1.6 3.3 3.6.5-2.6 2.5.6 3.6L13 12.2l-3.2 1.7.6-3.6-2.6-2.5 3.6-.5Z" />
									<path d="m33 2 1.6 3.3 3.6.5-2.6 2.5.6 3.6L33 10.2l-3.2 1.7.6-3.6-2.6-2.5 3.6-.5Z" />
									<path d="m53 4 1.6 3.3 3.6.5-2.6 2.5.6 3.6L53 12.2l-3.2 1.7.6-3.6-2.6-2.5 3.6-.5Z" />
									<circle cx="33" cy="26" r="6" />
									<path d="M22 56v-8a11 11 0 0 1 22 0v8Z" />
									<circle cx="14" cy="31" r="5" />
									<path d="M4 56v-5a10 10 0 0 1 16-8" />
									<circle cx="52" cy="31" r="5" />
									<path d="M62 56v-5a10 10 0 0 0-16-8" />
								</g>
							) : item.icon === "truck" ? (
								<g>
									<path d="M3 14h36v28H3Z" />
									<path d="M39 22h12l10 11v9H39Z" />
									<path d="M44 26v8h13" />
									<circle cx="14" cy="46" r="5" />
									<circle cx="50" cy="46" r="5" />
									<path d="M3 52h58" />
								</g>
							) : item.icon === "warehouse" ? (
								<g>
									<path d="M4 22 33 6l29 16v32H4Z" />
									<path d="M14 54V30h38v24" />
									<path d="M20 36h12v9H20ZM34 36h12v9H34ZM27 45h12v9H27Z" />
								</g>
							) : (
								<g>
									<path d="M8 13h14l7-9h5l-3 9h11l4-4h4l-2 7 2 7h-4l-4-4H31l3 9h-5l-7-9H8" />
									<circle cx="33" cy="40" r="15" />
									<path d="M18 40h30M33 25c-5 4-7 9-7 15s2 11 7 15M33 25c5 4 7 9 7 15s-2 11-7 15" />
									<path d="M6 58h12M48 58h12M24 58h2M30 58h2M36 58h2M42 58h2" />
								</g>
							)}
						</svg>
					</span>
					<span class="isb-text">
						<span class="isb-figure">
							<span class="isb-sr">{fmt(item.value)}{item.suffix ?? ""}</span>
							<span class="isb-num" aria-hidden="true" data-isb-num data-value={item.value}>{fmt(item.value)}</span><span class="isb-suffix" aria-hidden="true">{item.suffix ?? ""}</span>
						</span>
						<span class="isb-label">{item.label}</span>
					</span>
				</li>
			))
		}
	</ul>
</section>
)}

<style>
	.isb {
		--isb-navy: var(--tg-navy, #020e28);
		--isb-orange: var(--tg-orange, #fd5523);
		--isb-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
		--isb-body: var(--tg-font-body, "DM Sans", "DM Sans Placeholder", ui-sans-serif, system-ui, sans-serif);
		--isb-ease: cubic-bezier(0.25, 0.1, 0.25, 1);

		box-sizing: border-box;
		position: relative;
		isolation: isolate;
		padding: 60px 20px;
		background: var(--isb-navy);
		color: #fff;
		overflow: hidden;
	}

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

	/* The photo sits under a navy wash at 90%, as measured. */
	.isb-bg {
		position: absolute;
		inset: 0;
		z-index: -1;
		background: var(--isb-navy);
	}

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

	.isb-bg::after {
		content: "";
		position: absolute;
		inset: 0;
		background: rgb(2 14 40 / 0.9);
	}

	.isb .isb-row {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 28px;
		max-width: 1253px;
		margin: 0 auto;
		padding: 0;
		list-style: none;
		transition:
			opacity 0.8s var(--isb-ease),
			transform 0.8s var(--isb-ease);
	}

	.isb[data-isb-ready]:not([data-in]) .isb-row {
		opacity: 0;
		transform: translateY(40px);
	}

	.isb-item {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 10px;
		text-align: center;
	}

	.isb-icon {
		display: block;
		flex: none;
		width: 40px;
		height: 36px;
		color: var(--isb-orange);
	}

	.isb-icon svg {
		display: block;
		width: 100%;
		height: 100%;
		fill: none;
		stroke: currentColor;
		stroke-width: 2;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	.isb-text {
		display: flex;
		flex-direction: column;
		align-items: center;
	}

	.isb-figure {
		font: 600 28px/normal var(--isb-display);
		font-variant-numeric: tabular-nums;
		color: #fff;
		white-space: nowrap;
	}

	.isb-label {
		font: 400 16px/21.04px var(--isb-body);
		color: #ecf0f1;
	}

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

	/* ---- tablet: two by two ---- */
	@media (min-width: 810px) {
		.isb .isb-row {
			display: grid;
			grid-template-columns: repeat(2, minmax(0, max-content));
			justify-content: space-around;
			gap: 36px 60px;
		}

		.isb-item {
			flex-direction: row;
			gap: 24px;
			text-align: left;
		}

		.isb-icon {
			width: 66px;
			height: 60px;
		}

		.isb-icon svg {
			stroke-width: 1.6;
		}

		.isb-text {
			align-items: flex-start;
		}

		.isb-figure {
			font-size: 34px;
		}
	}

	/* ---- desktop: one row, space-between, as measured ---- */
	@media (min-width: 1200px) {
		.isb .isb-row {
			display: flex;
			flex-direction: row;
			justify-content: space-between;
			align-items: center;
		}

		.isb-item {
			min-width: 240px;
		}
	}

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

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

	@media (forced-colors: active) {
		.isb-icon {
			color: CanvasText;
		}
	}
</style>

<script>
	/*
	 * Appear-on-scroll and count-up. One IntersectionObserver per instance,
	 * scoped to its root; it sets `data-in` once, then counts each figure from
	 * 0 to its data-value with requestAnimationFrame. Everything is torn down
	 * on astro:before-swap. Under reduced motion nothing hides or counts: the
	 * final figures, already in the HTML, stay as they are.
	 */
	function initIconStatBand(root: HTMLElement) {
		if (root.dataset.isbReady === "") return;
		const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
		if (reduced || !("IntersectionObserver" in window)) {
			root.dataset.in = "";
			return;
		}

		const controller = new AbortController();
		const { signal } = controller;
		const duration = Number(root.dataset.isbDuration) || 0;
		const nums = Array.from(root.querySelectorAll<HTMLElement>("[data-isb-num]"));
		const fmt = (n: number) => Math.round(n).toLocaleString("en-US");
		let frame = 0;

		root.dataset.isbReady = "";
		if (duration > 0) nums.forEach((el) => (el.textContent = "0"));

		const count = () => {
			if (duration <= 0) return;
			const start = performance.now();
			const tick = (now: number) => {
				const t = Math.min(1, (now - start) / duration);
				const eased = 1 - Math.pow(1 - t, 3);
				nums.forEach((el) => (el.textContent = fmt(Number(el.dataset.value) * eased)));
				if (t < 1 && !signal.aborted) frame = requestAnimationFrame(tick);
			};
			frame = requestAnimationFrame(tick);
		};

		const observer = new IntersectionObserver(
			(entries) => {
				if (entries.some((entry) => entry.isIntersecting)) {
					root.dataset.in = "";
					observer.disconnect();
					count();
				}
			},
			{ threshold: 0.25 },
		);
		observer.observe(root);

		signal.addEventListener("abort", () => {
			observer.disconnect();
			cancelAnimationFrame(frame);
		});

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

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

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

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

===== END src/components/IconStatBand.astro =====

===== FILE (required): public/icon-stat-band/highway-dusk.webp =====
[binary asset — download https://astro.baysixmedia.com/icon-stat-band/highway-dusk.webp and save it to public/icon-stat-band/highway-dusk.webp]

===== FILE (required): src/components/CommittedAboutSplit.astro =====
---
/**
 * CommittedAboutSplit
 *
 * A two-column about section: a rounded photo on one side, and on the other an
 * orange eyebrow with a short rule, a heading, a paragraph, three "»" points
 * and an orange pill link. `mediaSide` puts the photo left or right.
 *
 * Give it a `videoUrl` and the photo becomes a video thumbnail: a navy wash,
 * a round play button in the middle, and a modal <dialog> that plays the
 * video (a YouTube/Vimeo embed in an iframe, or an .mp4/.webm file in a
 * <video>). Without JavaScript the play button is a plain link to the video.
 *
 * On scroll the photo slides in from its outer side (40px) and the copy from
 * the other side.
 *
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

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

export interface CommittedAboutSplitProps {
	/** Which side the photo sits on at 810px and up. On a phone the DOM order holds. */
	mediaSide?: "left" | "right";
	/** The photo, or the video thumbnail. No `src` renders the labelled placeholder. */
	image?: { src?: string; alt?: string };
	/** Turns the photo into a video thumbnail with a play button and a modal player. */
	videoUrl?: string;
	/** Accessible name for the play button and the player. Omitted: "Play video" / "Video". */
	videoTitle?: string;
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	text?: string;
	/** The "»" points. `[]` drops the list. */
	points?: string[];
	/** The orange pill. `null` (the default) drops it. */
	cta?: CommittedAboutSplitLink | null;
	/** Section surface. The home variant is #ecf0f1, the about variant white. */
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends CommittedAboutSplitProps {}

/*
 * The heading, copy, points, button, photo and video title are the caller's:
 * nothing about a business is filled in here. "About us" is a generic label
 * and never renders the section on its own. The catalog demo passes its
 * sample content through the registry's `props`.
 */
const {
	mediaSide = "left",
	image: imageProp,
	videoUrl,
	videoTitle = "",
	eyebrow = "About us",
	heading = "",
	headingLevel = "h2",
	text = "",
	points = [],
	cta = null,
	background = "#ecf0f1",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const hasVideo = Boolean(videoUrl && videoUrl.trim());
/* A photo given without alt is decorative. No photo: the neutral placeholder. */
const image = { src: imageProp?.src ?? "", alt: imageProp?.alt ?? "" };
const pointList = (points ?? []).filter((p) => typeof p === "string" && p.trim());
const title = videoTitle.trim();
const playLabel = title ? `Play video: ${title}` : "Play video";

/* Copy worth a column; the generic eyebrow alone is not. */
const hasCopy = Boolean(heading || text || pointList.length > 0 || cta);
/* Nothing to say → nothing rendered. */
const hasContent = hasCopy || hasVideo;

const Heading = headingLevel;
const uid = id ?? `cas-${Math.random().toString(36).slice(2, 8)}`;
const dialogId = `${uid}-player`;
const titleId = `${uid}-player-title`;

const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");
const video = hasVideo ? safeHref(videoUrl!) : "";
const isFile = /\.(mp4|webm|ogg)(\?|#|$)/i.test(video);

const IMAGE_SPEC = hasVideo ? "1:1 · 1128 × 1124 px" : "~0.95:1 · 1152 × 1214 px";
const IMAGE_HINT = hasVideo ? "Video thumbnail, square" : "Photo, near-square portrait";
---

{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<section
	class:list={["cas", className]}
	id={id}
	style={`--cas-bg: ${background};`}
	data-cas-root
	data-media-side={mediaSide}
	data-video={hasVideo ? "" : undefined}
>
	<div class="cas-inner">
		{
			mediaSide === "right" && hasCopy && (
				<div class="cas-copy" data-cas-copy>
					{eyebrow && (
						<p class="cas-eyebrow">
							{eyebrow}
							<span class="cas-rule" aria-hidden="true" />
						</p>
					)}
					{heading && <Heading class="cas-heading">{heading}</Heading>}
					{text && <p class="cas-text">{text}</p>}
					{pointList.length > 0 && (
						<ul class="cas-points" role="list">
							{pointList.map((point) => (
								<li class="cas-point">
									<svg class="cas-chev" viewBox="0 0 18 14" width="18" height="14" aria-hidden="true" focusable="false">
										<path d="M2 1.5 7.5 7 2 12.5M9.5 1.5 15 7l-5.5 5.5" />
									</svg>
									<span>{point}</span>
								</li>
							))}
						</ul>
					)}
					{cta && (
						<a class="cas-btn" href={safeHref(cta.href)}>
							{cta.label}
							<svg class="cas-btn-arrow" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false">
								<path d="M2 10 10 2M3.5 2H10v6.5" />
							</svg>
						</a>
					)}
				</div>
			)
		}

		<div class="cas-media" data-cas-media>
			{
				image.src ? (
					<img
						src={image.src}
						alt={image.alt}
						width={hasVideo ? 1128 : 1152}
						height={hasVideo ? 1124 : 1214}
						loading="lazy"
						decoding="async"
					/>
				) : (
					<div class="cas-ph" role="img" aria-label={`Image placeholder: ${IMAGE_HINT}`}>
						<div class="cas-ph-text" aria-hidden="true">
							<span class="cas-ph-label">Image</span>
							<span class="cas-ph-hint">{IMAGE_HINT}</span>
							<span class="cas-ph-spec">{IMAGE_SPEC}</span>
						</div>
					</div>
				)
			}
			{
				hasVideo && (
					<>
						<span class="cas-wash" aria-hidden="true" />
						<a
							class="cas-play"
							href={video}
							target="_blank"
							rel="noopener"
							aria-label={playLabel}
							aria-haspopup="dialog"
							data-cas-play
						>
							<span class="cas-play-halo" aria-hidden="true" />
							<span class="cas-play-disc" aria-hidden="true">
								<svg viewBox="0 0 12 14" width="12" height="14" focusable="false">
									<path d="M1 1.2v11.6a.6.6 0 0 0 .9.5l9.6-5.8a.6.6 0 0 0 0-1L1.9.7a.6.6 0 0 0-.9.5Z" />
								</svg>
							</span>
						</a>
					</>
				)
			}
		</div>

		{
			mediaSide !== "right" && hasCopy && (
				<div class="cas-copy" data-cas-copy>
					{eyebrow && (
						<p class="cas-eyebrow">
							{eyebrow}
							<span class="cas-rule" aria-hidden="true" />
						</p>
					)}
					{heading && <Heading class="cas-heading">{heading}</Heading>}
					{text && <p class="cas-text">{text}</p>}
					{pointList.length > 0 && (
						<ul class="cas-points" role="list">
							{pointList.map((point) => (
								<li class="cas-point">
									<svg class="cas-chev" viewBox="0 0 18 14" width="18" height="14" aria-hidden="true" focusable="false">
										<path d="M2 1.5 7.5 7 2 12.5M9.5 1.5 15 7l-5.5 5.5" />
									</svg>
									<span>{point}</span>
								</li>
							))}
						</ul>
					)}
					{cta && (
						<a class="cas-btn" href={safeHref(cta.href)}>
							{cta.label}
							<svg class="cas-btn-arrow" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false">
								<path d="M2 10 10 2M3.5 2H10v6.5" />
							</svg>
						</a>
					)}
				</div>
			)
		}
	</div>

	{
		hasVideo && (
			<dialog class="cas-dialog" id={dialogId} aria-labelledby={titleId} data-cas-dialog data-src={video} data-kind={isFile ? "file" : "embed"}>
				<div class="cas-dialog-bar">
					<p class="cas-dialog-title" id={titleId}>{title || "Video"}</p>
					<button type="button" class="cas-close" data-cas-close aria-label="Close video">
						<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false">
							<path d="M6 6l12 12M18 6 6 18" />
						</svg>
					</button>
				</div>
				<div class="cas-frame" data-cas-frame />
			</dialog>
		)
	}
</section>
)}

<style>
	.cas {
		--cas-navy: var(--tg-navy, #020e28);
		--cas-orange: var(--tg-orange, #fd5523);
		--cas-muted: var(--tg-muted, #61697d);
		--cas-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
		--cas-body: var(--tg-font-body, "DM Sans", "DM Sans Placeholder", ui-sans-serif, system-ui, sans-serif);
		--cas-ease: cubic-bezier(0.25, 0.1, 0.25, 1);
		--cas-radius: 30px;

		box-sizing: border-box;
		padding: 80px 20px;
		background: var(--cas-bg, #ecf0f1);
		color: var(--cas-navy);
		overflow-x: clip;
	}

	.cas[data-video] {
		--cas-radius: 20px;
	}

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

	.cas-inner {
		display: flex;
		flex-direction: column;
		gap: 30px;
		max-width: 1253px;
		margin: 0 auto;
	}

	/* ---- media ---- */
	.cas-media {
		position: relative;
		width: 100%;
		aspect-ratio: 350 / 278;
		border-radius: var(--cas-radius);
		overflow: hidden;
		background: #dfe5e7;
	}

	.cas[data-video] .cas-media {
		aspect-ratio: 1;
	}

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

	.cas-wash {
		position: absolute;
		inset: 0;
		background: rgb(2 14 40 / 0.5);
	}

	.cas-play {
		position: absolute;
		top: 50%;
		left: 50%;
		display: grid;
		place-items: center;
		width: 64px;
		height: 64px;
		margin: -32px 0 0 -32px;
		border-radius: 50%;
		cursor: pointer;
	}

	.cas-play-halo {
		position: absolute;
		inset: 0;
		border-radius: 50%;
		background: #fff;
		opacity: 0.3;
	}

	.cas-play-disc {
		position: relative;
		display: grid;
		place-items: center;
		width: 40px;
		height: 40px;
		padding-left: 2px;
		border-radius: 50%;
		background: #fff;
		transition: transform 0.3s var(--cas-ease);
	}

	.cas-play-disc svg {
		fill: var(--cas-orange);
	}

	.cas-play:hover .cas-play-disc {
		transform: scale(1.05);
	}

	.cas-play:focus-visible {
		outline: 3px solid #fff;
		outline-offset: 4px;
	}

	.cas-ph {
		display: grid;
		place-items: center;
		width: 100%;
		height: 100%;
		padding: 16px;
		text-align: center;
		background: repeating-linear-gradient(135deg, #e6e9ee 0 12px, #edf0f3 12px 24px);
		color: rgb(2 14 40 / 0.62);
	}

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

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

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

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

	/* ---- copy ---- */
	.cas-copy {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		min-width: 0;
	}

	.cas .cas-eyebrow {
		display: inline-flex;
		align-items: center;
		gap: 12px;
		margin: 0 0 16px;
		font: 600 16px/1 var(--cas-display);
		text-transform: uppercase;
		color: var(--cas-orange);
	}

	.cas-rule {
		display: none;
		width: 40px;
		height: 1px;
		background: var(--cas-orange);
	}

	.cas-heading {
		margin: 0;
		font: 600 32px/1.2 var(--cas-display);
		letter-spacing: normal;
		text-transform: capitalize;
		color: var(--cas-navy);
	}

	.cas .cas-text {
		margin: 14px 0 0;
		font: 400 16px/21.04px var(--cas-body);
		color: var(--cas-muted);
	}

	.cas .cas-points {
		display: flex;
		flex-direction: column;
		gap: 14px;
		margin: 22px 0 0;
		padding: 0;
		list-style: none;
	}

	.cas-point {
		display: flex;
		align-items: flex-start;
		gap: 12px;
		font: 600 18px/26px var(--cas-display);
		color: var(--cas-navy);
	}

	.cas-chev {
		flex: none;
		margin-top: 6px;
		fill: none;
		stroke: var(--cas-orange);
		stroke-width: 2.6;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	.cas .cas-btn {
		display: inline-flex;
		align-items: center;
		gap: 10px;
		min-height: 48px;
		margin-top: 30px;
		padding: 18px 24px;
		border-radius: 100px;
		background: var(--cas-orange);
		font: 600 14px/12.46px var(--cas-body);
		text-transform: capitalize;
		color: #fff;
		text-decoration: none;
		transition: background-color 0.3s var(--cas-ease);
	}

	.cas-btn-arrow {
		fill: none;
		stroke: currentColor;
		stroke-width: 1.8;
		stroke-linecap: round;
		stroke-linejoin: round;
		transition: transform 0.3s var(--cas-ease);
	}

	.cas .cas-btn:hover {
		background: var(--cas-navy);
	}

	.cas .cas-btn:hover .cas-btn-arrow {
		transform: rotate(45deg);
	}

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

	/* ---- appear: media from its outer side, copy from the other ---- */
	.cas-media,
	.cas-copy {
		transition:
			opacity 0.8s var(--cas-ease),
			transform 0.8s var(--cas-ease);
	}

	.cas[data-cas-ready]:not([data-in]) .cas-media,
	.cas[data-cas-ready]:not([data-in]) .cas-copy {
		opacity: 0;
	}

	.cas[data-cas-ready][data-media-side="left"]:not([data-in]) .cas-media,
	.cas[data-cas-ready][data-media-side="right"]:not([data-in]) .cas-copy {
		transform: translateX(-40px);
	}

	.cas[data-cas-ready][data-media-side="left"]:not([data-in]) .cas-copy,
	.cas[data-cas-ready][data-media-side="right"]:not([data-in]) .cas-media {
		transform: translateX(40px);
	}

	/* ---- dialog ---- */
	.cas-dialog {
		width: min(960px, calc(100vw - 32px));
		max-height: calc(100vh - 32px);
		padding: 0;
		border: 0;
		border-radius: 16px;
		background: var(--cas-navy);
		color: #fff;
		overflow: hidden;
	}

	.cas-dialog::backdrop {
		background: rgb(2 14 40 / 0.8);
	}

	.cas-dialog-bar {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 12px;
		padding: 6px 6px 6px 18px;
	}

	.cas .cas-dialog-title {
		margin: 0;
		font: 600 16px/1.3 var(--cas-display);
	}

	.cas-close {
		display: grid;
		place-items: center;
		flex: none;
		width: 44px;
		height: 44px;
		padding: 0;
		border: 0;
		border-radius: 50%;
		background: transparent;
		color: #fff;
		cursor: pointer;
		transition: background-color 0.3s var(--cas-ease);
	}

	.cas-close svg {
		fill: none;
		stroke: currentColor;
		stroke-width: 2;
		stroke-linecap: round;
	}

	.cas-close:hover {
		background: var(--cas-orange);
	}

	.cas-close:focus-visible {
		outline: 2px solid var(--cas-orange);
		outline-offset: 2px;
	}

	.cas-frame {
		position: relative;
		aspect-ratio: 16 / 9;
		background: #000;
	}

	.cas-frame :global(iframe),
	.cas-frame :global(video) {
		position: absolute;
		inset: 0;
		width: 100%;
		height: 100%;
		border: 0;
	}

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

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

		.cas-media {
			flex: 0 0 46%;
			aspect-ratio: 576 / 607;
		}

		.cas[data-video] .cas-media {
			flex-basis: 45%;
			aspect-ratio: 564 / 562;
		}

		.cas-copy {
			flex: 0 1 619px;
		}

		.cas .cas-eyebrow {
			margin-bottom: 24px;
			font-size: 18px;
			line-height: 12.6px;
		}

		.cas-rule {
			display: block;
		}

		.cas-heading {
			font-size: 40px;
		}

		.cas .cas-text {
			margin-top: 40px;
		}

		.cas .cas-points {
			gap: 16px;
			margin-top: 40px;
		}

		.cas-point {
			font-size: 20px;
			line-height: 30px;
		}

		.cas-chev {
			margin-top: 8px;
		}

		.cas .cas-btn {
			min-height: 56px;
			margin-top: 50px;
			padding: 22px 30px;
		}
	}

	/* ---- desktop: the measured 1253 row ---- */
	@media (min-width: 1200px) {
		.cas {
			padding: 120px 20px;
		}

		/* Step 39.2: --tg-grid-max / --tg-grid-gutter put the row on a site grid at desktop; the fallbacks give the original 1253px row. */
		.cas-inner {
			--cas-grid-max: var(--tg-grid-max, 1293px);
			--cas-grid-gutter: var(--tg-grid-gutter, 20px);
			max-width: calc(var(--cas-grid-max) - 2 * var(--cas-grid-gutter));
		}

		.cas-heading {
			font-size: 48px;
			line-height: 57.6px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.cas-media,
		.cas-copy,
		.cas-play-disc,
		.cas-btn-arrow {
			transition: none;
		}

		.cas .cas-btn:hover .cas-btn-arrow,
		.cas-play:hover .cas-play-disc {
			transform: none;
		}

		.cas[data-cas-ready]:not([data-in]) .cas-media,
		.cas[data-cas-ready]:not([data-in]) .cas-copy {
			opacity: 1;
			transform: none;
		}
	}

	@media (forced-colors: active) {
		.cas .cas-btn,
		.cas-play-disc {
			border: 1px solid ButtonText;
		}
	}
</style>

<script>
	/*
	 * Appear-on-scroll, plus the video player when the section has one.
	 * One IntersectionObserver per instance; every listener hangs off one
	 * AbortController, aborted on astro:before-swap. The player is a native
	 * modal <dialog>: Escape and the backdrop close it, focus goes back to the
	 * play button, and the iframe or <video> is created on open and removed on
	 * close so nothing keeps playing. Nothing moves focus or scrolls on load.
	 */
	function initCommittedAboutSplit(root: HTMLElement) {
		if (root.dataset.casReady === "") return;
		const controller = new AbortController();
		const { signal } = controller;
		const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

		root.dataset.casReady = "";

		if (reduced || !("IntersectionObserver" in window)) {
			root.dataset.in = "";
		} else {
			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());
		}

		const play = root.querySelector<HTMLAnchorElement>("[data-cas-play]");
		const dialog = root.querySelector<HTMLDialogElement>("[data-cas-dialog]");
		const frame = dialog?.querySelector<HTMLElement>("[data-cas-frame]");
		const close = dialog?.querySelector<HTMLButtonElement>("[data-cas-close]");

		if (play && dialog && frame && typeof dialog.showModal === "function") {
			const src = dialog.dataset.src ?? "";
			const title = dialog.querySelector(".cas-dialog-title")?.textContent ?? "Video";

			const mount = () => {
				frame.replaceChildren();
				if (dialog.dataset.kind === "file") {
					const video = document.createElement("video");
					video.src = src;
					video.controls = true;
					video.playsInline = true;
					video.autoplay = !reduced;
					video.setAttribute("aria-label", title);
					frame.append(video);
				} else {
					const iframe = document.createElement("iframe");
					const url = new URL(src, location.href);
					if (!reduced) url.searchParams.set("autoplay", "1");
					iframe.src = url.toString();
					iframe.title = title;
					iframe.allow = "autoplay; encrypted-media; picture-in-picture; fullscreen";
					iframe.allowFullscreen = true;
					frame.append(iframe);
				}
			};

			play.addEventListener(
				"click",
				(event) => {
					if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return;
					event.preventDefault();
					mount();
					dialog.showModal();
					close?.focus();
				},
				{ signal },
			);

			close?.addEventListener("click", () => dialog.close(), { signal });

			/* A click on the backdrop lands on the dialog element itself. */
			dialog.addEventListener(
				"click",
				(event) => {
					if (event.target === dialog) dialog.close();
				},
				{ signal },
			);

			dialog.addEventListener(
				"close",
				() => {
					frame.replaceChildren();
					play.focus();
				},
				{ signal },
			);

			signal.addEventListener("abort", () => {
				if (dialog.open) dialog.close();
				frame.replaceChildren();
			});
		}

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

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

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

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

===== END src/components/CommittedAboutSplit.astro =====

===== FILE (required): public/committed-about-split/port-dock.webp =====
[binary asset — download https://astro.baysixmedia.com/committed-about-split/port-dock.webp and save it to public/committed-about-split/port-dock.webp]

===== FILE (required): public/committed-about-split/port-aerial.webp =====
[binary asset — download https://astro.baysixmedia.com/committed-about-split/port-aerial.webp and save it to public/committed-about-split/port-aerial.webp]

===== FILE (required): src/components/ServicePhotoCards.astro =====
---
/**
 * ServicePhotoCards
 *
 * A services section: an orange eyebrow and a heading on the left with an
 * orange pill link on the right, over three tall photo cards. Each card is a
 * single link: a photo with a white panel pinned to its bottom holding an
 * icon tile that pokes out above it, the service name and a pink arrow tile.
 *
 * Desktop hover (and keyboard focus): the photo zooms to 1.1, the icon tile
 * fills orange, the arrow slides through, and a short description opens in
 * the panel, which grows upward and lifts the icon tile with it. On touch
 * screens and below 1200px the description is always shown, as the
 * reference's phone layout does.
 *
 * On scroll the header and the cards rise 40px into place, cards 0.1s apart.
 *
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

export type ServicePhotoIcon = "ship" | "truck" | "train" | "plane" | "warehouse";

export interface ServicePhotoCard {
	title: string;
	description?: string;
	href: string;
	icon?: ServicePhotoIcon;
	/** `src: ""` renders the labelled placeholder. */
	image?: { src?: string; alt?: string };
}

export interface ServicePhotoCardsProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	/** The orange pill beside the heading. `null` drops it. */
	cta?: { label: string; href: string } | null;
	cards?: ServicePhotoCard[];
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends ServicePhotoCardsProps {}

const {
	eyebrow = "Our services",
	heading = "",
	headingLevel = "h2",
	cta = null,
	cards = [],
	background = "#ffffff",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;
/* Each card is one link: it needs a title and an href, or it is dropped rather
   than drawn half-empty. A pill needs a label and an href. No cards, no section:
   the eyebrow and heading alone never render. */
const list = cards.filter((card) => card?.title && card?.href);
const pill = cta?.label && cta?.href ? cta : null;

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

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

/* Measured card: 388 × 430 at desktop; the files are 776 × 860. */
const IMAGE_SPEC = "~0.9:1 · 776 × 860 px";
---

{list.length > 0 && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{list.length > 0 && (
<section class:list={["spc", className]} id={id} style={`--spc-bg: ${background};`} data-spc-root>
	<div class="spc-inner">
		<div class="spc-head" data-spc-rise>
			<div class="spc-titles">
				{
					eyebrow && (
						<p class="spc-eyebrow">
							{eyebrow}
							<span class="spc-rule" aria-hidden="true" />
						</p>
					)
				}
				{heading && <Heading class="spc-heading">{heading}</Heading>}
			</div>
			{
				pill && (
					<a class="spc-btn" href={safeHref(pill.href)}>
						{pill.label}
						<svg class="spc-btn-arrow" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false">
							<path d="M2 10 10 2M3.5 2H10v6.5" />
						</svg>
					</a>
				)
			}
		</div>

		<ul class="spc-grid" role="list">
			{
				list.map((card, index) => {
					const titleId = `${uid}-t${index}`;
					const descId = `${uid}-d${index}`;
					const img = card.image ?? {};
					return (
						<li class="spc-item" data-spc-rise style={`--i: ${index};`}>
							<a
								class="spc-card"
								href={safeHref(card.href)}
								aria-labelledby={titleId}
								aria-describedby={card.description ? descId : undefined}
							>
								<span class="spc-media" aria-hidden={img.src && img.alt ? undefined : "true"}>
									{img.src ? (
										<img src={img.src} alt={img.alt ?? ""} width="776" height="860" loading="lazy" decoding="async" />
									) : (
										<span class="spc-ph">
											<span class="spc-ph-label">Image</span>
											<span class="spc-ph-hint">{card.title}, tall photo</span>
											<span class="spc-ph-spec">{IMAGE_SPEC}</span>
										</span>
									)}
								</span>
								<span class="spc-body">
									<span class="spc-icon" aria-hidden="true">
										<span class="spc-icon-hover" />
										<svg viewBox="0 0 40 40" width="40" height="40" focusable="false">
											{card.icon === "truck" ? (
												<g>
													<rect x="10" y="5" width="20" height="26" rx="3" />
													<path d="M10 13h20M10 22h20M14 9h12" />
													<circle cx="15" cy="26.5" r="1.5" />
													<circle cx="25" cy="26.5" r="1.5" />
													<path d="M13 31v4M27 31v4M7 14v6M33 14v6" />
												</g>
											) : card.icon === "train" ? (
												<g>
													<path d="M12 5h16a3 3 0 0 1 3 3v19a3 3 0 0 1-3 3H12a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3Z" />
													<path d="M9 17h22M15 9h10" />
													<rect x="13" y="11" width="14" height="6" />
													<circle cx="14" cy="24" r="1.5" />
													<circle cx="26" cy="24" r="1.5" />
													<path d="m13 30-4 6M27 30l4 6M11 33h18M4 36h32" />
												</g>
											) : card.icon === "plane" ? (
												<g>
													<path d="M4 22h12l8-12h4l-4 12h8l3-4h3l-1.5 6 1.5 6h-3l-3-4h-8l4 12h-4l-8-12H4" transform="translate(0 -4)" />
												</g>
											) : card.icon === "warehouse" ? (
												<g>
													<path d="M4 15 20 6l16 9v19H4Z" />
													<path d="M10 34V20h20v14M10 25h20M10 30h20" />
												</g>
											) : (
												<g>
													<path d="M4 27h32l-4 7H9Z" />
													<path d="M9 27v-8h22v8" />
													<path d="M13 19v-6h10v6M18 13V7h4M26 23h1M14 23h1M20 23h1" />
													<path d="M3 37c2.5 0 2.5-1.5 5-1.5s2.5 1.5 5 1.5 2.5-1.5 5-1.5 2.5 1.5 5 1.5 2.5-1.5 5-1.5 2.5 1.5 5 1.5 2.5-1.5 4-1.5" />
												</g>
											)}
										</svg>
									</span>
									<span class="spc-text">
										<span class="spc-title-row">
											<span class="spc-title" id={titleId}>{card.title}</span>
											<span class="spc-arrow" aria-hidden="true">
												<svg class="spc-arrow-a" viewBox="0 0 18 16" width="18" height="16" focusable="false">
													<path d="M1.5 8h15M10 1.5 16.5 8 10 14.5" />
												</svg>
												<svg class="spc-arrow-b" viewBox="0 0 18 16" width="18" height="16" focusable="false">
													<path d="M1.5 8h15M10 1.5 16.5 8 10 14.5" />
												</svg>
											</span>
										</span>
										{card.description && (
											<span class="spc-desc-wrap">
												<span class="spc-desc" id={descId}>{card.description}</span>
											</span>
										)}
									</span>
								</span>
							</a>
						</li>
					);
				})
			}
		</ul>
	</div>
</section>
)}

<style>
	.spc {
		--spc-navy: var(--tg-navy, #020e28);
		--spc-orange: var(--tg-orange, #fd5523);
		--spc-muted: var(--tg-muted, #61697d);
		--spc-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
		--spc-body: var(--tg-font-body, "DM Sans", "DM Sans Placeholder", ui-sans-serif, system-ui, sans-serif);
		--spc-ease: cubic-bezier(0.25, 0.1, 0.25, 1);

		box-sizing: border-box;
		padding: 80px 20px;
		background: var(--spc-bg, #fff);
		color: var(--spc-navy);
	}

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

	.spc-inner {
		display: flex;
		flex-direction: column;
		gap: 30px;
		max-width: 1224px;
		margin: 0 auto;
	}

	/* ---- header ---- */
	.spc-head {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 24px;
	}

	.spc-titles {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 16px;
		max-width: 645px;
	}

	.spc .spc-eyebrow {
		display: inline-flex;
		align-items: center;
		gap: 12px;
		margin: 0;
		font: 600 16px/1 var(--spc-display);
		text-transform: uppercase;
		color: var(--spc-orange);
	}

	.spc-rule {
		display: none;
		width: 40px;
		height: 1px;
		background: var(--spc-orange);
	}

	.spc-heading {
		margin: 0;
		font: 600 32px/1.2 var(--spc-display);
		letter-spacing: normal;
		text-transform: capitalize;
		color: var(--spc-navy);
	}

	.spc .spc-btn {
		display: inline-flex;
		flex: none;
		align-items: center;
		gap: 10px;
		min-height: 48px;
		padding: 18px 24px;
		border-radius: 100px;
		background: var(--spc-orange);
		font: 600 14px/12.46px var(--spc-body);
		text-transform: capitalize;
		color: #fff;
		text-decoration: none;
		transition: background-color 0.3s var(--spc-ease);
	}

	.spc-btn-arrow {
		fill: none;
		stroke: currentColor;
		stroke-width: 1.8;
		stroke-linecap: round;
		stroke-linejoin: round;
		transition: transform 0.3s var(--spc-ease);
	}

	.spc .spc-btn:hover {
		background: var(--spc-navy);
	}

	.spc .spc-btn:hover .spc-btn-arrow {
		transform: rotate(45deg);
	}

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

	/* ---- cards ---- */
	.spc .spc-grid {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: 24px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.spc-card {
		position: relative;
		isolation: isolate;
		display: flex;
		flex-direction: column;
		justify-content: flex-end;
		aspect-ratio: 350 / 335;
		padding: 12px;
		border-radius: 20px;
		overflow: hidden;
		color: var(--spc-navy);
		text-decoration: none;
		background: #dfe5e7;
	}

	.spc-card:focus-visible {
		outline: 3px solid var(--spc-orange);
		outline-offset: 4px;
	}

	.spc-media {
		position: absolute;
		inset: 0;
		z-index: -1;
		display: block;
	}

	.spc-media img {
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
		transition: transform 0.6s var(--spc-ease);
	}

	.spc-ph {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		gap: 8px;
		width: 100%;
		height: 100%;
		padding: 16px 16px 45%;
		text-align: center;
		background: repeating-linear-gradient(135deg, #e6e9ee 0 12px, #edf0f3 12px 24px);
		color: rgb(2 14 40 / 0.62);
	}

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

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

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

	.spc-body {
		position: relative;
		display: block;
		margin-top: 60px;
		padding: 40px 12px 16px;
		border-radius: 10px;
		background: #fff;
	}

	.spc-icon {
		position: absolute;
		top: -39px;
		left: 12px;
		display: grid;
		place-items: center;
		width: 60px;
		height: 60px;
		border-radius: 5px;
		background: #fff;
		box-shadow:
			rgb(12 12 13 / 0.05) 0 1px 4px 0,
			rgb(12 12 13 / 0.1) 0 1px 4px 0;
		color: var(--spc-navy);
		transition: color 0.4s var(--spc-ease);
	}

	.spc-icon-hover {
		position: absolute;
		inset: 0;
		border-radius: 5px;
		background: var(--spc-orange);
		opacity: 0;
		transition: opacity 0.4s var(--spc-ease);
	}

	.spc-icon svg {
		position: relative;
		fill: none;
		stroke: currentColor;
		stroke-width: 1.5;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	.spc-text {
		display: block;
	}

	.spc-title-row {
		display: flex;
		justify-content: space-between;
		align-items: flex-end;
		gap: 16px;
	}

	.spc-title {
		max-width: 180px;
		font: 700 20px/24px var(--spc-display);
		color: var(--spc-navy);
	}

	.spc-arrow {
		position: relative;
		flex: none;
		display: block;
		width: 40px;
		height: 40px;
		border-radius: 10px;
		background: #ffe0e0;
		overflow: hidden;
	}

	.spc-arrow svg {
		position: absolute;
		top: 12px;
		left: 11px;
		fill: none;
		stroke: var(--spc-orange);
		stroke-width: 2;
		stroke-linecap: round;
		stroke-linejoin: round;
		transition: transform 0.4s var(--spc-ease);
	}

	.spc-arrow-b {
		transform: translateX(-30px);
	}

	.spc-desc-wrap {
		display: grid;
		grid-template-rows: 1fr;
		margin-top: 12px;
		transition:
			grid-template-rows 0.4s var(--spc-ease),
			margin-top 0.4s var(--spc-ease),
			opacity 0.4s var(--spc-ease);
	}

	.spc-desc {
		display: block;
		min-height: 0;
		overflow: hidden;
		font: 400 15px/20px var(--spc-body);
		color: var(--spc-muted);
	}

	/* ---- hover / focus ---- */
	.spc-card:hover .spc-media img,
	.spc-card:focus-visible .spc-media img {
		transform: scale(1.1);
	}

	.spc-card:hover .spc-icon,
	.spc-card:focus-visible .spc-icon {
		color: #fff;
	}

	.spc-card:hover .spc-icon-hover,
	.spc-card:focus-visible .spc-icon-hover {
		opacity: 1;
	}

	.spc-card:hover .spc-arrow-a,
	.spc-card:focus-visible .spc-arrow-a {
		transform: translateX(30px);
	}

	.spc-card:hover .spc-arrow-b,
	.spc-card:focus-visible .spc-arrow-b {
		transform: translateX(0);
	}

	/* ---- appear ---- */
	[data-spc-rise] {
		transition:
			opacity 0.8s var(--spc-ease) calc(var(--i, 0) * 0.1s),
			transform 0.8s var(--spc-ease) calc(var(--i, 0) * 0.1s);
	}

	.spc[data-spc-ready]:not([data-in]) [data-spc-rise] {
		opacity: 0;
		transform: translateY(40px);
	}

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

		.spc-inner {
			gap: 50px;
		}

		.spc-head {
			flex-direction: row;
			justify-content: space-between;
			align-items: flex-end;
		}

		.spc .spc-eyebrow {
			font-size: 18px;
			line-height: 12.6px;
		}

		.spc-rule {
			display: block;
		}

		.spc-heading {
			font-size: 40px;
		}

		.spc .spc-btn {
			min-height: 56px;
			padding: 22px 30px;
		}

		.spc .spc-grid {
			grid-template-columns: repeat(2, minmax(0, 1fr));
			gap: 30px;
		}

		.spc-card {
			aspect-ratio: 388 / 430;
			padding: 20px;
		}

		.spc-body {
			padding: 40px 30px 20px;
		}

		.spc-icon {
			left: 30px;
		}

		.spc-title {
			max-width: 150px;
			font-size: 22px;
			line-height: 26px;
		}
	}

	/* ---- desktop: three 388 × 430 cards ---- */
	@media (min-width: 1200px) {
		.spc {
			padding: 120px 20px;
		}

		.spc-inner {
			gap: 60px;
		}

		.spc-heading {
			font-size: 48px;
			line-height: 57.6px;
		}

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

		.spc-title {
			max-width: 140px;
		}
	}

	/* Desktop with a real pointer: the description stays folded until the
	   card is hovered or focused, and the panel grows upward to show it. */
	@media (min-width: 1200px) and (hover: hover) and (pointer: fine) {
		.spc-desc-wrap {
			grid-template-rows: 0fr;
			margin-top: 0;
			opacity: 0;
		}

		.spc-card:hover .spc-desc-wrap,
		.spc-card:focus-visible .spc-desc-wrap {
			grid-template-rows: 1fr;
			margin-top: 20px;
			opacity: 1;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		[data-spc-rise],
		.spc-media img,
		.spc-arrow svg,
		.spc-desc-wrap,
		.spc-btn-arrow {
			transition: none;
		}

		.spc-card:hover .spc-media img,
		.spc-card:focus-visible .spc-media img,
		.spc .spc-btn:hover .spc-btn-arrow {
			transform: none;
		}

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

	@media (forced-colors: active) {
		.spc-body,
		.spc .spc-btn {
			border: 1px solid CanvasText;
		}
	}
</style>

<script>
	/*
	 * Appear-on-scroll. One IntersectionObserver per instance, scoped to its
	 * root, torn down on astro:before-swap; sets `data-in` once. The hidden
	 * start pose applies only after the root is marked ready, so no-JS and
	 * reduced motion show everything at rest. Hovers are pure CSS.
	 */
	function initServicePhotoCards(root: HTMLElement) {
		if (root.dataset.spcReady === "") return;
		const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
		if (reduced || !("IntersectionObserver" in window)) {
			root.dataset.in = "";
			return;
		}

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

		const observer = new IntersectionObserver(
			(entries) => {
				if (entries.some((entry) => entry.isIntersecting)) {
					root.dataset.in = "";
					observer.disconnect();
				}
			},
			{ threshold: 0.15 },
		);
		observer.observe(root);
		signal.addEventListener("abort", () => observer.disconnect());

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

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

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

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

===== END src/components/ServicePhotoCards.astro =====

===== FILE (required): public/service-photo-cards/ocean.webp =====
[binary asset — download https://astro.baysixmedia.com/service-photo-cards/ocean.webp and save it to public/service-photo-cards/ocean.webp]

===== FILE (required): public/service-photo-cards/truck.webp =====
[binary asset — download https://astro.baysixmedia.com/service-photo-cards/truck.webp and save it to public/service-photo-cards/truck.webp]

===== FILE (required): public/service-photo-cards/rail.webp =====
[binary asset — download https://astro.baysixmedia.com/service-photo-cards/rail.webp and save it to public/service-photo-cards/rail.webp]

===== FILE (required): src/components/DottedProcessSteps.astro =====
---
/**
 * DottedProcessSteps
 *
 * A light surface section: a centred orange eyebrow between two short lines,
 * a big navy heading, and three steps in a row. Each step is a round pale
 * medallion holding a line icon, hugged on its left by an orange half ring,
 * with a small orange number badge on top; then a title and a short text.
 * Dotted curved arrows join the steps (the first dips, the second arches).
 *
 * Appear on scroll: the title block and each step rise 40px and fade in,
 * staggered; the number badges slide in from -20px; the connectors draw in
 * left to right. The hidden start pose applies only after the script marks
 * the root ready, so no-JS and reduced motion render at rest.
 *
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

export type DottedProcessIcon = "globe-parcel" | "truck-plane" | "globe-truck" | "clipboard" | "warehouse";

export interface DottedProcessStep {
	title: string;
	text?: string;
	/** Built-in line icon. Defaults cycle through the first three. */
	icon?: DottedProcessIcon;
	/** Badge text. Defaults to the 1-based index, zero padded ("01"). */
	number?: string;
}

export interface DottedProcessStepsProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	/** Level of each step title. Defaults to one below the heading. */
	stepHeadingLevel?: "h3" | "h4";
	steps?: DottedProcessStep[];
	/** The dotted arrows between steps (desktop and tablet only). */
	connectors?: boolean;
	/** The section surface. */
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends DottedProcessStepsProps {}

/* No demo content: steps and heading are props. The generic eyebrow stays,
   but it never makes the section render on its own. */
const {
	eyebrow = "How It Works",
	heading = "",
	headingLevel = "h2",
	stepHeadingLevel,
	steps = [],
	connectors = true,
	background,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;
const StepHeading = stepHeadingLevel ?? (headingLevel === "h2" ? "h3" : "h4");
const CYCLE: DottedProcessIcon[] = ["globe-parcel", "truck-plane", "globe-truck"];
/* A step with no title is dropped; with none left, nothing renders. */
const items = (steps ?? []).filter((step) => step?.title).map((step, i) => ({
	...step,
	icon: step.icon ?? CYCLE[i % CYCLE.length],
	number: step.number ?? String(i + 1).padStart(2, "0"),
}));
/* Connectors sit on the column boundaries of a three-up row; with any other
   count the geometry does not hold, so they are drawn only for three. */
const showConnectors = connectors && items.length === 3;
---

{items.length > 0 && (
<>{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section
	class:list={["dps", className]}
	id={id}
	style={background ? `--dps-bg: ${background};` : undefined}
	data-dps-root
>
	<div class="dps-inner">
		<div class="dps-title" data-dps-rise>
			{
				eyebrow && (
					<p class="dps-eyebrow">
						<span class="dps-eyebrow-line" aria-hidden="true" />
						{eyebrow}
						<span class="dps-eyebrow-line" aria-hidden="true" />
					</p>
				)
			}
			{heading && <Heading class="dps-heading">{heading}</Heading>}
		</div>

		<div class="dps-row">
			<ol class="dps-steps" role="list">
				{
					items.map((step, i) => (
						<li class="dps-step" data-dps-rise style={`--dps-i: ${i};`}>
							<div class="dps-medal" aria-hidden="true">
								<span class="dps-ring" />
								<span class="dps-disc">
									{step.icon === "globe-parcel" && (
										<svg viewBox="0 0 60 60" width="60" height="60" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
											<circle cx="24" cy="30" r="18" />
											<ellipse cx="24" cy="30" rx="7.5" ry="18" />
											<path d="M7.5 23h33M7.5 37h24" />
											<rect x="33" y="31" width="19" height="16" rx="1.5" fill="#fff9f9" />
											<path d="M33 36.5h19M42.5 31v5.5" />
											<path d="M36 22h14m-3.5-3.5L50 22l-3.5 3.5" />
										</svg>
									)}
									{step.icon === "truck-plane" && (
										<svg viewBox="0 0 60 60" width="60" height="60" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
											<g transform="translate(38 17) rotate(-28)">
												<path d="M-14 0H11" />
												<path d="M2 0-6-10h-3.5L-6 0l-3.5 10H-6z" />
												<path d="M-11 0-14-5h-2.5l1.5 5-1.5 5h2.5z" />
											</g>
											<rect x="7" y="31" width="29" height="16" rx="1.5" />
											<path d="M16 31v16M26 31v16" />
											<path d="M36 35h9l6 7v5H36z" />
											<path d="M40 39h5" />
											<circle cx="15" cy="49.5" r="3.5" fill="#fff9f9" />
											<circle cx="44" cy="49.5" r="3.5" fill="#fff9f9" />
										</svg>
									)}
									{step.icon === "globe-truck" && (
										<svg viewBox="0 0 60 60" width="60" height="60" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
											<path d="M20 6v9m-3.5-3.5L20 15l3.5-3.5" />
											<circle cx="20" cy="33" r="14" />
											<ellipse cx="20" cy="33" rx="6" ry="14" />
											<path d="M6.5 28h27M6.5 38h20" />
											<rect x="27" y="35" width="15" height="12" rx="1.5" fill="#fff9f9" />
											<path d="M42 38h6l5 5v4H42z" fill="#fff9f9" />
											<circle cx="32" cy="49" r="3" fill="#fff9f9" />
											<circle cx="47" cy="49" r="3" fill="#fff9f9" />
											<g transform="translate(45 16) rotate(-28) scale(0.8)">
												<path d="M-14 0H11" />
												<path d="M2 0-6-10h-3.5L-6 0l-3.5 10H-6z" />
												<path d="M-11 0-14-5h-2.5l1.5 5-1.5 5h2.5z" />
											</g>
										</svg>
									)}
									{step.icon === "clipboard" && (
										<svg viewBox="0 0 60 60" width="60" height="60" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
											<rect x="13" y="10" width="34" height="42" rx="3" />
											<path d="M23 10V7h14v3M23 12h14" />
											<path d="M20 24l3 3 5-6M32 25h9M20 36l3 3 5-6M32 37h9" />
										</svg>
									)}
									{step.icon === "warehouse" && (
										<svg viewBox="0 0 60 60" width="60" height="60" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
											<path d="M6 24 30 10l24 14v26H6z" />
											<path d="M15 50V30h30v20M15 36h30M15 43h30" />
											<rect x="21" y="44" width="8" height="6" />
											<rect x="31" y="44" width="8" height="6" />
										</svg>
									)}
								</span>
								<span class="dps-num" data-dps-num>{step.number}</span>
							</div>
							<div class="dps-copy">
								<StepHeading class="dps-step-title">
									<span class="dps-sr">Step {i + 1}: </span>
									{step.title}
								</StepHeading>
								{step.text && <p class="dps-step-text">{step.text}</p>}
							</div>
						</li>
					))
				}
			</ol>

			{
				showConnectors && (
					<div class="dps-links" aria-hidden="true">
						<svg class="dps-link dps-link--1" viewBox="0 0 220 43" data-dps-link>
							<path class="dps-dots" d="M3 7Q100 52 199 12" />
							<path class="dps-head" d="M213.1 7 202.5 18.5 203.7 10.6 197.5 5.5Z" />
						</svg>
						<svg class="dps-link dps-link--2" viewBox="0 0 220 43" data-dps-link>
							<path class="dps-dots" d="M3 30Q105-10 199 27.6" />
							<path class="dps-head" d="M213 33.2 197.4 34.5 203.7 29.5 202.6 21.5Z" />
						</svg>
					</div>
				)
			}
		</div>
	</div>
</section></>
)}

<style>
	.dps {
		--dps-navy: var(--tg-navy, #020e28);
		--dps-orange: var(--tg-orange, #fd5523);
		--dps-muted: var(--tg-muted, #61697d);
		--dps-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
		--dps-body: var(--tg-font-body, "DM Sans", "DM Sans Placeholder", ui-sans-serif, system-ui, sans-serif);
		--dps-ease: cubic-bezier(0.22, 1, 0.36, 1);

		box-sizing: border-box;
		padding: 80px 0;
		background: var(--dps-bg, var(--tg-surface, #ecf0f1));
		color: var(--dps-navy);
		font-family: var(--dps-body);
		overflow-x: clip;
	}

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

	.dps-inner {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 48px;
		max-width: 1264px;
		margin: 0 auto;
		padding: 0 20px;
	}

	.dps-title {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 16px;
		width: 100%;
		text-align: center;
	}

	.dps .dps-eyebrow {
		display: inline-flex;
		align-items: center;
		gap: 12px;
		margin: 0;
		font: 600 16px/1 var(--dps-display);
		text-transform: uppercase;
		color: var(--dps-orange);
	}

	.dps-eyebrow-line {
		display: block;
		width: 40px;
		height: 1px;
		background: currentColor;
	}

	.dps .dps-heading {
		margin: 0;
		font: 600 32px/1.2 var(--dps-display);
		letter-spacing: normal;
		color: var(--dps-navy);
		overflow-wrap: break-word;
	}

	.dps-row {
		position: relative;
		width: 100%;
	}

	.dps .dps-steps {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: 48px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.dps-step {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 32px;
		min-width: 0;
		text-align: center;
	}

	/* ---- medallion: 150 × 166, the measured box ---- */
	.dps-medal {
		position: relative;
		flex: none;
		width: 150px;
		height: 166px;
	}

	.dps-ring {
		position: absolute;
		top: 16px;
		left: 0;
		width: 75px;
		height: 149px;
		border-radius: 75px 0 0 75px / 74.5px 0 0 74.5px;
		background: var(--dps-orange);
	}

	.dps-disc {
		position: absolute;
		top: 26px;
		left: 10px;
		display: grid;
		place-items: center;
		width: 130px;
		height: 130px;
		border-radius: 50%;
		background: #fff9f9;
		color: var(--dps-navy);
	}

	.dps-disc svg {
		display: block;
		width: 60px;
		height: 60px;
	}

	.dps-num {
		position: absolute;
		top: 0;
		left: 50%;
		display: grid;
		place-items: center;
		width: 40px;
		height: 40px;
		margin-left: -20px;
		border-radius: 50%;
		background: var(--dps-orange);
		font: 500 16px/26px var(--dps-display);
		color: #fff;
	}

	.dps-copy {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 16px;
		max-width: 329px;
	}

	.dps .dps-step-title {
		margin: 0;
		font: 600 24px/1.33 var(--dps-display);
		color: var(--dps-navy);
	}

	.dps .dps-step-text {
		margin: 0;
		font: 400 16px/21.04px var(--dps-body);
		color: var(--dps-muted);
	}

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

	/* ---- connectors: hidden on a phone, where the steps stack ---- */
	.dps-links {
		display: none;
	}

	.dps-link {
		position: absolute;
		top: 83px;
		width: clamp(96px, 15.3vw, 220px);
		height: auto;
		aspect-ratio: 220 / 43;
		overflow: visible;
		transform: translate(-50%, -50%);
		clip-path: inset(0 0 0 0);
	}

	.dps-link--1 {
		left: 32.1%;
	}

	.dps-link--2 {
		left: 67.9%;
	}

	.dps-dots {
		fill: none;
		stroke: var(--dps-navy);
		stroke-width: 2.6;
		stroke-linecap: round;
		stroke-dasharray: 0.1 6.4;
	}

	.dps-head {
		fill: var(--dps-navy);
	}

	/* ---- appear on scroll ---- */
	.dps-title,
	.dps-step {
		transition:
			opacity 0.8s var(--dps-ease),
			transform 0.8s var(--dps-ease);
	}

	.dps-step {
		transition-delay: calc(0.15s + var(--dps-i, 0) * 0.1s);
	}

	.dps-num {
		transition: transform 0.8s var(--dps-ease) calc(0.35s + var(--dps-i, 0) * 0.1s);
	}

	.dps-link {
		transition: clip-path 1s var(--dps-ease) 0.6s;
	}

	.dps-link--2 {
		transition-delay: 0.8s;
	}

	.dps[data-dps-ready]:not([data-in]) .dps-title,
	.dps[data-dps-ready]:not([data-in]) .dps-step {
		opacity: 0;
		transform: translateY(40px);
	}

	.dps[data-dps-ready]:not([data-in]) .dps-num {
		transform: translateX(-20px);
	}

	.dps[data-dps-ready]:not([data-in]) .dps-link {
		clip-path: inset(0 100% 0 0);
	}

	/* ---- tablet and up: the three-up row with connectors ---- */
	@media (min-width: 810px) {
		.dps {
			padding: 100px 0;
		}

		.dps-inner {
			gap: 60px;
		}

		.dps .dps-heading {
			font-size: 40px;
		}

		.dps .dps-steps {
			grid-template-columns: repeat(3, minmax(0, 329px));
			justify-content: space-between;
			column-gap: 24px;
		}

		.dps-links {
			display: block;
		}

		.dps-step {
			gap: 40px;
		}

		.dps-copy {
			gap: 20px;
		}
	}

	@media (min-width: 1200px) {
		.dps {
			padding: 120px 0;
		}

		.dps .dps-eyebrow {
			font-size: 18px;
		}

		.dps .dps-heading {
			font-size: 48px;
			line-height: 57.6px;
		}

		.dps .dps-step-title {
			font-size: 30px;
			line-height: 40px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.dps-title,
		.dps-step,
		.dps-num,
		.dps-link {
			transition: none;
		}

		.dps[data-dps-ready]:not([data-in]) .dps-title,
		.dps[data-dps-ready]:not([data-in]) .dps-step {
			opacity: 1;
			transform: none;
		}

		.dps[data-dps-ready]:not([data-in]) .dps-num {
			transform: none;
		}

		.dps[data-dps-ready]:not([data-in]) .dps-link {
			clip-path: inset(0 0 0 0);
		}
	}

	@media (forced-colors: active) {
		.dps-ring,
		.dps-num {
			forced-color-adjust: none;
		}

		.dps-disc {
			border: 1px solid CanvasText;
		}
	}
</style>

<script>
	/*
	 * Appear-on-scroll. 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.
	 */
	function initDottedProcessSteps(root: HTMLElement) {
		if (root.dataset.dpsReady === "" || root.dataset.in === "") 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.dpsReady = "";

		const observer = new IntersectionObserver(
			(entries) => {
				if (entries.some((entry) => entry.isIntersecting)) {
					root.dataset.in = "";
					observer.disconnect();
				}
			},
			{ threshold: 0.15 },
		);
		observer.observe(root);
		signal.addEventListener("abort", () => observer.disconnect());

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

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

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

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

===== END src/components/DottedProcessSteps.astro =====

===== FILE (required): src/components/AccordionSuccessShowcase.astro =====
---
/**
 * AccordionSuccessShowcase
 *
 * A dark navy section with a faint tyre-track texture. Left: an orange
 * eyebrow, a big white heading and an accordion list between two hairlines.
 * The open item is a raised translucent card with a curved orange left border
 * and an orange round arrow; closed items show their title, their line of
 * text and a dark round arrow. Right: the open item's photo with a white info
 * card overlapping its lower-left corner.
 *
 * A real accordion: each title is a <button aria-expanded aria-controls> for
 * that item's showcase panel (photo + info card). One open at a time, the
 * first open by default; clicking the open item keeps it open, so the photo
 * column is never empty. The button's hit area is stretched over the whole
 * item, so clicking anywhere on it works, as on the reference.
 *
 * Appear on scroll: the text side slides in from -40px, the photo side from
 * +40px. Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

export interface SuccessCard {
	title: string;
	text?: string;
}

export interface SuccessItem {
	title: string;
	text?: string;
	/** This item's photo. Falls back to the section `image`. */
	image?: { src?: string; alt?: string };
	/** The white info card over the photo. `null` drops it. */
	card?: SuccessCard | null;
}

export interface AccordionSuccessShowcaseProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	/** Level of each item title. Defaults to one below the heading. */
	itemHeadingLevel?: "h3" | "h4";
	items?: SuccessItem[];
	/** Shared photo for items without their own. `src: ""` = placeholder. */
	image?: { src?: string; alt?: string };
	/** 1-based index of the item open on load. */
	initialOpen?: number;
	/** The faint tyre-track texture. */
	texture?: boolean;
	/** The section surface. */
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends AccordionSuccessShowcaseProps {}

/* Items, results, heading and photo are the caller's: none are defaulted.
   No items → no section. */
const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	itemHeadingLevel,
	items = [],
	image: imageProp,
	initialOpen = 1,
	texture = true,
	background,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const uid = id ?? `ass-${Math.random().toString(36).slice(2, 8)}`;
const Heading = headingLevel;
const ItemHeading = itemHeadingLevel ?? (headingLevel === "h2" ? "h3" : "h4");

/* No default photo: an item with no image of its own and no shared `image`
   gets the neutral placeholder. */
const shared = { src: imageProp?.src ?? "", alt: imageProp?.alt ?? "" };

const filled = (v: unknown): v is string => typeof v === "string" && v.trim() !== "";
/* An item needs a title (it is the button); a card needs a title. */
const list = (items ?? []).filter((item) => item && filled(item.title));

const openIndex = Math.min(Math.max(1, Math.round(initialOpen)), Math.max(list.length, 1)) - 1;

const rows = list.map((item, i) => {
	const img = item.image ? { ...shared, ...item.image } : shared;
	if (item.image?.src !== undefined && item.image.alt === undefined) img.alt = "";
	return {
		...item,
		img,
		card: item.card && filled(item.card.title) ? item.card : null,
		open: i === openIndex,
		btnId: `${uid}-btn-${i}`,
		panelId: `${uid}-panel-${i}`,
	};
});

/* Measured photo: 601 × 752 at desktop (~0.8:1); the file is 1100 × 1146. */
const IMAGE_SPEC = "~4:5 · 1100 × 1146 px";
---

{rows.length > 0 && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{rows.length > 0 && (
<section
	class:list={["ass", className]}
	id={id}
	style={`--ass-rows: ${rows.length * 2 + 2};${background ? ` --ass-bg: ${background};` : ""}`}
	data-ass-root
>
	{
		texture && (
			<svg class="ass-texture" viewBox="0 0 700 760" preserveAspectRatio="xMinYMin slice" aria-hidden="true" focusable="false">
				<defs>
					<pattern id={`${uid}-tread`} width="64" height="30" patternUnits="userSpaceOnUse">
						<path d="M2 2 32 18 62 2v9L32 27 2 11z" fill="#fff" />
					</pattern>
				</defs>
				<g transform="rotate(24 350 380)">
					<rect x="90" y="-260" width="150" height="1300" fill={`url(#${uid}-tread)`} />
					<rect x="300" y="-260" width="150" height="1300" fill={`url(#${uid}-tread)`} />
				</g>
			</svg>
		)
	}
	<div class="ass-inner">
		{(eyebrow || heading) && (
		<div class="ass-head" data-ass-left>
			{
				eyebrow && (
					<p class="ass-eyebrow">
						{eyebrow}
						<span class="ass-eyebrow-line" aria-hidden="true" />
					</p>
				)
			}
			{heading && <Heading class="ass-heading">{heading}</Heading>}
		</div>
		)}

		{
			rows.map((row, i) => (
				<>
					<div
						class:list={["ass-item", { "ass-item--first": i === 0, "ass-item--last": i === rows.length - 1 }]}
						data-ass-item
						data-ass-left
						data-open={row.open ? "" : undefined}
						style={`--ass-row: ${i * 2 + 2};`}
					>
						<ItemHeading class="ass-item-title">
							<button
								type="button"
								class="ass-btn"
								id={row.btnId}
								aria-expanded={row.open ? "true" : "false"}
								aria-controls={row.panelId}
								data-ass-btn
							>
								{row.title}
							</button>
						</ItemHeading>
						{row.text && <p class="ass-item-text">{row.text}</p>}
						<span class="ass-arrow" aria-hidden="true">
							<svg viewBox="0 0 10 10" width="10" height="10">
								<path d="M1.5 8.5 8.5 1.5M3 1.5h5.5V7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
							</svg>
						</span>
					</div>
					<div
						class="ass-panel"
						id={row.panelId}
						role="region"
						aria-labelledby={row.btnId}
						data-ass-panel
						data-ass-right
						data-open={row.open ? "" : undefined}
						style={`--ass-row: ${i * 2 + 3};`}
					>
						<div class="ass-photo">
							{row.img.src ? (
								<img
									src={row.img.src}
									alt={row.img.alt}
									width="1100"
									height="1146"
									loading="lazy"
									decoding="async"
								/>
							) : (
								<div class="ass-ph" role="img" aria-label="Image placeholder">
									<div class="ass-ph-text" aria-hidden="true">
										<span class="ass-ph-label">Image</span>
										<span class="ass-ph-spec">{IMAGE_SPEC}</span>
									</div>
								</div>
							)}
						</div>
						{row.card && (
							<div class="ass-card">
								<svg class="ass-card-icon" viewBox="0 0 40 40" width="40" height="40" aria-hidden="true" focusable="false">
									<g fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
										<path d="M11 14V4m-3.5 3.5L11 4l3.5 3.5M20 14V4m-3.5 3.5L20 4l3.5 3.5M29 14V4m-3.5 3.5L29 4l3.5 3.5" />
										<path d="M5 19h27a4 4 0 0 1 0 8H8a4 4 0 0 0 0 8h27" />
									</g>
								</svg>
								<p class="ass-card-title">{row.card.title}</p>
								{row.card.text && <p class="ass-card-text">{row.card.text}</p>}
							</div>
						)}
					</div>
				</>
			))
		}
	</div>
</section>
)}

<style>
	.ass {
		--ass-navy: var(--tg-navy, #020e28);
		--ass-orange: var(--tg-orange, #fd5523);
		--ass-muted: var(--tg-muted, #61697d);
		--ass-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
		--ass-body: var(--tg-font-body, "DM Sans", "DM Sans Placeholder", ui-sans-serif, system-ui, sans-serif);
		--ass-ease: cubic-bezier(0.22, 1, 0.36, 1);

		position: relative;
		box-sizing: border-box;
		padding: 80px 0;
		background: var(--ass-bg, var(--ass-navy));
		color: #fff;
		font-family: var(--ass-body);
		overflow: clip;
		isolation: isolate;
	}

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

	/* Tyre tracks: two chevron-tread bands, very faint, fading out. */
	.ass-texture {
		position: absolute;
		top: 28%;
		left: 0;
		z-index: -1;
		width: min(48%, 700px);
		height: 72%;
		opacity: 0.045;
		pointer-events: none;
		-webkit-mask-image: radial-gradient(ellipse 70% 70% at 30% 55%, #000 30%, transparent 75%);
		mask-image: radial-gradient(ellipse 70% 70% at 30% 55%, #000 30%, transparent 75%);
	}

	.ass-inner {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		max-width: 1293px;
		margin: 0 auto;
		padding: 0 20px;
	}

	.ass-head {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 20px;
		padding-bottom: 32px;
	}

	.ass .ass-eyebrow {
		display: inline-flex;
		align-items: center;
		gap: 12px;
		margin: 0;
		font: 600 16px/1 var(--ass-display);
		text-transform: uppercase;
		color: var(--ass-orange);
	}

	.ass-eyebrow-line {
		display: none;
		width: 40px;
		height: 1px;
		background: currentColor;
	}

	.ass .ass-heading {
		margin: 0;
		font: 600 32px/1.2 var(--ass-display);
		color: #fff;
		overflow-wrap: break-word;
	}

	/* ---- accordion items ---- */
	.ass-item {
		position: relative;
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		row-gap: 16px;
		margin-top: 20px;
		padding: 16px 0 16px 18px;
		border-left: 3px solid rgb(255 255 255 / 0.15);
		border-radius: 20px;
		cursor: pointer;
		transition:
			background-color 0.4s var(--ass-ease),
			border-color 0.4s var(--ass-ease),
			padding 0.4s var(--ass-ease);
	}

	.ass-item--first {
		margin-top: 0;
	}

	.ass-item[data-open] {
		border-left-color: var(--ass-orange);
		background: rgb(255 255 255 / 0.07);
	}

	.ass .ass-item-title {
		margin: 0;
		font: 600 22px/34px var(--ass-display);
		color: #fff;
	}

	.ass-btn {
		display: block;
		width: 100%;
		min-height: 44px;
		margin: 0;
		padding: 5px 0;
		border: 0;
		background: none;
		color: inherit;
		font: inherit;
		text-align: left;
		cursor: pointer;
	}

	/* Stretch the button's hit area over the whole item. */
	.ass-btn::after {
		content: "";
		position: absolute;
		inset: 0;
		border-radius: 20px;
	}

	.ass-btn:focus-visible {
		outline: none;
	}

	.ass-btn:focus-visible::after {
		outline: 2px solid var(--ass-orange);
		outline-offset: 3px;
	}

	.ass .ass-item-text {
		margin: 0;
		padding-right: 16px;
		font: 400 16px/21.04px var(--ass-body);
		color: #ecf0f1;
	}

	.ass-arrow {
		display: none;
		place-items: center;
		width: 42px;
		height: 42px;
		border-radius: 50px;
		background: rgb(255 255 255 / 0.07);
		color: #fff;
		transition:
			background-color 0.4s var(--ass-ease),
			transform 0.4s var(--ass-ease);
	}

	.ass-item[data-open] .ass-arrow {
		background: var(--ass-orange);
	}

	.ass-item:hover .ass-arrow {
		transform: rotate(45deg);
	}

	/* ---- showcase panels ---- */
	.ass-panel {
		position: relative;
		display: none;
		margin-top: 20px;
	}

	.ass-panel[data-open] {
		display: block;
	}

	.ass-photo {
		position: relative;
		aspect-ratio: 16 / 10;
		border-radius: 12px;
		overflow: hidden;
		background: #0b1a36;
	}

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

	.ass-card {
		position: relative;
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 16px;
		width: auto;
		max-width: 330px;
		margin: -48px 16px 0;
		padding: 24px;
		border-radius: 20px;
		background: #fff;
		color: var(--ass-navy);
	}

	.ass-card-icon {
		display: block;
		width: 40px;
		height: 40px;
		color: var(--ass-orange);
	}

	.ass .ass-card-title {
		margin: 0;
		font: 600 22px/34px var(--ass-display);
		color: var(--ass-navy);
	}

	.ass .ass-card-text {
		margin: 0;
		font: 400 16px/21.04px var(--ass-body);
		color: var(--ass-muted);
	}

	.ass-ph {
		position: absolute;
		inset: 0;
		display: grid;
		place-items: center;
		padding: 16px;
		text-align: center;
		background: repeating-linear-gradient(135deg, #0d1c3a 0 12px, #12234438 12px 24px), #0b1a36;
		color: rgb(255 255 255 / 0.7);
	}

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

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

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

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

	/* ---- appear on scroll ---- */
	[data-ass-left],
	[data-ass-right] {
		transition:
			opacity 0.9s var(--ass-ease),
			transform 0.9s var(--ass-ease);
	}

	.ass-item[data-ass-left] {
		transition:
			opacity 0.9s var(--ass-ease),
			transform 0.9s var(--ass-ease),
			background-color 0.4s var(--ass-ease),
			border-color 0.4s var(--ass-ease),
			padding 0.4s var(--ass-ease);
	}

	.ass[data-ass-ready]:not([data-in]) [data-ass-left] {
		opacity: 0;
		transform: translateX(-40px);
	}

	.ass[data-ass-ready]:not([data-in]) [data-ass-right] {
		opacity: 0;
		transform: translateX(40px);
	}

	/* ---- tablet and up: text left, photo right ---- */
	@media (min-width: 810px) {
		.ass {
			padding: 100px 0;
		}

		.ass-inner {
			grid-template-columns: minmax(0, 584px) minmax(0, 601px);
			grid-template-rows: repeat(var(--ass-rows), auto) 1fr;
			justify-content: space-between;
			column-gap: 40px;
		}

		.ass-head {
			grid-column: 1;
			grid-row: 1;
			gap: 24px;
			padding-bottom: 60px;
			border-bottom: 1px solid var(--ass-muted);
			margin-bottom: 30px;
		}

		.ass-eyebrow-line {
			display: block;
		}

		.ass .ass-heading {
			font-size: 40px;
		}

		.ass-item {
			grid-column: 1;
			grid-row: var(--ass-row);
			grid-template-columns: minmax(0, 1fr) auto;
			align-items: center;
			row-gap: 24px;
			min-height: 100px;
			margin-top: 30px;
			padding: 0;
			border-left: 0 solid transparent;
			background: transparent;
		}

		.ass-item--first {
			margin-top: 0;
		}

		.ass-item--last {
			padding-bottom: 0;
		}

		.ass-item--last::before {
			content: "";
			position: absolute;
			right: 0;
			bottom: -30px;
			left: 0;
			height: 1px;
			background: var(--ass-muted);
		}

		.ass-item[data-open] {
			padding: 30px;
			border-left-width: 3px;
			background: rgb(255 255 255 / 0.07);
		}

		.ass .ass-item-title,
		.ass .ass-item-text {
			grid-column: 1;
			padding-right: 16px;
		}

		.ass .ass-item-title {
			font-size: 24px;
		}

		.ass-btn {
			padding: 0;
			min-height: 34px;
		}

		.ass-arrow {
			display: grid;
			grid-column: 2;
			grid-row: 1 / span 2;
		}

		/* All panels share the photo column; the open one shows. */
		.ass-panel,
		.ass-panel[data-open] {
			display: block;
			grid-column: 2;
			grid-row: 1 / -1;
			margin-top: 0;
		}

		.ass-panel {
			visibility: hidden;
			opacity: 0;
			transition:
				opacity 0.5s var(--ass-ease),
				transform 0.9s var(--ass-ease),
				visibility 0s linear 0.5s;
		}

		.ass-panel[data-open] {
			visibility: visible;
			opacity: 1;
			transition:
				opacity 0.5s var(--ass-ease),
				transform 0.9s var(--ass-ease),
				visibility 0s;
		}

		.ass-photo {
			height: 100%;
			min-height: 560px;
			aspect-ratio: auto;
		}

		.ass-card {
			position: absolute;
			bottom: 40px;
			left: 40px;
			width: calc(100% - 80px);
			max-width: 330px;
			margin: 0;
			padding: 30px;
			gap: 30px;
		}

		.ass .ass-card-title {
			margin-bottom: -3px;
			font-size: 24px;
		}
	}

	@media (min-width: 1200px) {
		.ass {
			padding: 120px 0;
		}

		.ass .ass-eyebrow {
			font-size: 18px;
		}

		.ass .ass-heading {
			font-size: 48px;
			line-height: 57.6px;
		}

		.ass-photo {
			min-height: 752px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.ass-item,
		.ass-arrow,
		[data-ass-left],
		[data-ass-right],
		.ass-item[data-ass-left] {
			transition: none;
		}

		.ass-item:hover .ass-arrow {
			transform: none;
		}

		.ass[data-ass-ready]:not([data-in]) [data-ass-left],
		.ass[data-ass-ready]:not([data-in]) [data-ass-right] {
			opacity: 1;
			transform: none;
		}
	}

	@media (prefers-reduced-motion: reduce) and (min-width: 810px) {
		.ass-panel,
		.ass-panel[data-open] {
			transition: none;
		}
	}

	@media (forced-colors: active) {
		.ass-item[data-open] {
			border: 1px solid CanvasText;
			border-left: 3px solid Highlight;
		}

		.ass-btn:focus-visible::after {
			outline-color: Highlight;
		}
	}
</style>

<script>
	/*
	 * Accordion + appear-on-scroll. One controller per instance, scoped to
	 * its root, aborted on astro:before-swap. Clicking a closed item opens it
	 * and closes the others; clicking the open one leaves it open. Arrow
	 * Up/Down, Home and End move focus between item buttons.
	 */
	function initAccordionSuccessShowcase(root: HTMLElement) {
		if (root.dataset.assReady === "" || root.dataset.assBound === "") return;
		const buttons = Array.from(root.querySelectorAll<HTMLButtonElement>("[data-ass-btn]"));
		if (!buttons.length) return;

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

		const open = (index: number) => {
			buttons.forEach((btn, i) => {
				const isOpen = i === index;
				btn.setAttribute("aria-expanded", String(isOpen));
				const item = btn.closest<HTMLElement>("[data-ass-item]");
				const panel = root.querySelector<HTMLElement>(`#${CSS.escape(btn.getAttribute("aria-controls") ?? "")}`);
				for (const el of [item, panel]) {
					if (!el) continue;
					if (isOpen) el.dataset.open = "";
					else delete el.dataset.open;
				}
			});
		};

		buttons.forEach((btn, i) => {
			btn.addEventListener(
				"click",
				() => {
					if (btn.getAttribute("aria-expanded") !== "true") open(i);
				},
				{ signal },
			);
			btn.addEventListener(
				"keydown",
				(event) => {
					let next = -1;
					if (event.key === "ArrowDown") next = (i + 1) % buttons.length;
					else if (event.key === "ArrowUp") next = (i - 1 + buttons.length) % buttons.length;
					else if (event.key === "Home") next = 0;
					else if (event.key === "End") next = buttons.length - 1;
					if (next < 0) return;
					event.preventDefault();
					buttons[next].focus();
				},
				{ signal },
			);
		});

		const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
		if (reduced || !("IntersectionObserver" in window)) {
			root.dataset.in = "";
		} else if (root.dataset.in !== "") {
			root.dataset.assReady = "";
			const observer = new IntersectionObserver(
				(entries) => {
					if (entries.some((entry) => entry.isIntersecting)) {
						root.dataset.in = "";
						observer.disconnect();
					}
				},
				{ threshold: 0.15 },
			);
			observer.observe(root);
			signal.addEventListener("abort", () => observer.disconnect());
		}

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

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

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

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

===== END src/components/AccordionSuccessShowcase.astro =====

===== FILE (required): public/accordion-success-showcase/cranes-dusk.webp =====
[binary asset — download https://astro.baysixmedia.com/accordion-success-showcase/cranes-dusk.webp and save it to public/accordion-success-showcase/cranes-dusk.webp]

===== FILE (required): src/components/TeamHoverCards.astro =====
---
/**
 * TeamHoverCards
 *
 * A light surface section: a centred orange eyebrow between two short lines,
 * a big navy heading, and three portrait cards. Each card is a pale pink
 * frame around a rounded photo, with a white name / role strip over the
 * photo's bottom edge and a navy round "share" icon on its right.
 *
 * Hover (pointer devices, 810px and up), as measured: the photo scales to
 * 1.05; the navy share icon fades out and a white pill of social links fades
 * in, rising from the icon, capped by an orange copy of the icon. The same
 * reveal runs on :focus-within, so the social links are reachable by
 * keyboard. On a phone, and on touch screens that cannot hover, the links are
 * a plain row under the name instead, as on the reference phone layout.
 *
 * Appear on scroll: the title block and the cards rise 40px, staggered 0.1s.
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

export type TeamSocialNetwork = "instagram" | "x" | "facebook" | "linkedin";

export interface TeamSocialLink {
	network: TeamSocialNetwork;
	href: string;
}

export interface TeamMember {
	name: string;
	role?: string;
	/** Profile page. The name (and the photo) link to it when set. */
	href?: string;
	/** `src: ""` renders the labelled placeholder. */
	image?: { src?: string; alt?: string };
	/** `[]` drops the share icon and the social links. */
	socials?: TeamSocialLink[];
}

export interface TeamHoverCardsProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	/** Level of each name. Defaults to one below the heading. */
	memberHeadingLevel?: "h3" | "h4";
	members?: TeamMember[];
	/** The section surface. */
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends TeamHoverCardsProps {}

/*
 * No built-in people: the section draws only the members you pass, and a
 * member without `socials` gets none. The component never invents a
 * person or a profile; the catalog demo passes its own.
 */
const {
	eyebrow = "Our People",
	heading = "",
	headingLevel = "h2",
	memberHeadingLevel,
	members = [],
	background,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;
const NameHeading = memberHeadingLevel ?? (headingLevel === "h2" ? "h3" : "h4");

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

const LABELS: Record<TeamSocialNetwork, string> = {
	instagram: "Instagram",
	x: "X",
	facebook: "Facebook",
	linkedin: "LinkedIn",
};

const cards = members.map((m) => {
	const image = { src: "", alt: "", ...(m.image ?? {}) };
	return {
		...m,
		image,
		href: m.href ? safeHref(m.href) : undefined,
		socials: (m.socials ?? []).map((s) => ({ ...s, href: safeHref(s.href) })),
	};
});

/* Measured slot: 352 × 414 at desktop (~0.85:1); the files are 704 × 828. */
const IMAGE_SPEC = "~0.85:1 · 704 × 828 px";
---

{cards.length > 0 && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{cards.length > 0 && (
<section
	class:list={["thc", className]}
	id={id}
	style={background ? `--thc-bg: ${background};` : undefined}
	data-thc-root
>
	<div class="thc-inner">
		{(eyebrow || heading) && (
		<div class="thc-title" data-thc-rise>
			{
				eyebrow && (
					<p class="thc-eyebrow">
						<span class="thc-eyebrow-line" aria-hidden="true" />
						{eyebrow}
						<span class="thc-eyebrow-line" aria-hidden="true" />
					</p>
				)
			}
			{heading && <Heading class="thc-heading">{heading}</Heading>}
		</div>
		)}

		<ul class="thc-grid" role="list">
			{
				cards.map((card, i) => (
					<li class="thc-card" data-thc-rise style={`--thc-i: ${i};`}>
						<div class="thc-media">
							{card.image.src ? (
								card.href ? (
									<a class="thc-media-link" href={card.href} tabindex="-1" aria-hidden="true">
										<img src={card.image.src} alt="" width="704" height="828" loading="lazy" decoding="async" />
									</a>
								) : (
									<img src={card.image.src} alt={card.image.alt} width="704" height="828" loading="lazy" decoding="async" />
								)
							) : (
								<div class="thc-ph" role="img" aria-label={`Image placeholder: portrait of ${card.name}`}>
									<div class="thc-ph-text" aria-hidden="true">
										<span class="thc-ph-label">Image</span>
										<span class="thc-ph-hint">Portrait, head and shoulders</span>
										<span class="thc-ph-spec">{IMAGE_SPEC}</span>
									</div>
								</div>
							)}
						</div>
						<div class="thc-strip">
							<div class="thc-info">
								<NameHeading class="thc-name">
									{card.href ? <a href={card.href}>{card.name}</a> : card.name}
								</NameHeading>
								{card.role && <p class="thc-role">{card.role}</p>}
							</div>
							{card.socials.length > 0 && (
								<div class="thc-share">
									<span class="thc-share-icon" aria-hidden="true">
										<svg viewBox="0 0 20 16" width="20" height="16">
											<path d="M8.2 10.4a3.4 3.4 0 0 0 4.8 0l3-3a3.4 3.4 0 0 0-4.8-4.8L9.8 4M11.8 5.6a3.4 3.4 0 0 0-4.8 0l-3 3a3.4 3.4 0 0 0 4.8 4.8l1.4-1.4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
										</svg>
									</span>
									<div class="thc-pill">
										<ul class="thc-social" role="list" aria-label={`${card.name} on social media`}>
											{card.socials.map((s) => (
												<li>
													<a class="thc-social-link" href={s.href} rel="noopener noreferrer" target="_blank">
														<span class="thc-sr">{LABELS[s.network]} (opens in a new tab)</span>
														{s.network === "instagram" && (
															<svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true" focusable="false">
																<rect x="1.2" y="1.2" width="11.6" height="11.6" rx="3.4" fill="none" stroke="currentColor" stroke-width="1.4" />
																<circle cx="7" cy="7" r="2.7" fill="none" stroke="currentColor" stroke-width="1.4" />
																<circle cx="10.4" cy="3.6" r="0.9" fill="currentColor" />
															</svg>
														)}
														{s.network === "x" && (
															<svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true" focusable="false">
																<path d="M1.5 1.5h3.3l7.7 11H9.2zM12.3 1.5 8 6.4M1.8 12.5 6 7.7" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round" />
															</svg>
														)}
														{s.network === "facebook" && (
															<svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true" focusable="false">
																<path d="M8 13.5V7.6h2l.3-2.3H8V3.9c0-.7.2-1.1 1.2-1.1h1.2V.7A15 15 0 0 0 8.6.6C6.9.6 5.7 1.6 5.7 3.5v1.8h-2v2.3h2v5.9z" fill="currentColor" />
															</svg>
														)}
														{s.network === "linkedin" && (
															<svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true" focusable="false">
																<path d="M1.3 4.8h2.6v8H1.3zM2.6.9a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3M5.6 4.8h2.5v1.1c.4-.7 1.3-1.3 2.6-1.3 2.7 0 3.1 1.7 3.1 4v4.2h-2.6V9.1c0-.9 0-2-1.3-2s-1.6 1-1.6 2v3.7H5.6z" fill="currentColor" />
															</svg>
														)}
													</a>
												</li>
											))}
										</ul>
										<span class="thc-pill-icon" aria-hidden="true">
											<svg viewBox="0 0 20 16" width="20" height="16">
												<path d="M8.2 10.4a3.4 3.4 0 0 0 4.8 0l3-3a3.4 3.4 0 0 0-4.8-4.8L9.8 4M11.8 5.6a3.4 3.4 0 0 0-4.8 0l-3 3a3.4 3.4 0 0 0 4.8 4.8l1.4-1.4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
											</svg>
										</span>
									</div>
								</div>
							)}
						</div>
					</li>
				))
			}
		</ul>
	</div>
</section>
)}

<style>
	.thc {
		--thc-navy: var(--tg-navy, #020e28);
		--thc-band: #001f3f;
		--thc-orange: var(--tg-orange, #fd5523);
		--thc-muted: var(--tg-muted, #61697d);
		--thc-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
		--thc-body: var(--tg-font-body, "DM Sans", "DM Sans Placeholder", ui-sans-serif, system-ui, sans-serif);
		--thc-ease: cubic-bezier(0.22, 1, 0.36, 1);

		box-sizing: border-box;
		padding: 80px 0;
		background: var(--thc-bg, var(--tg-surface, #ecf0f1));
		color: var(--thc-navy);
		font-family: var(--thc-body);
		overflow-x: clip;
	}

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

	.thc-inner {
		display: flex;
		flex-direction: column;
		gap: 40px;
		max-width: 1264px;
		margin: 0 auto;
		padding: 0 20px;
	}

	.thc-title {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 16px;
	}

	.thc .thc-eyebrow {
		display: inline-flex;
		align-items: center;
		gap: 12px;
		margin: 0;
		font: 600 16px/1 var(--thc-display);
		text-transform: uppercase;
		color: var(--thc-orange);
	}

	/* Left aligned on a phone, as measured, so the lines drop there. */
	.thc-eyebrow-line {
		display: none;
		width: 40px;
		height: 1px;
		background: currentColor;
	}

	.thc .thc-heading {
		margin: 0;
		font: 600 32px/1.2 var(--thc-display);
		color: var(--thc-navy);
		overflow-wrap: break-word;
	}

	.thc .thc-grid {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: 24px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	/* ---- card: phone / no-hover layout first ---- */
	.thc-card {
		position: relative;
		display: flex;
		flex-direction: column;
		min-width: 0;
		border-radius: 12px;
		background: #fff9f9;
		overflow: hidden;
	}

	.thc-media {
		position: relative;
		aspect-ratio: 7 / 5;
		overflow: hidden;
	}

	.thc-media-link,
	.thc-media img {
		display: block;
		width: 100%;
		height: 100%;
	}

	.thc-media img {
		object-fit: cover;
		object-position: 50% 25%;
		transition: transform 0.4s var(--thc-ease);
	}

	.thc-strip {
		display: flex;
		flex-direction: column;
		gap: 16px;
		padding: 16px;
	}

	.thc-info {
		display: flex;
		flex-direction: column;
		gap: 6px;
		min-width: 0;
	}

	.thc .thc-name {
		margin: 0;
		font: 600 20px/1.36 var(--thc-display);
		color: var(--thc-navy);
	}

	.thc-name a {
		color: inherit;
		text-decoration: none;
		transition: color 0.3s var(--thc-ease);
	}

	.thc-name a:hover {
		color: var(--thc-orange);
	}

	.thc-name a:focus-visible {
		outline: 2px solid var(--thc-orange);
		outline-offset: 3px;
		border-radius: 2px;
	}

	.thc .thc-role {
		margin: 0;
		font: 400 16px/21.04px var(--thc-body);
		color: var(--thc-orange);
	}

	.thc-share-icon,
	.thc-pill-icon {
		display: none;
	}

	.thc .thc-social {
		display: flex;
		flex-wrap: wrap;
		gap: 8px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.thc-social-link {
		display: grid;
		place-items: center;
		width: 44px;
		height: 44px;
		border-radius: 50%;
		background: #fff;
		color: var(--thc-orange);
		transition:
			background-color 0.3s var(--thc-ease),
			color 0.3s var(--thc-ease);
	}

	.thc-social-link svg {
		display: block;
		width: 14px;
		height: 14px;
	}

	.thc-social-link:hover {
		background: var(--thc-orange);
		color: #fff;
	}

	.thc-social-link:focus-visible {
		outline: 2px solid var(--thc-navy);
		outline-offset: 2px;
	}

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

	.thc-ph {
		display: grid;
		place-items: center;
		width: 100%;
		height: 100%;
		padding: 16px;
		text-align: center;
		background: repeating-linear-gradient(135deg, #e6e9ee 0 12px, #edf0f3 12px 24px);
		color: rgb(2 14 40 / 0.62);
	}

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

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

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

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

	.thc-card:hover .thc-media img {
		transform: scale(1.05);
	}

	/* ---- appear on scroll ---- */
	.thc-title,
	.thc-card {
		transition:
			opacity 0.8s var(--thc-ease),
			transform 0.8s var(--thc-ease);
	}

	.thc-card {
		transition-delay: calc(0.1s + var(--thc-i, 0) * 0.1s);
	}

	.thc[data-thc-ready]:not([data-in]) .thc-title,
	.thc[data-thc-ready]:not([data-in]) .thc-card {
		opacity: 0;
		transform: translateY(40px);
	}

	/* ---- tablet and up: centred title, three-up grid ---- */
	@media (min-width: 810px) {
		.thc {
			padding: 100px 0;
		}

		.thc-inner {
			gap: 60px;
		}

		.thc-title {
			align-items: center;
			text-align: center;
		}

		.thc-eyebrow-line {
			display: block;
		}

		.thc .thc-heading {
			font-size: 40px;
		}

		.thc .thc-grid {
			display: flex;
			flex-wrap: wrap;
			justify-content: center;
		}

		.thc .thc-grid > .thc-card {
			flex: 1 1 220px;
			max-width: 392px;
		}

		.thc-card {
			padding: 20px;
			border-radius: 20px;
			overflow: visible;
			cursor: pointer;
		}

		.thc-media {
			aspect-ratio: 352 / 414;
			border-radius: 15px;
		}
	}

	/* ---- the measured overlay strip and hover reveal: pointer devices ---- */
	@media (min-width: 810px) and (hover: hover) {
		.thc-strip {
			position: absolute;
			right: 20px;
			bottom: 20px;
			left: 20px;
			z-index: 2;
			flex-direction: row;
			justify-content: space-between;
			align-items: center;
			gap: 10px;
			min-height: 85px;
			padding: 13px 20px;
			border-radius: 15px;
			background: #fff;
		}

		.thc-info {
			gap: 8px;
		}

		.thc .thc-name {
			font-size: 22px;
			line-height: 29.92px;
		}

		.thc-share {
			position: relative;
			flex: none;
			width: 56px;
			height: 56px;
		}

		.thc-share-icon,
		.thc-pill-icon {
			display: grid;
			place-items: center;
			width: 56px;
			height: 56px;
			border-radius: 100px;
			color: #fff;
		}

		.thc-share-icon {
			background: var(--thc-band);
			transition: opacity 0.4s var(--thc-ease);
		}

		.thc-pill-icon {
			flex: none;
			margin: 0 -6px;
			background: var(--thc-orange);
		}

		/* The pill rises from the icon: socials on top, orange icon at the
		   bottom, sitting exactly over the navy one. */
		.thc-pill {
			position: absolute;
			bottom: 0;
			left: 0;
			display: flex;
			flex-direction: column;
			justify-content: flex-end;
			align-items: center;
			gap: 10px;
			width: 56px;
			padding: 6px 6px 0;
			border-radius: 100px;
			background: #fff;
			opacity: 0;
			pointer-events: none;
			transition: opacity 0.4s var(--thc-ease);
		}

		.thc .thc-social {
			flex-direction: column;
			flex-wrap: nowrap;
			gap: 8px;
		}

		.thc-social-link {
			background: rgb(253 85 35 / 0.1);
		}

		.thc-card:hover .thc-share-icon,
		.thc-card:focus-within .thc-share-icon {
			opacity: 0;
		}

		.thc-card:hover .thc-pill,
		.thc-card:focus-within .thc-pill {
			opacity: 1;
			pointer-events: auto;
		}
	}

	@media (min-width: 1200px) {
		.thc {
			padding: 120px 0;
		}

		.thc .thc-eyebrow {
			font-size: 18px;
		}

		.thc .thc-heading {
			font-size: 48px;
			line-height: 57.6px;
		}

	}

	@media (prefers-reduced-motion: reduce) {
		.thc-title,
		.thc-card,
		.thc-media img {
			transition: none;
		}

		.thc-card:hover .thc-media img {
			transform: none;
		}

		.thc[data-thc-ready]:not([data-in]) .thc-title,
		.thc[data-thc-ready]:not([data-in]) .thc-card {
			opacity: 1;
			transform: none;
		}
	}

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

		.thc-social-link {
			border: 1px solid LinkText;
		}
	}
</style>

<script>
	/*
	 * Appear-on-scroll only; the hover reveal is CSS. One IntersectionObserver
	 * per instance, scoped to its root, torn down on astro:before-swap.
	 */
	function initTeamHoverCards(root: HTMLElement) {
		if (root.dataset.thcReady === "" || root.dataset.in === "") 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.thcReady = "";

		const observer = new IntersectionObserver(
			(entries) => {
				if (entries.some((entry) => entry.isIntersecting)) {
					root.dataset.in = "";
					observer.disconnect();
				}
			},
			{ threshold: 0.15 },
		);
		observer.observe(root);
		signal.addEventListener("abort", () => observer.disconnect());

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

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

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

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

===== END src/components/TeamHoverCards.astro =====

===== FILE (required): public/team-hover-cards/member-1.webp =====
[binary asset — download https://astro.baysixmedia.com/team-hover-cards/member-1.webp and save it to public/team-hover-cards/member-1.webp]

===== FILE (required): public/team-hover-cards/member-2.webp =====
[binary asset — download https://astro.baysixmedia.com/team-hover-cards/member-2.webp and save it to public/team-hover-cards/member-2.webp]

===== FILE (required): public/team-hover-cards/member-3.webp =====
[binary asset — download https://astro.baysixmedia.com/team-hover-cards/member-3.webp and save it to public/team-hover-cards/member-3.webp]

===== FILE (required): src/components/CargoLogoMarquee.astro =====
---
/**
 * CargoLogoMarquee
 *
 * A band of partner wordmarks drifting slowly right to left, one full loop
 * of the list every ~92s.
 *
 * - `tone: "photo"` (default): a 200px band over a port photo under a navy
 *   overlay, with white and orange marks.
 * - `tone: "light"`: a light grey band with navy and orange marks, and an
 *   optional heading line above it set between two hairlines.
 *
 * Marks come only from `logos` (with no logos it renders nothing); six simple
 * ship / crane / plane / rail / truck / globe glyphs are drawn inline. The marquee is
 * pure CSS: the list is rendered twice and the track slides by exactly one
 * copy, so the loop is seamless; the copy is aria-hidden. It pauses on hover,
 * while anything inside has focus, and while its pause toggle is on; it never
 * runs under prefers-reduced-motion. No script.
 *
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

export type CargoGlyph = "ship" | "crane" | "plane" | "rail" | "truck" | "globe";

export interface CargoLogo {
	/** First word of the wordmark, in the base colour. */
	name: string;
	/** Second word, in orange. */
	accent?: string;
	/** Tiny line under the wordmark. */
	tagline?: string;
	glyph?: CargoGlyph;
	/** Optional link for the mark. */
	href?: string;
}

export interface CargoLogoMarqueeProps {
	tone?: "photo" | "light";
	/** Light tone only: the line above the band. `""` drops it. */
	heading?: string;
	logos?: CargoLogo[];
	/** Photo tone background. `src: ""` = plain navy band. */
	image?: { src?: string; alt?: string };
	/** Seconds for one full loop of the list. */
	duration?: number;
	/** A small pause toggle in the band's corner (WCAG 2.2.2). */
	pauseToggle?: boolean;
	/** Accessible name for the list of marks. */
	label?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends CargoLogoMarqueeProps {}

const {
	tone = "photo",
	heading = "",
	logos: rawLogos = [],
	image: imageProp,
	duration = 92,
	pauseToggle = true,
	label = "Partners",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

/* No photo by default: the photo tone falls back to its plain navy band. */
const image = { src: "", alt: "", ...(imageProp ?? {}) };
/* A mark without a name is dropped. No marks, no band: the heading alone would be a claim. */
const logos = rawLogos.filter((logo) => logo?.name);
const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");
const uid = id ?? `clm-${Math.random().toString(36).slice(2, 8)}`;
const copies = [false, true];
const seconds = Math.max(5, Number(duration) || 92);
---

{logos.length > 0 && (
<>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section
	class:list={["clm", `clm--${tone}`, className]}
	id={id}
	style={`--clm-duration: ${seconds}s;`}
	aria-label={label}
	data-clm-root
>
	{
		tone === "light" && heading && (
			<div class="clm-heading-row">
				<span class="clm-hairline" aria-hidden="true" />
				<p class="clm-heading">{heading}</p>
				<span class="clm-hairline" aria-hidden="true" />
			</div>
		)
	}
	<div class="clm-band">
		{
			tone === "photo" && image.src && (
				<img class="clm-bg" src={image.src} alt={image.alt} width="2000" height="280" loading="lazy" decoding="async" />
			)
		}
		{
			pauseToggle && (
				<>
					<input class="clm-toggle" type="checkbox" id={`${uid}-pause`} />
					<label class="clm-toggle-label" for={`${uid}-pause`}>
						<span class="clm-sr">Pause the moving logos</span>
						<svg class="clm-i-pause" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false">
							<path d="M3 2h2v8H3zM7 2h2v8H7z" fill="currentColor" />
						</svg>
						<svg class="clm-i-play" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false">
							<path d="M3 1.5v9l7.5-4.5z" fill="currentColor" />
						</svg>
					</label>
				</>
			)
		}
		<div class="clm-viewport">
			<div class="clm-track">
				{
					copies.map((isCopy) => (
						<ul class="clm-group" role="list" aria-hidden={isCopy ? "true" : undefined}>
							{logos.map((logo) => {
								const inner = (
									<>
										<svg class="clm-glyph" viewBox="0 0 80 44" width="80" height="44" aria-hidden="true" focusable="false">
											{logo.glyph === "ship" && (
												<g>
													<rect class="a" x="18" y="12" width="10" height="10" rx="1" />
													<rect class="a" x="30" y="12" width="10" height="10" rx="1" />
													<rect class="a" x="42" y="6" width="10" height="16" rx="1" />
													<path class="p" d="M56 6h8v18h-8z" />
													<path class="p" d="M4 25h72l-9 13H15z" />
													<path class="s" d="M8 41c5 0 5-2 10-2s5 2 10 2 5-2 10-2 5 2 10 2 5-2 10-2 5 2 10 2" />
												</g>
											)}
											{logo.glyph === "crane" && (
												<g>
													<path class="s" d="M14 42V6h6v36M14 12h58M20 6l10 6M58 12v10" />
													<rect class="a" x="48" y="22" width="20" height="10" rx="1" />
													<path class="p" d="M4 38h34v4H4z" />
													<rect class="a" x="40" y="34" width="14" height="8" rx="1" />
													<rect class="p" x="56" y="34" width="16" height="8" rx="1" />
												</g>
											)}
											{logo.glyph === "plane" && (
												<g>
													<path class="a" d="M8 32 70 12c4-1 7 0 7 2s-3 4-6 5L16 36z" />
													<path class="p" d="M36 22 28 4h6l18 14zM40 26l-4 14h6l12-18zM14 32 8 22h4l10 8z" />
												</g>
											)}
											{logo.glyph === "rail" && (
												<g>
													<path class="a" d="M6 10h50l16 12v12H6z" />
													<rect class="p" x="12" y="15" width="8" height="7" rx="1" />
													<rect class="p" x="24" y="15" width="8" height="7" rx="1" />
													<rect class="p" x="36" y="15" width="8" height="7" rx="1" />
													<path class="p" d="M56 15h4l8 7H56z" />
													<circle class="p" cx="16" cy="37" r="4" />
													<circle class="p" cx="30" cy="37" r="4" />
													<circle class="p" cx="58" cy="37" r="4" />
													<path class="s" d="M2 42h76" />
												</g>
											)}
											{logo.glyph === "truck" && (
												<g>
													<rect class="a" x="4" y="8" width="46" height="24" rx="2" />
													<path class="p" d="M52 14h14l10 11v7H52z" />
													<path class="bg" d="M56 17h8l5 7H56z" />
													<circle class="p" cx="16" cy="36" r="5" />
													<circle class="p" cx="38" cy="36" r="5" />
													<circle class="p" cx="64" cy="36" r="5" />
												</g>
											)}
											{logo.glyph === "globe" && (
												<g>
													<circle class="s" cx="40" cy="22" r="18" />
													<ellipse class="s" cx="40" cy="22" rx="8" ry="18" />
													<path class="s" d="M22 22h36" />
													<path class="a" d="M6 30c16 10 52 10 68-8l2 3C60 44 20 42 6 30z" />
												</g>
											)}
										</svg>
										<span class="clm-name">
											{logo.name}
											{logo.accent && <span class="clm-accent">{logo.accent}</span>}
										</span>
										{logo.tagline && <span class="clm-tag">{logo.tagline}</span>}
									</>
								);
								return (
									<li class="clm-item">
										{logo.href ? (
											<a class="clm-logo" href={safeHref(logo.href)} tabindex={isCopy ? "-1" : undefined}>
												{inner}
											</a>
										) : (
											<span class="clm-logo">{inner}</span>
										)}
									</li>
								);
							})}
						</ul>
					))
				}
			</div>
		</div>
	</div>
</section>
</>
)}

<style>
	.clm {
		--clm-navy: var(--tg-navy, #020e28);
		--clm-orange: var(--tg-orange, #fd5523);
		--clm-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
		--clm-gap: 64px;

		box-sizing: border-box;
		font-family: var(--clm-display);
		overflow-x: clip;
	}

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

	.clm--photo {
		--clm-ink: #fff;
		background: var(--clm-navy);
	}

	.clm--light {
		--clm-ink: var(--clm-navy);
		display: flex;
		flex-direction: column;
		gap: 40px;
		background: #fff;
	}

	/* ---- light tone heading line ---- */
	.clm-heading-row {
		display: flex;
		justify-content: center;
		align-items: center;
		gap: 16px;
		width: 100%;
		max-width: 1293px;
		margin: 0 auto;
		padding: 0 20px;
	}

	.clm-hairline {
		display: none;
		flex: 1 1 0;
		max-width: 300px;
		height: 1px;
		background: #d8dde1;
	}

	.clm .clm-heading {
		margin: 0;
		font: 500 18px/1.5 var(--clm-display);
		text-align: center;
		color: var(--clm-navy);
	}

	/* ---- band ---- */
	.clm-band {
		position: relative;
		display: flex;
		align-items: center;
		min-height: 136px;
		padding: 28px 0;
		isolation: isolate;
	}

	.clm--light .clm-band {
		padding: 40px 0;
		background: #f4f4f4;
	}

	.clm-bg {
		position: absolute;
		inset: 0;
		z-index: -2;
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	/* Navy overlay over the photo. */
	.clm--photo .clm-band::before {
		content: "";
		position: absolute;
		inset: 0;
		z-index: -1;
		background: rgb(2 14 40 / 0.86);
	}

	.clm-viewport {
		width: 100%;
		overflow: hidden;
		container-type: inline-size;
	}

	.clm-track {
		display: flex;
		width: max-content;
		animation: clm-slide var(--clm-duration, 92s) linear infinite;
	}

	@keyframes clm-slide {
		from {
			transform: translateX(0);
		}
		to {
			transform: translateX(-50%);
		}
	}

	.clm .clm-group {
		display: flex;
		flex: none;
		justify-content: space-around;
		align-items: center;
		gap: var(--clm-gap);
		min-width: 100cqw;
		margin: 0;
		padding: 0 var(--clm-gap) 0 0;
		list-style: none;
	}

	.clm-item {
		flex: none;
	}

	.clm-logo {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 4px;
		height: 80px;
		justify-content: center;
		color: var(--clm-ink);
		text-decoration: none;
		white-space: nowrap;
	}

	a.clm-logo:focus-visible {
		outline: 2px solid var(--clm-orange);
		outline-offset: 4px;
		border-radius: 4px;
	}

	.clm-glyph {
		display: block;
		width: 64px;
		height: auto;
		overflow: visible;
	}

	.clm-glyph .p {
		fill: var(--clm-ink);
	}

	.clm-glyph .a {
		fill: var(--clm-orange);
	}

	.clm-glyph .s {
		fill: none;
		stroke: var(--clm-ink);
		stroke-width: 3;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	.clm-glyph .bg {
		fill: var(--clm-orange);
		opacity: 0.35;
	}

	.clm-name {
		font: 700 15px/1 var(--clm-display);
		letter-spacing: 0.02em;
		text-transform: uppercase;
	}

	.clm-accent {
		color: var(--clm-orange);
	}

	.clm-tag {
		font: 500 6.5px/1 var(--clm-display);
		letter-spacing: 0.24em;
		text-transform: uppercase;
		opacity: 0.7;
	}

	/* ---- pause: hover, focus inside, or the toggle ---- */
	.clm-band:hover .clm-track,
	.clm-band:focus-within .clm-track,
	.clm-toggle:checked ~ .clm-viewport .clm-track {
		animation-play-state: paused;
	}

	.clm-toggle {
		position: absolute;
		width: 1px;
		height: 1px;
		margin: 0;
		opacity: 0;
		pointer-events: none;
	}

	.clm-toggle-label {
		position: absolute;
		right: 8px;
		bottom: 4px;
		z-index: 1;
		display: grid;
		place-items: center;
		width: 44px;
		height: 44px;
		border-radius: 50%;
		color: var(--clm-ink);
		opacity: 0.35;
		cursor: pointer;
		transition: opacity 0.3s ease-out;
	}

	.clm-toggle-label::before {
		content: "";
		position: absolute;
		inset: 10px;
		border: 1px solid currentColor;
		border-radius: 50%;
	}

	.clm-band:hover .clm-toggle-label,
	.clm-toggle:focus-visible + .clm-toggle-label {
		opacity: 1;
	}

	.clm-toggle:focus-visible + .clm-toggle-label {
		outline: 2px solid var(--clm-orange);
		outline-offset: -2px;
	}

	.clm-i-play,
	.clm-toggle:checked + .clm-toggle-label .clm-i-pause {
		display: none;
	}

	.clm-toggle:checked + .clm-toggle-label .clm-i-play {
		display: block;
	}

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

	@media (min-width: 810px) {
		.clm {
			--clm-gap: 120px;
		}

		.clm--light {
			gap: 60px;
		}

		.clm-hairline {
			display: block;
		}

		.clm .clm-heading {
			font-size: 20px;
			line-height: 30px;
		}

		.clm-band {
			min-height: 200px;
			padding: 60px 0;
		}

		.clm--light .clm-band {
			padding: 60px 0;
		}

		.clm-glyph {
			width: 76px;
		}

		.clm-name {
			font-size: 17px;
		}
	}

	@media (min-width: 1200px) {
		.clm {
			--clm-gap: 171px;
		}
	}

	/* Static under reduced motion; the copy is hidden so nothing repeats. */
	@media (prefers-reduced-motion: reduce) {
		.clm-track {
			animation: none;
			width: 100%;
		}

		.clm .clm-group {
			flex-wrap: wrap;
			justify-content: center;
			row-gap: 24px;
			min-width: 0;
			width: 100%;
			padding: 0 20px;
			gap: 24px 48px;
		}

		.clm .clm-group[aria-hidden="true"],
		.clm-toggle,
		.clm-toggle-label {
			display: none;
		}
	}

	@media (forced-colors: active) {
		.clm-glyph .p,
		.clm-glyph .a,
		.clm-glyph .bg {
			fill: CanvasText;
		}

		.clm-glyph .s {
			stroke: CanvasText;
		}
	}
</style>

===== END src/components/CargoLogoMarquee.astro =====

===== FILE (required): public/cargo-logo-marquee/port-aerial.webp =====
[binary asset — download https://astro.baysixmedia.com/cargo-logo-marquee/port-aerial.webp and save it to public/cargo-logo-marquee/port-aerial.webp]

===== FILE (required): src/components/TrustReasonsSplit.astro =====
---
/**
 * TrustReasonsSplit
 *
 * A white split section: a tall photo in a 30px-rounded frame on one side;
 * on the other an orange eyebrow with a trailing line, a big navy heading, a
 * paragraph, a two-by-two list of reasons with orange "»" markers, and a row
 * holding an orange pill button and a "call us" block (navy round phone
 * icon, a label and an orange phone link).
 *
 * Hover, as measured: the pill turns navy and its arrow rotates 45deg; the
 * phone link turns from orange to navy (0.4s, cubic-bezier(.44,0,.56,1)).
 *
 * Appear on scroll: the photo slides in from -40px, the text from +40px
 * (mirrored with `imageSide="right"`). Reads --tg-* design tokens from any
 * ancestor, with fallbacks.
 */

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

export interface TrustReasonsContact {
	/** The small line above the number. */
	label?: string;
	/** The number as displayed. */
	phone: string;
	/** Defaults to a tel: link built from `phone`. */
	href?: string;
}

export interface TrustReasonsSplitProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	text?: string;
	/** The "»" list. Two columns from 810px. */
	reasons?: string[];
	/** The orange pill. `null` (the default) drops it. */
	cta?: TrustReasonsLink | null;
	/** The call-us block. `null` (the default) drops it; a phone with no digits and no `href` drops it too. */
	contact?: TrustReasonsContact | null;
	/** No `src` renders the labelled placeholder. */
	image?: { src?: string; alt?: string };
	/** Which side the photo sits on from 810px. */
	imageSide?: "left" | "right";
	/** The section surface. */
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends TrustReasonsSplitProps {}

/*
 * Every piece of copy, the reasons, the button, the phone and the photo are
 * the caller's: nothing about a business is filled in here. The catalog demo
 * passes its sample content through the registry's `props`.
 */
const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	text = "",
	reasons = [],
	cta = null,
	contact = null,
	image: imageProp,
	imageSide = "left",
	background,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const image = { src: imageProp?.src ?? "", alt: imageProp?.alt ?? "" };

const Heading = headingLevel;

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

/* 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}` : "";
};

/* A call link only with somewhere to dial: words in `phone` are not a number. */
const phoneNumber = typeof contact?.phone === "string" ? contact.phone.trim() : "";
const passedHref = contact?.href ? safeHref(contact.href) : "#";
const callHref = phoneNumber ? (passedHref !== "#" ? passedHref : telHref(phoneNumber)) : "";
const call = callHref ? { label: contact?.label ?? "", phone: phoneNumber, href: callHref } : null;

const reasonList = (reasons ?? []).filter((r) => typeof r === "string" && r.trim());

/* Nothing to say → nothing rendered: an eyebrow or a photo alone is not a section. */
const hasContent = Boolean(heading || text || reasonList.length > 0 || cta || call);

/* Measured slot: 537 × 644 at desktop (~0.83:1); the file is 1074 × 1288. */
const IMAGE_SPEC = "~5:6 · 1074 × 1288 px";
---

{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<section
	class:list={["trs", className]}
	id={id}
	style={background ? `--trs-bg: ${background};` : undefined}
	data-trs-root
	data-image-side={imageSide}
>
	<div class="trs-inner">
		<div class="trs-media" data-trs-media>
			{
				image.src ? (
					<img src={image.src} alt={image.alt} width="1074" height="1288" loading="lazy" decoding="async" />
				) : (
					<div class="trs-ph" role="img" aria-label="Image placeholder: portrait photo">
						<div class="trs-ph-text" aria-hidden="true">
							<span class="trs-ph-label">Image</span>
							<span class="trs-ph-hint">Portrait photo</span>
							<span class="trs-ph-spec">{IMAGE_SPEC}</span>
						</div>
					</div>
				)
			}
		</div>

		<div class="trs-copy" data-trs-copy>
			{(eyebrow || heading) && (
			<div class="trs-title">
				{
					eyebrow && (
						<p class="trs-eyebrow">
							{eyebrow}
							<span class="trs-eyebrow-line" aria-hidden="true" />
						</p>
					)
				}
				{heading && <Heading class="trs-heading">{heading}</Heading>}
			</div>
			)}
			{text && <p class="trs-text">{text}</p>}
			{
				(reasonList.length > 0 || cta || call) && (
					<div class="trs-items">
						{reasonList.length > 0 && (
							<ul class="trs-reasons" role="list">
								{reasonList.map((reason) => (
									<li class="trs-reason">
										<svg class="trs-mark" viewBox="0 0 18 14" width="18" height="14" aria-hidden="true" focusable="false">
											<path d="M1 1.5 6.5 7 1 12.5M9 1.5 14.5 7 9 12.5" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" />
										</svg>
										<span>{reason}</span>
									</li>
								))}
							</ul>
						)}
						{(cta || call) && (
							<div class="trs-actions">
								{cta && (
									<a class="trs-btn" href={safeHref(cta.href)}>
										{cta.label}
										<svg class="trs-btn-arrow" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false">
											<path d="M2 10 10 2M3.5 2H10v6.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
										</svg>
									</a>
								)}
								{call && (
									<div class="trs-contact">
										<span class="trs-contact-icon" aria-hidden="true">
											<svg viewBox="0 0 24 24" width="22" height="22">
												<path d="M6.6 10.8a15.2 15.2 0 0 0 6.6 6.6l2.2-2.2a1 1 0 0 1 1-.25 11.4 11.4 0 0 0 3.6.57 1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57a1 1 0 0 1-.25 1z" fill="currentColor" />
											</svg>
										</span>
										<p class="trs-contact-text">
											{call.label && <span class="trs-contact-label">{call.label}</span>}
											<a class="trs-phone" href={call.href}>{call.phone}</a>
										</p>
									</div>
								)}
							</div>
						)}
					</div>
				)
			}
		</div>
	</div>
</section>
)}

<style>
	.trs {
		--trs-navy: var(--tg-navy, #020e28);
		--trs-orange: var(--tg-orange, #fd5523);
		--trs-muted: var(--tg-muted, #61697d);
		--trs-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
		--trs-body: var(--tg-font-body, "DM Sans", "DM Sans Placeholder", ui-sans-serif, system-ui, sans-serif);
		--trs-ease: cubic-bezier(0.22, 1, 0.36, 1);
		--trs-from-media: -40px;
		--trs-from-copy: 40px;

		box-sizing: border-box;
		padding: 80px 0;
		background: var(--trs-bg, #fff);
		color: var(--trs-navy);
		font-family: var(--trs-body);
		overflow-x: clip;
	}

	.trs[data-image-side="right"] {
		--trs-from-media: 40px;
		--trs-from-copy: -40px;
	}

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

	.trs-inner {
		display: flex;
		flex-direction: column;
		gap: 32px;
		max-width: 1234px;
		margin: 0 auto;
		padding: 0 20px;
	}

	.trs-media {
		position: relative;
		aspect-ratio: 35 / 32;
		border-radius: 20px;
		overflow: hidden;
		background: #e6e9ee;
	}

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

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

	.trs-title {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 16px;
	}

	.trs .trs-eyebrow {
		display: inline-flex;
		align-items: center;
		gap: 12px;
		margin: 0;
		font: 600 16px/1 var(--trs-display);
		text-transform: uppercase;
		color: var(--trs-orange);
	}

	.trs-eyebrow-line {
		display: none;
		width: 40px;
		height: 1px;
		background: currentColor;
	}

	.trs .trs-heading {
		margin: 0;
		font: 600 32px/1.2 var(--trs-display);
		color: var(--trs-navy);
		overflow-wrap: break-word;
	}

	.trs .trs-text {
		margin: 0;
		font: 400 16px/21.04px var(--trs-body);
		color: var(--trs-muted);
	}

	.trs-items {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 32px;
		width: 100%;
	}

	.trs .trs-reasons {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: 10px 24px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.trs-reason {
		display: flex;
		align-items: flex-start;
		gap: 12px;
		font: 500 16px/26px var(--trs-display);
		color: var(--trs-navy);
	}

	.trs-mark {
		flex: none;
		width: 18px;
		height: 14px;
		margin-top: 6px;
		color: var(--trs-orange);
	}

	.trs-actions {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 20px 34px;
	}

	.trs-btn {
		display: inline-flex;
		align-items: center;
		gap: 10px;
		min-height: 48px;
		padding: 0 24px;
		border-radius: 100px;
		background: var(--trs-orange);
		color: #fff;
		font: 600 14px/1 var(--trs-body);
		text-decoration: none;
		white-space: nowrap;
		transition: background-color 0.4s var(--trs-ease);
	}

	.trs-btn-arrow {
		flex: none;
		transition: transform 0.4s var(--trs-ease);
	}

	.trs-btn:hover {
		background: var(--trs-navy);
	}

	.trs-btn:hover .trs-btn-arrow {
		transform: rotate(45deg);
	}

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

	.trs-contact {
		display: flex;
		align-items: center;
		gap: 20px;
		min-width: 0;
	}

	.trs-contact-icon {
		display: grid;
		flex: none;
		place-items: center;
		width: 56px;
		height: 56px;
		border-radius: 50%;
		background: var(--trs-navy);
		color: #fff;
	}

	.trs .trs-contact-text {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 4px;
		margin: 0;
		font: 600 16px/1.3 var(--trs-display);
		color: var(--trs-navy);
	}

	.trs-phone {
		display: inline-flex;
		align-items: center;
		/* 44px hit area without changing the measured line spacing. */
		min-height: 44px;
		margin: -10px 0;
		color: var(--trs-orange);
		text-decoration: none;
		transition: color 0.4s cubic-bezier(0.44, 0, 0.56, 1);
	}

	.trs-phone:hover {
		color: var(--trs-navy);
	}

	.trs-phone:focus-visible {
		outline: 2px solid var(--trs-orange);
		outline-offset: 3px;
		border-radius: 2px;
	}

	.trs-ph {
		position: absolute;
		inset: 0;
		display: grid;
		place-items: center;
		padding: 16px;
		text-align: center;
		background: repeating-linear-gradient(135deg, #e6e9ee 0 12px, #edf0f3 12px 24px);
		color: rgb(2 14 40 / 0.62);
	}

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

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

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

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

	/* ---- appear on scroll ---- */
	.trs-media,
	.trs-copy {
		transition:
			opacity 0.9s var(--trs-ease),
			transform 0.9s var(--trs-ease);
	}

	.trs[data-trs-ready]:not([data-in]) .trs-media {
		opacity: 0;
		transform: translateX(var(--trs-from-media));
	}

	.trs[data-trs-ready]:not([data-in]) .trs-copy {
		opacity: 0;
		transform: translateX(var(--trs-from-copy));
	}

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

		.trs-inner {
			display: grid;
			grid-template-columns: minmax(0, 537fr) minmax(0, 597fr);
			align-items: center;
			gap: 40px;
		}

		.trs[data-image-side="right"] .trs-media {
			order: 2;
		}

		.trs-media {
			aspect-ratio: 537 / 644;
			border-radius: 30px;
		}

		.trs-copy {
			gap: 40px;
		}

		.trs-eyebrow-line {
			display: block;
		}

		.trs .trs-heading {
			font-size: 40px;
		}

		.trs-items {
			gap: 50px;
		}

		.trs .trs-reasons {
			grid-template-columns: repeat(2, minmax(0, max-content));
			gap: 24px;
		}

		.trs-actions {
			flex-direction: row;
			flex-wrap: wrap;
			align-items: center;
		}

		.trs-btn {
			min-height: 56px;
			padding: 0 30px;
		}
	}

	@media (min-width: 1200px) {
		.trs {
			padding: 120px 0;
		}

		/* Step 39.2: --tg-grid-max / --tg-grid-gutter put the content on a site grid at desktop; the fallbacks are the original 1234 / 20. */
		.trs-inner {
			column-gap: 60px;
			max-width: var(--tg-grid-max, 1234px);
			padding-inline: var(--tg-grid-gutter, 20px);
		}

		.trs .trs-eyebrow {
			font-size: 18px;
		}

		.trs .trs-heading {
			font-size: 48px;
			line-height: 57.6px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.trs-media,
		.trs-copy,
		.trs-btn-arrow {
			transition: none;
		}

		.trs-btn:hover .trs-btn-arrow {
			transform: none;
		}

		.trs[data-trs-ready]:not([data-in]) .trs-media,
		.trs[data-trs-ready]:not([data-in]) .trs-copy {
			opacity: 1;
			transform: none;
		}
	}

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

		.trs-contact-icon {
			border: 1px solid CanvasText;
		}
	}
</style>

<script>
	/*
	 * Appear-on-scroll. 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.
	 */
	function initTrustReasonsSplit(root: HTMLElement) {
		if (root.dataset.trsReady === "" || root.dataset.in === "") 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.trsReady = "";

		const observer = new IntersectionObserver(
			(entries) => {
				if (entries.some((entry) => entry.isIntersecting)) {
					root.dataset.in = "";
					observer.disconnect();
				}
			},
			{ threshold: 0.15 },
		);
		observer.observe(root);
		signal.addEventListener("abort", () => observer.disconnect());

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

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

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

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

===== END src/components/TrustReasonsSplit.astro =====

===== FILE (required): public/trust-reasons-split/ship-loading.webp =====
[binary asset — download https://astro.baysixmedia.com/trust-reasons-split/ship-loading.webp and save it to public/trust-reasons-split/ship-loading.webp]

===== FILE (required): src/components/NumberedTestimonialSwitcher.astro =====
---
/**
 * NumberedTestimonialSwitcher
 *
 * A light testimonial section: centred eyebrow and heading, then a rounded
 * portrait on the left and a large quote, name and role on the right. Under
 * the quote, numbered tabs "01 ——— 02 03 04" pick the testimonial; the
 * active number carries the orange line after it. Picking one cross-fades
 * the portrait and slides its quote card in from the side (from the right
 * when moving forward, from the left when moving back). No autoplay.
 *
 * Real WAI-ARIA tabs: roving tabindex, Left/Right/Home/End, aria-selected
 * and aria-controls. Below 810px the reference drops the tabs and lists
 * every testimonial, each with its own photo; so does this.
 *
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

export interface NumberedTestimonial {
	quote: string;
	name: string;
	role?: string;
	/** Portrait. `src: ""` renders the labelled placeholder instead. */
	image?: { src?: string; alt?: string };
}

export interface NumberedTestimonialSwitcherProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	testimonials?: NumberedTestimonial[];
	/** 1-based testimonial shown first. */
	initial?: number;
	/** Accessible name of the tab row. */
	tabsLabel?: string;
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends NumberedTestimonialSwitcherProps {}

const {
	eyebrow = "Testimonial",
	heading = "What Our Clients Say",
	headingLevel = "h2",
	/* No testimonials of its own: they come from the caller or not at all. */
	testimonials = [],
	initial = 1,
	tabsLabel = "Choose a testimonial",
	background = "#f6f6f6",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;
const uid = id ?? `nts-${Math.random().toString(36).slice(2, 9)}`;
const items = testimonials.slice(0, 9);
const active = Math.min(Math.max(1, Math.round(initial)), Math.max(1, items.length)) - 1;
const pad = (n: number) => String(n).padStart(2, "0");
const pos = (i: number) => (i === active ? "active" : i < active ? "before" : "after");

/* Measured slot: 392 × 452 at desktop; the files are 784 × 904. */
const IMAGE_SPEC = "~6:7 portrait · 784 × 904 px";
---

{items.length > 0 && (<>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section
	class:list={["nts", className]}
	id={id}
	style={`--nts-bg: ${background};`}
	data-nts-root
>
	<div class="nts-inner">
		<div class="nts-title" data-nts-appear="up">
			{
				eyebrow && (
					<p class="nts-eyebrow">
						<span class="nts-eyebrow-line" aria-hidden="true" />
						{eyebrow}
						<span class="nts-eyebrow-line" aria-hidden="true" />
					</p>
				)
			}
			<Heading class="nts-heading">{heading}</Heading>
		</div>

		<div class="nts-stage">
			{
				items.map((t, i) => {
					const image = { src: "", alt: "", ...(t.image ?? {}) };
					return (
						<div
							class="nts-panel"
							id={`${uid}-panel-${i + 1}`}
							role="tabpanel"
							aria-labelledby={`${uid}-tab-${i + 1}`}
							tabindex="0"
							data-nts-panel
							data-pos={pos(i)}
						>
							<div class="nts-photo-col" data-nts-appear="left">
								<div class="nts-photo">
									{image.src ? (
										<img
											src={image.src}
											alt={image.alt}
											width="784"
											height="904"
											loading={i === active ? "eager" : "lazy"}
											decoding="async"
										/>
									) : (
										<div class="nts-ph" role="img" aria-label={`Image placeholder: portrait of ${t.name}`}>
											<div class="nts-ph-text" aria-hidden="true">
												<span class="nts-ph-label">Image</span>
												<span class="nts-ph-hint">Portrait of {t.name}</span>
												<span class="nts-ph-spec">{IMAGE_SPEC}</span>
											</div>
										</div>
									)}
								</div>
							</div>
							<div class="nts-copy" data-nts-appear="right">
								<figure class="nts-card">
									<blockquote class="nts-quote">
										<p>{t.quote}</p>
									</blockquote>
									<figcaption class="nts-person">
										<span class="nts-name">{t.name}</span>
										{t.role && <span class="nts-role">{t.role}</span>}
									</figcaption>
									<span class="nts-glyph nts-glyph--phone" aria-hidden="true">
										<svg viewBox="0 0 118 75" width="46" height="29" focusable="false">
											<path d="M24 4C12 4 3 13 3 25s9 21 21 21c2 0 4 0 5-1-2 9-9 17-19 21l3 6c19-6 32-22 32-42C45 14 36 4 24 4Z" />
											<path d="M86 4C74 4 65 13 65 25s9 21 21 21c2 0 4 0 5-1-2 9-9 17-19 21l3 6c19-6 32-22 32-42 0-16-9-26-21-26Z" />
										</svg>
									</span>
								</figure>
							</div>
						</div>
					);
				})
			}

			<div class="nts-bottom" data-nts-appear="right">
				<div class="nts-tabs" role="tablist" aria-label={tabsLabel}>
					{
						items.map((t, i) => (
							<button
								type="button"
								class="nts-tab"
								id={`${uid}-tab-${i + 1}`}
								role="tab"
								aria-selected={i === active ? "true" : "false"}
								aria-controls={`${uid}-panel-${i + 1}`}
								tabindex={i === active ? "0" : "-1"}
								data-nts-tab
							>
								<span class="nts-num">{pad(i + 1)}</span>
								<span class="nts-sr">: {t.name}</span>
								<span class="nts-line" aria-hidden="true" />
							</button>
						))
					}
				</div>
				<span class="nts-glyph nts-glyph--desk" aria-hidden="true">
					<svg viewBox="0 0 118 75" width="116" height="74" focusable="false">
						<path d="M24 4C12 4 3 13 3 25s9 21 21 21c2 0 4 0 5-1-2 9-9 17-19 21l3 6c19-6 32-22 32-42C45 14 36 4 24 4Z" />
						<path d="M86 4C74 4 65 13 65 25s9 21 21 21c2 0 4 0 5-1-2 9-9 17-19 21l3 6c19-6 32-22 32-42 0-16-9-26-21-26Z" />
					</svg>
				</span>
			</div>
		</div>
	</div>
</section>
</>)}

<style>
	.nts {
		--nts-navy: var(--tg-navy, #020e28);
		--nts-orange: var(--tg-orange, #fd5523);
		--nts-muted: var(--tg-muted, #61697d);
		--nts-display: var(--tg-font-display, Outfit, ui-sans-serif, system-ui, sans-serif);
		--nts-body: var(--tg-font-body, "DM Sans", ui-sans-serif, system-ui, sans-serif);
		--nts-ease: cubic-bezier(0.22, 1, 0.36, 1);
		--nts-photo-w: 392px;
		--nts-col-gap: 61px;

		position: relative;
		box-sizing: border-box;
		padding: 60px 16px;
		background: var(--nts-bg);
		color: var(--nts-navy);
		font-family: var(--nts-body);
		overflow: hidden;
	}

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

	.nts-inner {
		display: flex;
		flex-direction: column;
		gap: 32px;
		max-width: 1224px;
		margin: 0 auto;
	}

	/* ---- title block ---- */
	.nts-title {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 16px;
	}

	.nts .nts-eyebrow {
		display: flex;
		align-items: center;
		gap: 12px;
		margin: 0;
		font: 600 16px/1 var(--nts-display);
		text-transform: uppercase;
		color: var(--nts-orange);
	}

	.nts-eyebrow-line {
		display: none;
		width: 40px;
		height: 1px;
		background: currentColor;
	}

	.nts-heading {
		margin: 0;
		font: 600 30px/1.2 var(--nts-display);
		text-transform: capitalize;
		color: var(--nts-navy);
	}

	/* ---- stage: phone = a list of every testimonial ---- */
	.nts-stage {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: 40px;
	}

	.nts-panel {
		display: flex;
		flex-direction: column;
		gap: 24px;
		border-radius: 20px;
	}

	.nts-panel:focus-visible {
		outline: 2px solid var(--nts-orange);
		outline-offset: 6px;
	}

	.nts-photo {
		position: relative;
		overflow: hidden;
		aspect-ratio: 334 / 212;
		border-radius: 20px;
		background: #e3e6e9;
	}

	.nts-photo img {
		position: absolute;
		inset: 0;
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
		object-position: 50% 25%;
	}

	.nts-card {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: 24px;
		margin: 0;
	}

	.nts-quote {
		margin: 0;
	}

	.nts .nts-quote p {
		margin: 0;
		font: 400 20px/1.4 var(--nts-display);
		color: var(--nts-navy);
	}

	.nts-person {
		display: flex;
		flex-direction: column;
		gap: 10px;
		padding-right: 60px;
	}

	.nts-name {
		font: 600 22px/1.1 var(--nts-display);
		color: var(--nts-navy);
	}

	.nts-role {
		font: 600 14px/1 var(--nts-display);
		text-transform: uppercase;
		color: var(--nts-orange);
	}

	.nts-glyph {
		color: rgb(253 85 35 / 0.14);
		line-height: 0;
	}

	.nts-glyph svg {
		display: block;
		fill: currentColor;
	}

	.nts-glyph--phone {
		position: absolute;
		right: 0;
		bottom: 0;
	}

	/* The tabs are a desktop/tablet control; the phone lists everything. */
	.nts-bottom {
		display: none;
	}

	/* ---- tablet and up: portrait left, switching quote right ---- */
	@media (min-width: 810px) {
		.nts {
			--nts-photo-w: 300px;
			--nts-col-gap: 40px;
			padding: 100px 30px;
		}

		.nts-inner {
			gap: 60px;
		}

		.nts-title {
			align-items: center;
			gap: 20px;
			text-align: center;
		}

		.nts .nts-eyebrow {
			font-size: 18px;
		}

		.nts-eyebrow-line {
			display: block;
		}

		.nts-heading {
			font-size: 40px;
			line-height: 1.2;
		}

		.nts-stage {
			display: grid;
			grid-template: "stack" auto / minmax(0, 1fr);
			gap: 0;
		}

		/* Every panel occupies the same cell; the active one sits on top. */
		.nts-panel {
			grid-area: stack;
			display: grid;
			grid-template-columns: var(--nts-photo-w) minmax(0, 1fr);
			column-gap: var(--nts-col-gap);
			gap: 0 var(--nts-col-gap);
			visibility: hidden;
			z-index: 0;
			/* Stay visible while the outgoing photo fades and card slides. */
			transition: visibility 0s linear 0.6s;
		}

		.nts-panel[data-pos="active"] {
			visibility: visible;
			z-index: 1;
			transition-delay: 0s;
		}

		.nts-photo-col {
			grid-column: 1;
		}

		.nts-photo {
			aspect-ratio: 392 / 452;
			border-radius: 30px;
			background: transparent;
		}

		/* Cross-fade: only the active panel's portrait is opaque. */
		.nts-photo {
			opacity: 0;
			transition: opacity 0.5s ease-out;
		}

		.nts-panel[data-pos="active"] .nts-photo {
			opacity: 1;
		}

		/* The copy column clips its card; the card slides in from the side. */
		.nts-copy {
			grid-column: 2;
			padding: 20px 0 72px;
			overflow: hidden;
		}

		.nts-card {
			gap: 48px;
			transform: translateX(calc(100% + 13px));
			transition: transform 0.6s var(--nts-ease);
		}

		.nts-panel[data-pos="before"] .nts-card {
			transform: translateX(calc(-100% - 13px));
		}

		.nts-panel[data-pos="active"] .nts-card {
			transform: none;
		}

		.nts .nts-quote p {
			font-size: 24px;
			line-height: 1.22;
		}

		.nts-person {
			gap: 19px;
			padding-right: 0;
		}

		.nts-name {
			font-size: 26px;
			line-height: 21px;
		}

		.nts-role {
			font-size: 16px;
			line-height: 12.6px;
		}

		.nts-glyph--phone {
			display: none;
		}

		/* The tab row + glyph sit at the bottom of the copy column. */
		.nts-bottom {
			position: absolute;
			z-index: 2;
			left: calc(var(--nts-photo-w) + var(--nts-col-gap));
			right: 0;
			bottom: 0;
			display: flex;
			align-items: center;
			min-height: 44px;
		}

		.nts-glyph--desk {
			position: absolute;
			right: 3px;
			bottom: 0;
			pointer-events: none;
		}

		/* Measured pitch: 44px per inactive number (24 + 20 gap), and the
		   active one plus its line (127px) then the same 20px. Each button is
		   a 44px target, so the pitch comes from min-width, not a gap. */
		.nts-tabs {
			display: flex;
			align-items: center;
			gap: 0;
		}

		.nts-tab[aria-selected="true"] {
			margin-right: 20px;
		}

		/* Tabs do nothing without the script, so they appear with it. */
		.nts:not([data-nts-ready]) .nts-tabs {
			visibility: hidden;
		}

		.nts-tab {
			display: inline-flex;
			align-items: center;
			gap: 7px;
			min-width: 44px;
			min-height: 44px;
			margin: 0;
			padding: 0;
			border: 0;
			border-radius: 6px;
			background: none;
			color: var(--nts-muted);
			font: 600 20px/18px var(--nts-display);
			cursor: pointer;
			transition: color 0.3s ease-out;
		}

		.nts-tab:hover {
			color: #f9562a;
		}

		.nts-tab[aria-selected="true"] {
			color: var(--nts-orange);
		}

		.nts-tab:focus-visible {
			outline: 2px solid var(--nts-orange);
			outline-offset: 2px;
		}

		.nts-line {
			display: block;
			width: 0;
			height: 2px;
			background: var(--nts-orange);
			transition: width 0.4s var(--nts-ease);
		}

		.nts-tab[aria-selected="true"] .nts-line {
			width: 99px;
		}
	}

	@media (min-width: 1200px) {
		.nts {
			--nts-photo-w: 392px;
			--nts-col-gap: 61px;
			padding: 120px 20px;
		}

		.nts-heading {
			font-size: 48px;
			line-height: 57.6px;
		}

		.nts-copy {
			padding-bottom: 60px;
		}

		.nts-card {
			gap: 60px;
		}

		.nts .nts-quote p {
			font-size: 30px;
			line-height: 36.6px;
		}

		.nts-name {
			font-size: 30px;
		}

		.nts-role {
			font-size: 18px;
		}
	}

	/* ---- placeholder for `src: ""` ---- */
	.nts-ph {
		position: absolute;
		inset: 0;
		display: grid;
		place-items: center;
		padding: 16px;
		text-align: center;
		border: 1px dashed rgb(2 14 40 / 0.22);
		border-radius: inherit;
		background: repeating-linear-gradient(135deg, #e6e9ee 0 12px, #edf0f3 12px 24px);
		color: rgb(2 14 40 / 0.62);
	}

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

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

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

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

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

	/* ---- appear on scroll: start pose only once the script is ready ---- */
	[data-nts-appear] {
		transition:
			opacity 0.8s var(--nts-ease),
			transform 0.8s var(--nts-ease);
	}

	.nts[data-nts-ready]:not([data-in]) [data-nts-appear] {
		opacity: 0;
	}

	.nts[data-nts-ready]:not([data-in]) [data-nts-appear="up"] {
		transform: translateY(40px);
	}

	.nts[data-nts-ready]:not([data-in]) [data-nts-appear="left"] {
		transform: translateX(-40px);
	}

	.nts[data-nts-ready]:not([data-in]) [data-nts-appear="right"] {
		transform: translateX(40px);
	}

	.nts[data-in] .nts-stage [data-nts-appear] {
		transition-delay: 0.1s;
	}

	@media (prefers-reduced-motion: reduce) {
		.nts [data-nts-appear],
		.nts .nts-panel,
		.nts .nts-photo,
		.nts .nts-card,
		.nts .nts-line,
		.nts .nts-tab {
			transition: none;
		}

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

	@media (forced-colors: active) {
		.nts-tab[aria-selected="true"] {
			text-decoration: underline;
		}

		.nts-line {
			background: CanvasText;
		}
	}
</style>

<script>
	/*
	 * NumberedTestimonialSwitcher controller.
	 *
	 * Automatic-activation tabs: Left/Right/Home/End move focus AND select;
	 * the roving tabindex keeps one tab in the Tab order. Selecting sets each
	 * panel's data-pos to before / active / after, and CSS does the portrait
	 * cross-fade and the card slide. One IntersectionObserver for the appear.
	 * Every listener and the observer hang off one AbortController, aborted on
	 * astro:before-swap. No timers, no autoplay, no focus moves on load.
	 */
	function initNumberedTestimonialSwitcher(root: HTMLElement) {
		if (root.dataset.ntsReady === "") return;

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

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

		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;
			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.dataset.pos = i === next ? "active" : i < next ? "before" : "after";
			});
			index = next;
			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":
						case "ArrowDown":
							next = index + 1;
							break;
						case "ArrowLeft":
						case "ArrowUp":
							next = index - 1;
							break;
						case "Home":
							next = 0;
							break;
						case "End":
							next = tabs.length - 1;
							break;
						default:
							return;
					}
					event.preventDefault();
					select(next, true);
				},
				{ signal },
			);
		});

		/* Warm the other portraits once the section is near, so a switch
		   never waits on a lazy image. */
		const warm = () =>
			root.querySelectorAll<HTMLImageElement>(".nts-photo img").forEach((img) => {
				img.loading = "eager";
			});

		root.dataset.ntsReady = "";
		let observer: IntersectionObserver | undefined;
		if (reduced || !("IntersectionObserver" in window)) {
			root.setAttribute("data-in", "");
			warm();
		} else {
			observer = new IntersectionObserver(
				(entries) => {
					if (entries.some((entry) => entry.isIntersecting)) {
						root.setAttribute("data-in", "");
						warm();
						observer?.disconnect();
					}
				},
				{ threshold: 0.15 },
			);
			observer.observe(root);
			signal.addEventListener("abort", () => observer?.disconnect());
		}

		document.addEventListener(
			"astro:before-swap",
			() => {
				controller.abort();
				select(servedIndex, false);
				root.removeAttribute("data-in");
				delete root.dataset.ntsReady;
			},
			{ once: true, signal },
		);
	}

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

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

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

===== END src/components/NumberedTestimonialSwitcher.astro =====

===== FILE (required): public/numbered-testimonial-switcher/client-1.webp =====
[binary asset — download https://astro.baysixmedia.com/numbered-testimonial-switcher/client-1.webp and save it to public/numbered-testimonial-switcher/client-1.webp]

===== FILE (required): public/numbered-testimonial-switcher/client-2.webp =====
[binary asset — download https://astro.baysixmedia.com/numbered-testimonial-switcher/client-2.webp and save it to public/numbered-testimonial-switcher/client-2.webp]

===== FILE (required): public/numbered-testimonial-switcher/client-3.webp =====
[binary asset — download https://astro.baysixmedia.com/numbered-testimonial-switcher/client-3.webp and save it to public/numbered-testimonial-switcher/client-3.webp]

===== FILE (required): public/numbered-testimonial-switcher/client-4.webp =====
[binary asset — download https://astro.baysixmedia.com/numbered-testimonial-switcher/client-4.webp and save it to public/numbered-testimonial-switcher/client-4.webp]

===== FILE (required): src/components/StackedPricingPlans.astro =====
---
/**
 * StackedPricingPlans
 *
 * A white pricing section: on the left an eyebrow, heading, paragraph and
 * two bold selling points with orange double-chevron marks; on the right
 * three horizontal plan cards stacked, each with the plan name, a price
 * "/Month", a "Get Started" pill and a feature list with orange checks (or a
 * grey cross for what the plan leaves out). One plan is featured: a dark
 * navy card with white text.
 *
 * The cards rise 40px into place, staggered, when the column scrolls in.
 * Side by side from 1200px; stacked below, and on a phone each card turns
 * vertical with a hairline between the price and the list.
 *
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

export interface PricingFeature {
	label: string;
	/** `false` renders the grey cross: not in this plan. */
	included?: boolean;
}

export interface PricingPlan {
	name: string;
	price: string;
	period?: string;
	featured?: boolean;
	cta?: { label: string; href: string } | null;
	features?: (string | PricingFeature)[];
}

export interface StackedPricingPlansProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	text?: string;
	/** Bold lines under the paragraph, each with the orange double chevron. */
	highlights?: string[];
	plans?: PricingPlan[];
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends StackedPricingPlansProps {}

/* Plans, prices, heading and copy are the caller's: nothing about a business
   is defaulted. The generic eyebrow stays, but never renders on its own. */
const {
	eyebrow = "Pricing plan",
	heading = "",
	headingLevel = "h2",
	text = "",
	highlights = [],
	plans = [],
	background = "#ffffff",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

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

const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");
const feature = (f: string | PricingFeature): PricingFeature =>
	typeof f === "string" ? { label: f, included: true } : { included: true, ...f };
const filled = (v: unknown): v is string => typeof v === "string" && v.trim() !== "";

/* A plan needs a name and a price to be drawn; a CTA needs a label and an
   href; a feature needs a label. Anything short of that is dropped. No
   plans → no section. */
const planList = (plans ?? [])
	.filter((plan) => plan && filled(plan.name) && filled(plan.price))
	.map((plan) => ({
		...plan,
		cta: plan.cta && filled(plan.cta.label) && filled(plan.cta.href) ? plan.cta : null,
		features: (plan.features ?? []).map(feature).filter((f) => filled(f.label)),
	}));
const lines = (highlights ?? []).filter(filled);
---

{planList.length > 0 && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{planList.length > 0 && (
<section class:list={["spp", className]} id={id} style={`--spp-bg: ${background};`} data-spp-root>
	<div class="spp-inner">
		<div class="spp-text" data-spp-appear>
			{
				eyebrow && (
					<p class="spp-eyebrow">
						{eyebrow}
						<span class="spp-eyebrow-line" aria-hidden="true" />
					</p>
				)
			}
			{heading && <Heading class="spp-heading">{heading}</Heading>}
			{text && <p class="spp-lede">{text}</p>}
			{
				lines.length > 0 && (
					<ul class="spp-highlights">
						{lines.map((line) => (
							<li class="spp-highlight">
								<svg class="spp-chevrons" viewBox="0 0 18 14" width="18" height="14" aria-hidden="true" focusable="false">
									<path d="M1.5 1.5 7 7l-5.5 5.5M10 1.5 15.5 7 10 12.5" />
								</svg>
								<span>{line}</span>
							</li>
						))}
					</ul>
				)
			}
		</div>

		<ul class="spp-plans" role="list">
			{
				planList.map((plan, i) => (
					<li
						class:list={["spp-plan", { "spp-plan--featured": plan.featured }]}
						data-spp-appear
						style={`--spp-i: ${i};`}
					>
						<div class="spp-head">
							<div class="spp-price-block">
								<PlanHeading class="spp-name">{plan.name}</PlanHeading>
								<p class="spp-price">
									<span class="spp-amount">{plan.price}</span>
									{plan.period && <span class="spp-period">{plan.period}</span>}
								</p>
							</div>
							{plan.cta && (
								<a class="spp-cta" href={safeHref(plan.cta.href)}>
									{plan.cta.label}
									<span class="spp-sr"> with the {plan.name} plan</span>
									<svg class="spp-cta-icon" viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false">
										<path d="M2 10 10 2M3.5 2H10v6.5" />
									</svg>
								</a>
							)}
						</div>
						<span class="spp-divider" aria-hidden="true" />
						{plan.features.length > 0 && (
							<ul class="spp-features" role="list">
								{plan.features.map((f) => (
									<li class:list={["spp-feature", { "spp-feature--out": !f.included }]}>
										{f.included ? (
											<svg class="spp-mark" viewBox="0 0 16 12" width="14" height="14" aria-hidden="true" focusable="false">
												<path d="M1.5 6.5 5.5 10.5 14.5 1.5" />
											</svg>
										) : (
											<svg class="spp-mark" viewBox="0 0 12 12" width="14" height="14" aria-hidden="true" focusable="false">
												<path d="M1.5 1.5 10.5 10.5M10.5 1.5 1.5 10.5" />
											</svg>
										)}
										<span>
											{!f.included && <span class="spp-sr">Not included: </span>}
											{f.label}
										</span>
									</li>
								))}
							</ul>
						)}
					</li>
				))
			}
		</ul>
	</div>
</section>
)}

<style>
	.spp {
		--spp-navy: var(--tg-navy, #020e28);
		--spp-orange: var(--tg-orange, #fd5523);
		--spp-muted: var(--tg-muted, #61697d);
		--spp-surface: var(--tg-surface, #ecf0f1);
		--spp-dark: #001f3f;
		--spp-display: var(--tg-font-display, Outfit, ui-sans-serif, system-ui, sans-serif);
		--spp-body: var(--tg-font-body, "DM Sans", ui-sans-serif, system-ui, sans-serif);
		--spp-ease: cubic-bezier(0.22, 1, 0.36, 1);

		box-sizing: border-box;
		padding: 60px 16px;
		background: var(--spp-bg);
		color: var(--spp-navy);
		font-family: var(--spp-body);
		overflow: hidden;
	}

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

	.spp-inner {
		display: flex;
		flex-direction: column;
		gap: 36px;
		max-width: 1253px;
		margin: 0 auto;
	}

	/* ---- left column ---- */
	.spp-text {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 16px;
		min-width: 0;
	}

	.spp .spp-eyebrow {
		display: flex;
		align-items: center;
		gap: 12px;
		margin: 0;
		font: 600 16px/1 var(--spp-display);
		text-transform: uppercase;
		color: var(--spp-orange);
	}

	.spp-eyebrow-line {
		display: block;
		width: 40px;
		height: 1px;
		background: currentColor;
	}

	.spp-heading {
		margin: 4px 0 0;
		font: 600 30px/1.2 var(--spp-display);
		text-transform: capitalize;
		color: var(--spp-navy);
	}

	.spp .spp-lede {
		margin: 0;
		font: 400 16px/21.04px var(--spp-body);
		color: var(--spp-muted);
	}

	.spp .spp-highlights {
		display: flex;
		flex-direction: column;
		gap: 16px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.spp-highlight {
		display: flex;
		align-items: flex-start;
		gap: 12px;
		font: 600 18px/1.5 var(--spp-display);
		color: var(--spp-navy);
	}

	.spp-chevrons {
		flex: none;
		margin-top: 6px;
		fill: none;
		stroke: var(--spp-orange);
		stroke-width: 2.6;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	/* ---- plan cards ---- */
	.spp .spp-plans {
		display: flex;
		flex-direction: column;
		gap: 24px;
		margin: 0;
		padding: 0;
		list-style: none;
		min-width: 0;
	}

	.spp-plan {
		display: flex;
		flex-direction: column;
		gap: 24px;
		padding: 24px;
		border: 1px solid #e3e6ea;
		border-radius: 20px;
		background: #fff;
	}

	.spp-plan--featured {
		border-color: var(--spp-dark);
		background: var(--spp-dark);
	}

	.spp-head {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 16px;
		flex: none;
	}

	.spp-name {
		margin: 0;
		font: 600 16px/11.6px var(--spp-display);
		color: var(--spp-navy);
	}

	.spp .spp-price {
		display: flex;
		align-items: flex-end;
		margin: 0;
	}

	.spp-amount {
		font: 600 36px/46px var(--spp-display);
		color: var(--spp-navy);
	}

	.spp-period {
		padding-bottom: 4px;
		font: 400 16px/21.04px var(--spp-body);
		color: var(--spp-muted);
	}

	.spp-plan--featured .spp-name,
	.spp-plan--featured .spp-amount {
		color: #fff;
	}

	.spp-plan--featured .spp-period {
		color: var(--spp-surface);
	}

	/* The pill. Hover as measured: #ecf0f1 → navy, text → white, the arrow
	   inverts and turns 45°. */
	.spp-cta {
		display: inline-flex;
		align-items: center;
		gap: 8px;
		min-height: 47px;
		padding: 0 27px;
		border-radius: 6px;
		background: var(--spp-surface);
		color: var(--spp-navy);
		font: 600 14px/12.46px var(--spp-body);
		text-decoration: none;
		text-transform: capitalize;
		white-space: nowrap;
		transition:
			background-color 0.3s ease-out,
			color 0.3s ease-out;
	}

	.spp-cta-icon {
		flex: none;
		fill: none;
		stroke: var(--spp-navy);
		stroke-width: 1.8;
		stroke-linecap: round;
		stroke-linejoin: round;
		filter: invert(0);
		transition:
			filter 0.3s ease-out,
			transform 0.3s ease-out;
	}

	.spp-cta:hover {
		background: var(--spp-navy);
		color: #fff;
	}

	.spp-cta:hover .spp-cta-icon {
		filter: invert(1);
		transform: rotate(45deg);
	}

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

	.spp-divider {
		display: block;
		height: 1px;
		background: #e3e6ea;
	}

	.spp-plan--featured .spp-divider {
		background: rgb(255 255 255 / 0.85);
	}

	.spp .spp-features {
		display: flex;
		flex-direction: column;
		gap: 12px;
		margin: 0;
		padding: 0;
		list-style: none;
		min-width: 0;
	}

	.spp-feature {
		display: flex;
		align-items: flex-start;
		gap: 16px;
		font: 400 16px/24.6px var(--spp-body);
		color: var(--spp-muted);
	}

	.spp-plan--featured .spp-feature {
		color: #fff;
	}

	.spp-mark {
		flex: none;
		margin-top: 5.5px;
		fill: none;
		stroke: var(--spp-orange);
		stroke-width: 2;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	.spp-feature--out .spp-mark {
		stroke: #7b8394;
	}

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

	/* ---- tablet: cards go horizontal, still stacked under the text ---- */
	@media (min-width: 810px) {
		.spp {
			padding: 100px 30px;
		}

		.spp-inner {
			gap: 48px;
		}

		.spp-text {
			gap: 22px;
		}

		.spp .spp-eyebrow {
			font-size: 18px;
			line-height: 12.6px;
		}

		.spp-heading {
			margin-top: 2px;
			font-size: 40px;
			line-height: 1.2;
		}

		.spp-highlight {
			font-size: 20px;
			line-height: 30px;
		}

		.spp-chevrons {
			margin-top: 7px;
		}

		.spp .spp-plans {
			gap: 30px;
		}

		.spp-plan {
			flex-direction: row;
			align-items: center;
			gap: 40px;
			padding: 40px;
			border-radius: 30px;
		}

		.spp-head {
			width: 190px;
		}

		/* Measured as a 1px-wide, zero-height line wrap: nothing shows at
		   desktop, but it adds a second 40px gap before the list. */
		.spp-divider {
			flex: none;
			width: 1px;
			height: 0;
		}

		.spp-feature {
			font-size: 18px;
		}
	}

	/* ---- desktop: text left (533), cards right (639) ---- */
	@media (min-width: 1200px) {
		.spp {
			padding: 120px 20px;
		}

		.spp-inner {
			flex-direction: row;
			justify-content: space-between;
			align-items: flex-start;
			gap: 40px;
		}

		.spp-text {
			flex: 0 1 533px;
		}

		.spp-heading {
			font-size: 48px;
			line-height: 57.6px;
		}

		.spp .spp-plans {
			flex: 0 1 639px;
		}

		.spp-head {
			width: 150px;
		}
	}

	/* ---- appear on scroll: start pose only once the script is ready ---- */
	[data-spp-appear] {
		transition:
			opacity 0.8s var(--spp-ease),
			transform 0.8s var(--spp-ease);
		transition-delay: calc(var(--spp-i, 0) * 0.1s);
	}

	.spp[data-spp-ready]:not([data-in]) [data-spp-appear] {
		opacity: 0;
		transform: translateY(40px);
	}

	@media (prefers-reduced-motion: reduce) {
		[data-spp-appear] {
			transition: none;
		}

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

		.spp-cta:hover .spp-cta-icon {
			transform: none;
		}
	}

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

		.spp-cta-icon,
		.spp-mark,
		.spp-chevrons {
			stroke: CanvasText;
		}
	}
</style>

<script>
	/*
	 * Appear-on-scroll for StackedPricingPlans. One IntersectionObserver per
	 * instance, scoped to its root, torn down on astro:before-swap. Under
	 * reduced motion (or with no IntersectionObserver) it marks the root in
	 * straight away, so nothing ever starts hidden.
	 */
	function initStackedPricingPlans(root: HTMLElement) {
		if (root.dataset.sppReady === "") return;

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

		root.dataset.sppReady = "";
		if (reduced || !("IntersectionObserver" in window)) {
			root.setAttribute("data-in", "");
		} else {
			const observer = new IntersectionObserver(
				(entries) => {
					if (entries.some((entry) => entry.isIntersecting)) {
						root.setAttribute("data-in", "");
						observer.disconnect();
					}
				},
				{ threshold: 0.15 },
			);
			observer.observe(root);
			signal.addEventListener("abort", () => observer.disconnect());
		}

		document.addEventListener(
			"astro:before-swap",
			() => {
				controller.abort();
				root.removeAttribute("data-in");
				delete root.dataset.sppReady;
			},
			{ once: true, signal },
		);
	}

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

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

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

===== END src/components/StackedPricingPlans.astro =====

===== FILE (required): src/components/LogisticsBlogCards.astro =====
---
/**
 * LogisticsBlogCards
 *
 * A light grey section with a faint drawn herringbone pattern: centred
 * eyebrow and heading, then three white post cards. Each card is one link:
 * a rounded photo, a category tag, the title, and an author row (initials
 * or photo, name, date) with a pink arrow tile at the right.
 *
 * Hover, as measured: the photo scales to 1.1 inside its frame and the
 * arrow tile turns from #ffe0e0 to orange. The title block and the cards
 * rise 40px into place, staggered, when the section scrolls in.
 *
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

export interface BlogCardPost {
	title: string;
	href: string;
	tag?: string;
	/** Photo. `src: ""` renders the labelled placeholder instead. */
	image?: { src?: string; alt?: string };
	author?: {
		name: string;
		/** Optional line before the date, e.g. "Founder". */
		role?: string;
		/** Small round photo. Without one the author's initials are drawn. */
		avatar?: { src: string; alt?: string };
	};
	/** ISO date, e.g. "2026-08-14". Rendered in a <time>. */
	date?: string;
}

export interface LogisticsBlogCardsProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	posts?: BlogCardPost[];
	/** Locale for the visible date. */
	locale?: string;
	/** Draw the faint herringbone pattern behind the section. */
	pattern?: boolean;
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends LogisticsBlogCardsProps {}

const {
	eyebrow = "Blog & news",
	heading = "",
	headingLevel = "h2",
	posts: postsProp = [],
	locale = "en-US",
	pattern = true,
	background = "#ecf0f1",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

/* A post needs a title and a link; anything less is dropped, never padded.
   An author with no name is treated as no author. */
const posts = postsProp
	.filter((post) => post?.title && post.href)
	.map((post) => (post.author && !post.author.name ? { ...post, author: undefined } : post));

const Heading = headingLevel;
const CardHeading = headingLevel === "h2" ? "h3" : "h4";
const uid = id ?? `lbc-${Math.random().toString(36).slice(2, 9)}`;

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

const initials = (name: string) =>
	name
		.split(/\s+/)
		.filter(Boolean)
		.slice(0, 2)
		.map((part) => part[0]!.toUpperCase())
		.join("");

function formatDate(iso: string) {
	const d = new Date(`${iso}T12:00:00Z`);
	if (Number.isNaN(d.getTime())) return iso;
	return new Intl.DateTimeFormat(locale, {
		month: "short",
		day: "numeric",
		year: "numeric",
		timeZone: "UTC",
	}).format(d);
}

/* Measured slot: 388 × 248 at desktop; the files are 776 × 496. */
const IMAGE_SPEC = "~1.56:1 · 776 × 496 px";
---

{posts.length > 0 && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{posts.length > 0 && (
<section
	class:list={["lbc", className]}
	id={id}
	style={`--lbc-bg: ${background};`}
	data-pattern={pattern ? "" : undefined}
	data-lbc-root
>
	<div class="lbc-inner">
		{(eyebrow || heading) && (
		<div class="lbc-title" data-lbc-appear>
			{
				eyebrow && (
					<p class="lbc-eyebrow">
						<span class="lbc-eyebrow-line" aria-hidden="true" />
						{eyebrow}
						<span class="lbc-eyebrow-line" aria-hidden="true" />
					</p>
				)
			}
			{heading && <Heading class="lbc-heading">{heading}</Heading>}
		</div>
		)}

		<ul class="lbc-grid" role="list">
			{
				posts.map((post, i) => {
					const image = { src: "", alt: "", ...(post.image ?? {}) };
					const titleId = `${uid}-title-${i + 1}`;
					const metaId = `${uid}-meta-${i + 1}`;
					return (
						<li class="lbc-item" data-lbc-appear style={`--lbc-i: ${i + 1};`}>
							<a
								class="lbc-card"
								href={safeHref(post.href)}
								aria-labelledby={titleId}
								aria-describedby={post.author || post.date ? metaId : undefined}
							>
								<div class="lbc-media">
									{image.src ? (
										<img
											src={image.src}
											alt={image.alt}
											width="776"
											height="496"
											loading="lazy"
											decoding="async"
										/>
									) : (
										<div class="lbc-ph" role="img" aria-label={`Image placeholder: ${post.title}`}>
											<div class="lbc-ph-text" aria-hidden="true">
												<span class="lbc-ph-label">Image</span>
												<span class="lbc-ph-hint">Post photo, landscape</span>
												<span class="lbc-ph-spec">{IMAGE_SPEC}</span>
											</div>
										</div>
									)}
								</div>
								<div class="lbc-body">
									{post.tag && <span class="lbc-tag">{post.tag}</span>}
									<CardHeading class="lbc-card-title" id={titleId}>
										{post.title}
									</CardHeading>
								</div>
								<div class="lbc-foot">
									{(post.author || post.date) && (
										<div class="lbc-meta" id={metaId}>
											{post.author &&
												(post.author.avatar?.src ? (
													<img
														class="lbc-avatar"
														src={post.author.avatar.src}
														alt={post.author.avatar.alt ?? ""}
														width="44"
														height="44"
														loading="lazy"
														decoding="async"
													/>
												) : (
													<span class="lbc-avatar lbc-avatar--initials" aria-hidden="true">
														{initials(post.author.name)}
													</span>
												))}
											<span class="lbc-meta-text">
												{post.author && <span class="lbc-author">{post.author.name}</span>}
												{(post.author?.role || post.date) && (
													<span class="lbc-date">
														{post.author?.role && <span>{post.author.role}{post.date && " · "}</span>}
														{post.date && <time datetime={post.date}>{formatDate(post.date)}</time>}
													</span>
												)}
											</span>
										</div>
									)}
									<span class="lbc-tile" aria-hidden="true">
										<svg viewBox="0 0 24 24" width="24" height="24" focusable="false">
											<path d="M4 12h15M13 6l6 6-6 6" />
										</svg>
									</span>
								</div>
							</a>
						</li>
					);
				})
			}
		</ul>
	</div>
</section>
)}

<style>
	.lbc {
		--lbc-navy: var(--tg-navy, #020e28);
		--lbc-orange: var(--tg-orange, #fd5523);
		--lbc-muted: var(--tg-muted, #61697d);
		--lbc-title-ink: #0e2d3a;
		--lbc-display: var(--tg-font-display, Outfit, ui-sans-serif, system-ui, sans-serif);
		--lbc-body: var(--tg-font-body, "DM Sans", ui-sans-serif, system-ui, sans-serif);
		--lbc-ease: cubic-bezier(0.22, 1, 0.36, 1);

		position: relative;
		box-sizing: border-box;
		padding: 60px 16px;
		background-color: var(--lbc-bg);
		color: var(--lbc-navy);
		font-family: var(--lbc-body);
		overflow: hidden;
	}

	/* A faint herringbone drawn in SVG: pale zig-zag strokes, lighter and
	   darker, on the grey surface. Replaces the reference's texture image. */
	.lbc[data-pattern] {
		background-image:
			url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='48' viewBox='0 0 96 48'%3E%3Cg fill='none' stroke-width='1.2'%3E%3Cpath d='M0 24 24 0 48 24 72 0 96 24M0 48 24 24 48 48 72 24 96 48' stroke='%23ffffff' stroke-opacity='.6'/%3E%3Cpath d='M0 36 24 12 48 36 72 12 96 36' stroke='%23020e28' stroke-opacity='.035'/%3E%3C/g%3E%3C/svg%3E");
		background-size: 140px 70px;
	}

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

	.lbc-inner {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: 32px;
		max-width: 1224px;
		margin: 0 auto;
	}

	.lbc-title {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 16px;
	}

	.lbc .lbc-eyebrow {
		display: flex;
		align-items: center;
		gap: 12px;
		margin: 0;
		font: 600 16px/1 var(--lbc-display);
		text-transform: uppercase;
		color: var(--lbc-orange);
	}

	.lbc-eyebrow-line {
		display: none;
		width: 40px;
		height: 1px;
		background: currentColor;
	}

	.lbc-heading {
		margin: 0;
		font: 600 30px/1.2 var(--lbc-display);
		text-transform: capitalize;
		color: var(--lbc-navy);
	}

	.lbc .lbc-grid {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: 24px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.lbc-item {
		display: flex;
		min-width: 0;
	}

	.lbc-card {
		display: flex;
		flex-direction: column;
		width: 100%;
		border-radius: 20px;
		background: #fff;
		color: inherit;
		text-decoration: none;
	}

	.lbc-card:focus-visible {
		outline: 3px solid var(--lbc-orange);
		outline-offset: 4px;
	}

	.lbc-media {
		position: relative;
		overflow: hidden;
		aspect-ratio: 388 / 248;
		border-radius: 20px;
		background: #dfe4e6;
	}

	.lbc-media img {
		position: absolute;
		inset: 0;
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
		transition: transform 0.4s ease-out;
	}

	.lbc-body {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 12px;
		padding: 24px 18px 20px;
		flex: 1;
	}

	.lbc-tag {
		display: inline-flex;
		padding: 2px 16px;
		border: 1px solid #ffd9cc;
		border-radius: 100px;
		font: 400 16px/21.04px var(--lbc-body);
		color: var(--lbc-orange);
	}

	.lbc-card-title {
		margin: 0;
		font: 600 20px/1.42 var(--lbc-display);
		color: var(--lbc-title-ink);
	}

	.lbc-foot {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 12px;
		padding: 0 18px 20px;
	}

	.lbc-meta {
		display: flex;
		align-items: center;
		gap: 10px;
		min-width: 0;
	}

	.lbc-avatar {
		flex: none;
		width: 44px;
		height: 44px;
		border-radius: 50%;
		object-fit: cover;
	}

	.lbc-avatar--initials {
		display: grid;
		place-items: center;
		background: var(--lbc-navy);
		color: #fff;
		font: 600 15px/1 var(--lbc-display);
		letter-spacing: 0.02em;
	}

	.lbc-meta-text {
		display: flex;
		flex-direction: column;
		gap: 6px;
		min-width: 0;
	}

	.lbc-author {
		font: 400 16px/21.04px var(--lbc-body);
		color: var(--lbc-navy);
	}

	.lbc-date {
		font: 400 14px/1 var(--lbc-body);
		color: var(--lbc-muted);
	}

	/* The arrow tile. Hover: #ffe0e0 → orange, as measured. */
	.lbc-tile {
		display: grid;
		place-items: center;
		flex: none;
		width: 46px;
		height: 48px;
		border-radius: 10px;
		background: #ffe0e0;
		color: var(--lbc-orange);
		transition:
			background-color 0.3s ease-out,
			color 0.3s ease-out;
	}

	.lbc-tile svg {
		display: block;
		fill: none;
		stroke: currentColor;
		stroke-width: 1.8;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	.lbc-card:hover .lbc-media img,
	.lbc-card:focus-visible .lbc-media img {
		transform: scale(1.1);
	}

	.lbc-card:hover .lbc-tile,
	.lbc-card:focus-visible .lbc-tile {
		background: var(--lbc-orange);
		color: #fff;
	}

	/* placeholder for `src: ""` */
	.lbc-ph {
		position: absolute;
		inset: 0;
		display: grid;
		place-items: center;
		padding: 16px;
		text-align: center;
		border: 1px dashed rgb(2 14 40 / 0.22);
		border-radius: inherit;
		background: repeating-linear-gradient(135deg, #e6e9ee 0 12px, #edf0f3 12px 24px);
		color: rgb(2 14 40 / 0.62);
	}

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

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

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

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

	/* ---- tablet: centred title, two columns ---- */
	@media (min-width: 810px) {
		.lbc {
			padding: 100px 30px;
		}

		.lbc-inner {
			gap: 60px;
		}

		.lbc-title {
			align-items: center;
			text-align: center;
		}

		.lbc .lbc-eyebrow {
			font-size: 18px;
			line-height: 12.6px;
		}

		.lbc-eyebrow-line {
			display: block;
		}

		.lbc-heading {
			font-size: 40px;
		}

		.lbc .lbc-grid {
			grid-template-columns: repeat(2, minmax(0, 1fr));
			gap: 30px;
		}

		.lbc-body {
			padding: 30px 30px 20px;
		}

		.lbc-foot {
			padding: 0 30px 30px;
		}

		.lbc-card-title {
			font-size: 24px;
			line-height: 34px;
		}
	}

	/* ---- desktop: three 388px cards ---- */
	@media (min-width: 1200px) {
		.lbc {
			padding: 120px 20px;
		}

		.lbc-heading {
			font-size: 48px;
			line-height: 57.6px;
		}

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

	/* ---- appear on scroll: start pose only once the script is ready ---- */
	[data-lbc-appear] {
		transition:
			opacity 0.8s var(--lbc-ease),
			transform 0.8s var(--lbc-ease);
		transition-delay: calc(var(--lbc-i, 0) * 0.1s);
	}

	.lbc[data-lbc-ready]:not([data-in]) [data-lbc-appear] {
		opacity: 0;
		transform: translateY(40px);
	}

	@media (prefers-reduced-motion: reduce) {
		[data-lbc-appear] {
			transition: none;
		}

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

		.lbc-card:hover .lbc-media img,
		.lbc-card:focus-visible .lbc-media img {
			transform: none;
		}
	}

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

<script>
	/*
	 * Appear-on-scroll for LogisticsBlogCards. One IntersectionObserver per
	 * instance, scoped to its root, torn down on astro:before-swap. Under
	 * reduced motion (or with no IntersectionObserver) it marks the root in
	 * straight away, so nothing ever starts hidden.
	 */
	function initLogisticsBlogCards(root: HTMLElement) {
		if (root.dataset.lbcReady === "") return;

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

		root.dataset.lbcReady = "";
		if (reduced || !("IntersectionObserver" in window)) {
			root.setAttribute("data-in", "");
		} else {
			const observer = new IntersectionObserver(
				(entries) => {
					if (entries.some((entry) => entry.isIntersecting)) {
						root.setAttribute("data-in", "");
						observer.disconnect();
					}
				},
				{ threshold: 0.15 },
			);
			observer.observe(root);
			signal.addEventListener("abort", () => observer.disconnect());
		}

		document.addEventListener(
			"astro:before-swap",
			() => {
				controller.abort();
				root.removeAttribute("data-in");
				delete root.dataset.lbcReady;
			},
			{ once: true, signal },
		);
	}

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

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

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

===== END src/components/LogisticsBlogCards.astro =====

===== FILE (required): public/logistics-blog-cards/post-1.webp =====
[binary asset — download https://astro.baysixmedia.com/logistics-blog-cards/post-1.webp and save it to public/logistics-blog-cards/post-1.webp]

===== FILE (required): public/logistics-blog-cards/post-2.webp =====
[binary asset — download https://astro.baysixmedia.com/logistics-blog-cards/post-2.webp and save it to public/logistics-blog-cards/post-2.webp]

===== FILE (required): public/logistics-blog-cards/post-3.webp =====
[binary asset — download https://astro.baysixmedia.com/logistics-blog-cards/post-3.webp and save it to public/logistics-blog-cards/post-3.webp]

===== FILE (required): src/components/NewsletterPanelFooter.astro =====
---
/**
 * NewsletterPanelFooter
 *
 * A navy footer in two parts. On top, "Subscribe Our Newsletter" beside an
 * email field with an orange Subscribe button (a real form). Below, the
 * brand block — logo, a short blurb, four social circles — and three
 * columns: Quick Links and Our Services (links with » bullets) and Contact
 * Us (mail and phone). Headings carry a short white-and-orange underline.
 *
 * Two measured layouts:
 * - `variant="panel"` (default, the reference's inner pages): the brand and
 *   columns sit in a rounded panel with a slightly lighter fill and a
 *   gradient hairline, and a centred copyright line closes the footer.
 * - `variant="band"` (the reference's home page): a hairline divides the
 *   newsletter row from the columns, and the copyright sits on an orange band.
 *
 * Nothing about the business is assumed: no name, blurb, link, contact,
 * social profile, copyright or sign-up form is shown unless it is passed, a
 * block with nothing in it is not drawn, and with no content at all nothing
 * renders.
 *
 * The top content rises 40px and fades in once when scrolled into view.
 * Reads --tg-* design tokens from any ancestor, with fallbacks.
 */

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

export interface NewsletterPanelFooterColumn {
	heading: string;
	links: NewsletterPanelFooterLink[];
}

export interface NewsletterPanelFooterContact {
	icon: "email" | "phone" | "address";
	label: string;
	href: string;
}

export type NewsletterPanelFooterNetwork = "facebook" | "x" | "instagram" | "linkedin" | "youtube" | "link";

export interface NewsletterPanelFooterSocial {
	network: NewsletterPanelFooterNetwork;
	href: string;
	/** Accessible name, e.g. "Northpass on LinkedIn". */
	label: string;
}

export interface NewsletterPanelFooterProps {
	variant?: "panel" | "band";
	/** The newsletter heading. Unset together with no `newsletter` drops the row. */
	heading?: string;
	/** The subscribe form. Off by default; `{}` shows it with no `action` (posts to the current URL). */
	newsletter?: {
		action?: string;
		method?: "get" | "post";
		/** Field name as submitted. */
		name?: string;
		label?: string;
		placeholder?: string;
		buttonLabel?: string;
	} | null;
	brandName?: string;
	brandHref?: string;
	/** Replaces the drawn mark and wordmark with an image (use a light logo). */
	logo?: { src: string; alt: string; width?: number; height?: number };
	blurb?: string;
	social?: NewsletterPanelFooterSocial[];
	columns?: NewsletterPanelFooterColumn[];
	contactHeading?: string;
	contacts?: NewsletterPanelFooterContact[];
	/** Text of the copyright line. `{year}` is replaced with the build year. Defaults to one naming `brandName`; with neither, no line. */
	copyright?: string;
	/** Makes the copyright line a link. */
	copyrightHref?: string;
	year?: number;
	/** Scroll-in rise for the top content. */
	animate?: boolean;
	navLabel?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends NewsletterPanelFooterProps {}

const {
	variant = "panel",
	heading = "",
	newsletter = null,
	brandName = "",
	brandHref = "/",
	logo,
	blurb = "",
	social = [],
	columns = [],
	contactHeading = "Contact Us",
	contacts = [],
	copyright,
	copyrightHref,
	year = new Date().getFullYear(),
	animate = true,
	navLabel = "Footer",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=DM+Sans:wght@400;500;600&display=swap",
	id,
} = Astro.props;

const seed = id ? id : `npf-${Math.random().toString(36).slice(2, 8)}`;
const emailId = `${seed}-email`;
const form = newsletter
	? {
			method: "post" as const,
			name: "email",
			label: "Email address",
			placeholder: "Email Address",
			buttonLabel: "Subscribe",
			...newsletter,
		}
	: null;
const hasTop = Boolean(heading || form);
const hasMark = Boolean(logo || brandName);
const hasBrand = Boolean(hasMark || blurb || social.length > 0);
const hasCols = columns.length > 0 || contacts.length > 0;
const hasPanel = hasBrand || hasCols;
/* No name, no copyright line: never a line naming nobody. */
const copyText = (copyright ?? (brandName ? `Copyright © {year} ${brandName}. All Rights Reserved.` : "")).replace("{year}", String(year));
const hasContent = hasTop || hasPanel || Boolean(copyText);
const isExternal = (href: string) => /^https?:\/\//.test(href);

const CONTACT_ICONS: Record<NewsletterPanelFooterContact["icon"], string> = {
	email:
		'<path d="M1 4.2A1.7 1.7 0 0 1 2.7 2.5h10.6A1.7 1.7 0 0 1 15 4.2v.3L8 9 1 4.5z" fill="currentColor"/><path d="M1 6.3l6.5 4.2a1 1 0 0 0 1 0L15 6.3v5.5a1.7 1.7 0 0 1-1.7 1.7H2.7A1.7 1.7 0 0 1 1 11.8z" fill="currentColor"/>',
	phone:
		'<path d="M4.2 1.5 6.3 4a1 1 0 0 1-.1 1.3L5 6.5a8.4 8.4 0 0 0 4.5 4.5l1.2-1.2a1 1 0 0 1 1.3-.1l2.5 2.1a1 1 0 0 1 .1 1.4l-1 1.1c-.7.7-1.7.9-2.6.6A14 14 0 0 1 1.1 4.9c-.3-.9-.1-1.9.6-2.6l1-1a1 1 0 0 1 1.5.2z" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M9.6 1.6a5 5 0 0 1 4.8 4.8M9.4 4a2.6 2.6 0 0 1 2.6 2.6" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>',
	address:
		'<path d="M8 .8a5.6 5.6 0 0 0-5.6 5.6c0 4.1 5 8.5 5.2 8.7a.6.6 0 0 0 .8 0c.2-.2 5.2-4.6 5.2-8.7A5.6 5.6 0 0 0 8 .8zm0 7.9a2.3 2.3 0 1 1 0-4.6 2.3 2.3 0 0 1 0 4.6z" fill="currentColor"/>',
};

const GLYPHS: Record<NewsletterPanelFooterNetwork, string> = {
	facebook:
		'<path d="M13.6 21v-7.6h2.6l.4-3.1h-3V8.4c0-.9.3-1.5 1.5-1.5h1.6V4.1c-.3 0-1.2-.1-2.3-.1-2.3 0-3.9 1.4-3.9 4v2.3H7.9v3.1h2.6V21z" fill="currentColor"/>',
	x: '<path d="M4 4h4.6l11.4 16h-4.6z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M19.4 4l-6.2 7M10.8 13l-6.2 7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>',
	instagram:
		'<rect x="4" y="4" width="16" height="16" rx="4.6" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="3.7" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="16.6" cy="7.4" r="1.1" fill="currentColor"/>',
	linkedin:
		'<rect x="4.2" y="9.2" width="3.3" height="10.6" fill="currentColor"/><circle cx="5.85" cy="5.9" r="1.95" fill="currentColor"/><path d="M10 9.2h3.2v1.5c.5-.9 1.7-1.8 3.5-1.8 3.4 0 4 2.2 4 5.1v5.8h-3.3v-5.1c0-1.3 0-2.9-1.8-2.9s-2.1 1.4-2.1 2.8v5.2H10z" fill="currentColor"/>',
	youtube:
		'<rect x="3" y="6" width="18" height="12.5" rx="3.6" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M10.4 9.4v5.7l4.9-2.85z" fill="currentColor"/>',
	link: '<path d="M10 14l4-4M8.5 11.5 6.6 13.4a3.2 3.2 0 0 0 4.5 4.5l1.9-1.9M15.5 12.5l1.9-1.9a3.2 3.2 0 0 0-4.5-4.5L11 8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>',
};
---

{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<footer
	class:list={["npf", `npf--${variant}`]}
	id={id}
	data-npf-root
	data-animate={animate ? "" : undefined}
>
	{(hasTop || hasPanel) && (
	<div class="npf-main">
		{
			hasTop && (
				<div class="npf-top npf-rise" style="--npf-i: 0">
					{heading && <h2 class="npf-heading">{heading}</h2>}
					{form && (
						<form class="npf-form" action={form.action} method={form.method}>
							<label class="npf-sr" for={emailId}>{form.label}</label>
							<div class="npf-field">
								<input
									class="npf-input"
									id={emailId}
									type="email"
									name={form.name}
									placeholder={form.placeholder}
									autocomplete="email"
									required
								/>
								<button class="npf-submit" type="submit">
									<span>{form.buttonLabel}</span>
									<svg class="npf-plane" viewBox="0 0 22 21" aria-hidden="true" focusable="false">
										<path d="M20.6 1.2 1.6 8.6c-.8.3-.8 1.4 0 1.7l6 2.2 2.2 6.3c.3.8 1.4.9 1.8.1L21.9 2.5c.4-.8-.4-1.6-1.3-1.3zM9.1 12.2l7.7-6.9-6 8.1-.3 3.2z" fill="currentColor" />
									</svg>
								</button>
							</div>
						</form>
					)}
				</div>
			)
		}

		{variant === "band" && hasTop && hasPanel && <div class="npf-rule" aria-hidden="true" />}

		{hasPanel && (
		<div class="npf-panel">
			<div class="npf-links npf-rise" style="--npf-i: 1">
				{hasBrand && (
				<div class="npf-brandblock">
					{hasMark && (
					<a class="npf-brand" href={brandHref} aria-label={logo ? undefined : `${brandName} home`}>
						{
							logo ? (
								<img
									class="npf-logo-img"
									src={logo.src}
									alt={logo.alt}
									width={logo.width ?? 157}
									height={logo.height ?? 30}
									loading="lazy"
								/>
							) : (
								<>
									<svg class="npf-mark" viewBox="0 0 46 30" aria-hidden="true" focusable="false">
										<path d="M2 9h11M5 14h9M1 19h13" fill="none" stroke="var(--npf-orange)" stroke-width="2.6" stroke-linecap="round" />
										<rect x="14" y="4" width="17" height="15" rx="2" fill="currentColor" />
										<path d="M32 8h6.2l5.3 5.6V19H32z" fill="currentColor" />
										<path d="M34.4 10.4h3.2l3 3.2h-6.2z" fill="var(--npf-orange)" />
										<circle cx="20" cy="21.5" r="3.4" fill="currentColor" stroke="var(--npf-navy)" stroke-width="1.6" />
										<circle cx="37.5" cy="21.5" r="3.4" fill="currentColor" stroke="var(--npf-navy)" stroke-width="1.6" />
										<path d="M4 27.5h40" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
									</svg>
									<span class="npf-wordmark" aria-hidden="true">{brandName}</span>
								</>
							)
						}
					</a>
					)}
					{blurb && <p class="npf-blurb">{blurb}</p>}
					{
						social.length > 0 && (
							<ul class="npf-social" role="list">
								{social.map((s) => (
									<li>
										<a
											class="npf-social-link"
											href={s.href}
											aria-label={s.label}
											target={isExternal(s.href) ? "_blank" : undefined}
											rel={isExternal(s.href) ? "noopener noreferrer" : undefined}
										>
											<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" set:html={GLYPHS[s.network] ?? GLYPHS.link} />
										</a>
									</li>
								))}
							</ul>
						)
					}
				</div>
				)}

				{hasCols && (
				<nav class="npf-cols" aria-label={navLabel}>
					{
						columns.map((col) => (
							<div class="npf-col">
								<h3 class="npf-col-heading">{col.heading}</h3>
								<span class="npf-underline" aria-hidden="true"><span /><span /></span>
								<ul class="npf-list" role="list">
									{col.links.map((link) => (
										<li>
											<a class="npf-link" href={link.href}>
												<svg class="npf-bullet" viewBox="0 0 18 18" aria-hidden="true" focusable="false">
													<path d="M4.5 5l4 4-4 4M9.5 5l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" />
												</svg>
												<span>{link.label}</span>
											</a>
										</li>
									))}
								</ul>
							</div>
						))
					}
					{
						contacts.length > 0 && (
							<div class="npf-col npf-col--contact">
								<h3 class="npf-col-heading">{contactHeading}</h3>
								<span class="npf-underline" aria-hidden="true"><span /><span /></span>
								<ul class="npf-list npf-list--contact" role="list">
									{contacts.map((c) => (
										<li>
											<a class="npf-link" href={c.href}>
												<svg class="npf-contact-icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false" set:html={CONTACT_ICONS[c.icon]} />
												<span>{c.label}</span>
											</a>
										</li>
									))}
								</ul>
							</div>
						)
					}
				</nav>
				)}
			</div>
		</div>
		)}
	</div>
	)}

	{copyText && (
	<div class="npf-bottom">
		<p class="npf-copy">
			{
				copyrightHref ? (
					<a class="npf-copy-link" href={copyrightHref}>
						{copyText}
					</a>
				) : (
					copyText
				)
			}
		</p>
	</div>
	)}
</footer>
)}

<style>
	.npf {
		--npf-navy: var(--tg-navy, #020e28);
		--npf-orange: var(--tg-orange, #fd5523);
		--npf-hover: #0e2d3a;
		--npf-soft: #b0b2b8;
		--npf-display: var(--tg-font-display, Outfit, ui-sans-serif, system-ui, sans-serif);
		--npf-body: var(--tg-font-body, "DM Sans", ui-sans-serif, system-ui, sans-serif);
		--npf-dur: 0.4s;
		--npf-ease: cubic-bezier(0.44, 0, 0.56, 1);

		box-sizing: border-box;
		width: 100%;
		background: var(--npf-navy);
		color: #fff;
		font-family: var(--npf-body);
		overflow: hidden;
	}

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

	.npf ul,
	.npf p,
	.npf h2,
	.npf h3 {
		margin: 0;
	}

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

	.npf-main {
		display: flex;
		flex-direction: column;
		gap: 48px;
		padding: 60px 20px 40px;
	}

	/* ---- newsletter row ---- */
	.npf-top {
		display: flex;
		flex-direction: column;
		gap: 30px;
		width: 100%;
		max-width: 1224px;
		margin: 0 auto;
	}

	.npf-heading {
		max-width: 420px;
		font: 600 32px/1.2 var(--npf-display);
		color: #fff;
		text-transform: capitalize;
	}

	.npf-form {
		width: 100%;
	}

	.npf-field {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: 20px;
	}

	.npf-input {
		width: 100%;
		height: 48px;
		padding: 0 25px;
		border: 1px solid rgb(255 255 255 / 0.18);
		border-radius: 50px;
		background: rgb(255 255 255 / 0.07);
		color: #fff;
		font: 400 15px/1 var(--npf-body);
		transition:
			background-color var(--npf-dur) var(--npf-ease),
			border-color var(--npf-dur) var(--npf-ease),
			box-shadow var(--npf-dur) var(--npf-ease);
	}

	.npf-input::placeholder {
		color: #d4d6db;
		opacity: 1;
	}

	.npf-input:focus {
		outline: none;
		border-color: var(--npf-orange);
		background: rgb(255 255 255 / 0.1);
		box-shadow: 0 0 0 3px rgb(253 85 35 / 0.35);
	}

	.npf-submit {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: 10px;
		height: 48px;
		padding: 0 19px 0 25px;
		border: 0;
		border-radius: 50px;
		background: var(--npf-orange);
		color: #fff;
		font: 600 16px/1 var(--npf-display);
		cursor: pointer;
		transition: background-color var(--npf-dur) var(--npf-ease);
	}

	.npf-submit:hover {
		background: var(--npf-hover);
	}

	.npf-plane {
		width: 20px;
		height: 20px;
		flex: none;
	}

	.npf-rule {
		width: 100%;
		max-width: 1224px;
		height: 1px;
		margin: 0 auto;
		background: rgb(255 255 255 / 0.2);
	}

	/* ---- brand block and columns ---- */
	.npf-links {
		display: flex;
		flex-direction: column;
		gap: 36px;
		width: 100%;
		max-width: 1224px;
		margin: 0 auto;
	}

	.npf-brandblock {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 24px;
		max-width: 320px;
	}

	.npf-brand {
		display: inline-flex;
		align-items: center;
		gap: 9px;
		min-height: 44px;
		color: #fff;
		text-decoration: none;
		border-radius: 6px;
	}

	.npf-mark {
		width: 46px;
		height: 30px;
		flex: none;
	}

	.npf-wordmark {
		font: 600 24px/1 var(--npf-display);
		letter-spacing: -0.01em;
	}

	.npf-logo-img {
		display: block;
		height: 30px;
		width: auto;
	}

	.npf-blurb {
		color: var(--npf-soft);
		font: 400 16px/21px var(--npf-body);
	}

	.npf .npf-social {
		display: flex;
		align-items: center;
		gap: 10px;
	}

	.npf-social-link {
		position: relative;
		display: grid;
		place-items: center;
		width: 36px;
		height: 36px;
		border-radius: 100px;
		background: rgb(255 255 255 / 0.07);
		color: #fff;
		transition: background-color var(--npf-dur) var(--npf-ease);
	}

	/* 36px circle as measured, 44px target. */
	.npf-social-link::after {
		content: "";
		position: absolute;
		inset: -4px;
		border-radius: inherit;
	}

	.npf-social-link svg {
		width: 15px;
		height: 15px;
	}

	.npf-social-link:hover {
		background: var(--npf-orange);
	}

	.npf-cols {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: 36px;
	}

	.npf-col-heading {
		font: 700 24px/1 var(--npf-display);
		color: #fff;
	}

	.npf-underline {
		display: flex;
		gap: 6px;
		margin: 18px 0 20px;
	}

	.npf-underline span {
		display: block;
		width: 18px;
		height: 2px;
		background: #fff;
	}

	.npf-underline span + span {
		width: 83px;
		background: var(--npf-orange);
	}

	.npf .npf-list {
		display: flex;
		flex-direction: column;
		gap: 0;
	}

	.npf-link {
		display: inline-flex;
		align-items: center;
		gap: 8px;
		min-height: 44px;
		color: var(--npf-soft);
		font: 400 16px/21px var(--npf-body);
		text-decoration: none;
		border-radius: 4px;
		transition: color var(--npf-dur) var(--npf-ease);
	}

	.npf-link:hover {
		color: var(--npf-orange);
	}

	.npf-bullet {
		width: 18px;
		height: 18px;
		flex: none;
	}

	.npf-contact-icon {
		width: 16px;
		height: 16px;
		flex: none;
	}

	.npf-list--contact .npf-link {
		overflow-wrap: anywhere;
	}

	/* ---- the panel ---- */
	.npf--panel .npf-panel {
		position: relative;
		width: 100%;
		max-width: 1385px;
		margin: 0 auto;
		padding: 30px 16px;
		border-radius: 20px;
		/* A navy fill with a 10% white wash inside a 2px gradient hairline,
		   as measured: border-box gradient under a padding-box fill. */
		border: 2px solid transparent;
		background:
			linear-gradient(107deg, rgb(255 255 255 / 0.1), rgb(255 255 255 / 0.1)) padding-box,
			linear-gradient(var(--npf-navy), var(--npf-navy)) padding-box,
			linear-gradient(107deg, rgb(255 255 255 / 0.2), rgb(255 255 255 / 0.5)) border-box;
	}

	/* ---- copyright ---- */
	.npf-bottom {
		padding: 0 20px 40px;
		text-align: center;
	}

	.npf-copy {
		color: #fff;
		font: 400 16px/21px var(--npf-body);
	}

	.npf-copy-link {
		color: inherit;
		text-decoration: none;
		border-radius: 4px;
		transition: color var(--npf-dur) var(--npf-ease);
	}

	.npf-copy-link:hover {
		color: var(--npf-orange);
	}

	.npf--band .npf-main {
		padding-bottom: 60px;
	}

	.npf--band .npf-bottom {
		padding: 30px 20px;
		background: var(--npf-orange);
	}

	/* Orange on orange would vanish: on the band the link turns navy. */
	.npf--band .npf-copy-link:hover {
		color: var(--npf-navy);
	}

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

	.npf--band .npf-copy-link:focus-visible,
	.npf-submit:focus-visible {
		outline-color: #fff;
	}

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

	/* ---- appear on scroll ----
	   The hidden start pose applies only once the script has marked the root
	   ready, so without JS or under reduced motion everything is at rest. */
	.npf-rise {
		transition:
			opacity 0.8s var(--npf-ease) calc(var(--npf-i, 0) * 0.1s),
			transform 0.8s var(--npf-ease) calc(var(--npf-i, 0) * 0.1s);
	}

	.npf[data-npf-ready]:not([data-in]) .npf-rise {
		opacity: 0;
		transform: translateY(40px);
	}

	/* ---- tablet ---- */
	@media (min-width: 810px) {
		.npf-main {
			gap: 64px;
			padding: 80px 30px 40px;
		}

		.npf-top {
			flex-direction: row;
			align-items: center;
			justify-content: space-between;
			gap: 40px;
		}

		.npf-heading {
			font-size: 44px;
			line-height: 1.2;
		}

		.npf-form {
			flex: 0 1 443px;
		}

		.npf-field {
			display: block;
		}

		.npf-input {
			height: 56px;
			padding-right: 160px;
		}

		.npf-submit {
			position: absolute;
			top: 0;
			right: 0;
			height: 56px;
		}

		.npf-links {
			gap: 56px;
		}

		.npf-cols {
			grid-template-columns: repeat(3, minmax(0, 1fr));
			gap: 30px;
		}

		.npf--panel .npf-panel {
			padding: 60px 40px;
			border-radius: 40px;
		}

		.npf .npf-list {
			gap: 2px;
		}
	}

	/* ---- desktop: brand block left, three columns right ---- */
	@media (min-width: 1200px) {
		.npf-main {
			gap: 80px;
			padding: 80px 20px 40px;
		}

		.npf--band .npf-main {
			padding-bottom: 88px;
		}

		.npf-heading {
			font-size: 48px;
			line-height: 57.6px;
		}

		.npf-links {
			flex-direction: row;
			justify-content: space-between;
			align-items: flex-start;
			gap: 40px;
		}

		.npf-brandblock {
			flex: 0 0 307px;
			gap: 40px;
		}

		.npf-brandblock .npf-blurb {
			margin-top: -10px;
		}

		.npf-cols {
			flex: 0 1 815px;
			gap: 0;
		}

		.npf--panel .npf-panel {
			padding: 78px 20px;
			border-radius: 50px;
		}

		.npf-top,
		.npf-links {
			max-width: 1224px;
		}
	}

	/* Band variant: the columns keep the reference's 80px rhythm, no panel. */
	.npf--band .npf-panel {
		width: 100%;
	}

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

		.npf-rise {
			transition: none;
		}
	}

	@media (forced-colors: active) {
		.npf-submit,
		.npf-social-link {
			border: 1px solid CanvasText;
		}
	}
</style>

<script>
	/*
	 * Appear-on-scroll for the footer's top content. 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, without
	 * IntersectionObserver, or with animate={false}, nothing ever starts hidden.
	 */
	function initNewsletterPanelFooter(root: HTMLElement) {
		if (root.dataset.npfReady === "") return;
		if (!root.hasAttribute("data-animate")) return;

		const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
		if (reduced || !("IntersectionObserver" in window)) {
			root.dataset.in = "";
			return;
		}

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

		const observer = new IntersectionObserver(
			(entries) => {
				if (entries.some((entry) => entry.isIntersecting)) {
					root.dataset.in = "";
					observer.disconnect();
				}
			},
			{ threshold: 0.12 },
		);
		observer.observe(root);
		signal.addEventListener("abort", () => observer.disconnect());

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

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

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

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

===== END src/components/NewsletterPanelFooter.astro =====

--- SECTIONS ---
- Solid bar header (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/solid-bar-header/
- Circle badge hero (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/circle-badge-hero/
- Icon stat band (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/icon-stat-band/
- Committed about split (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/committed-about-split/
- Service photo cards (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/service-photo-cards/
- Dotted process steps (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/dotted-process-steps/
- Accordion success showcase (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/accordion-success-showcase/
- Team hover cards (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/team-hover-cards/
- Cargo logo marquee (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/cargo-logo-marquee/
- Trust reasons split (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/trust-reasons-split/
- Numbered testimonial switcher (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/numbered-testimonial-switcher/
- Stacked pricing plans (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/stacked-pricing-plans/
- Logistics blog cards (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/logistics-blog-cards/
- Newsletter panel footer (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/newsletter-panel-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

14 sections, top to bottom. Each is an ordinary library component — open one for its props, its own preview and its verification record.

  1. HeaderSolid bar headerin verification
  2. SectionCircle badge heroin verification
  3. SectionIcon stat bandin verification
  4. SectionCommitted about splitin verification
  5. SectionService photo cardsin verification
  6. SectionDotted process stepsin verification
  7. SectionAccordion success showcasein verification
  8. SectionTeam hover cardsin verification
  9. SectionCargo logo marqueein verification
  10. SectionTrust reasons splitin verification
  11. SectionNumbered testimonial switcherin verification
  12. SectionStacked pricing plansin verification
  13. SectionLogistics blog cardsin verification
  14. FooterNewsletter panel footerin verification

38 required files · page file src/pages/transport-home.astro · no npm packages